mirror of
https://github.com/yingziwu/mastodon.git
synced 2026-02-26 12:12:43 +00:00
Merge tag 'v4.2.11'
This commit is contained in:
commit
f86d63458d
43 changed files with 212 additions and 66 deletions
|
|
@ -20,7 +20,7 @@ export function changeSetting(path, value) {
|
|||
}
|
||||
|
||||
const debouncedSave = debounce((dispatch, getState) => {
|
||||
if (getState().getIn(['settings', 'saved'])) {
|
||||
if (getState().getIn(['settings', 'saved']) || !getState().getIn(['meta', 'me'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class ReportReasonSelector extends PureComponent {
|
|||
|
||||
api().put(`/api/v1/admin/reports/${id}`, {
|
||||
category,
|
||||
rule_ids,
|
||||
rule_ids: category === 'violation' ? rule_ids : [],
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ class DropdownMenu extends PureComponent {
|
|||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -274,6 +274,7 @@ class Search extends PureComponent {
|
|||
}
|
||||
|
||||
_calculateOptions (value) {
|
||||
const { signedIn } = this.context.identity;
|
||||
const trimmedValue = value.trim();
|
||||
const options = [];
|
||||
|
||||
|
|
@ -298,7 +299,7 @@ class Search extends PureComponent {
|
|||
|
||||
const couldBeStatusSearch = searchEnabled;
|
||||
|
||||
if (couldBeStatusSearch) {
|
||||
if (couldBeStatusSearch && signedIn) {
|
||||
options.push({ key: 'status-search', label: <FormattedMessage id='search.quick_action.status_search' defaultMessage='Posts matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleStatusSearch });
|
||||
}
|
||||
|
||||
|
|
@ -375,7 +376,7 @@ class Search extends PureComponent {
|
|||
|
||||
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
|
||||
|
||||
{searchEnabled ? (
|
||||
{searchEnabled && signedIn ? (
|
||||
<div className='search__popout__menu'>
|
||||
{this.defaultOptions.map(({ key, label, action }, i) => (
|
||||
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === ((options.length || recent.size) + i) })}>
|
||||
|
|
@ -385,7 +386,11 @@ class Search extends PureComponent {
|
|||
</div>
|
||||
) : (
|
||||
<div className='search__popout__menu__message'>
|
||||
<FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
|
||||
{searchEnabled ? (
|
||||
<FormattedMessage id='search_popout.full_text_search_logged_out_message' defaultMessage='Only available when logged in.' />
|
||||
) : (
|
||||
<FormattedMessage id='search_popout.full_text_search_disabled_message' defaultMessage='Not available on {domain}.' values={{ domain }} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -221,7 +221,7 @@ class FocalPointModal extends ImmutablePureComponent {
|
|||
const worker = createWorker({
|
||||
workerPath: tesseractWorkerPath,
|
||||
corePath: tesseractCorePath,
|
||||
langPath: `${assetHost}/ocr/lang-data/`,
|
||||
langPath: `${assetHost}/ocr/lang-data`,
|
||||
logger: ({ status, progress }) => {
|
||||
if (status === 'recognizing text') {
|
||||
this.setState({ ocrStatus: 'detecting', progress });
|
||||
|
|
|
|||
|
|
@ -591,6 +591,7 @@
|
|||
"search.quick_action.status_search": "Posts matching {x}",
|
||||
"search.search_or_paste": "Search or paste URL",
|
||||
"search_popout.full_text_search_disabled_message": "Not available on {domain}.",
|
||||
"search_popout.full_text_search_logged_out_message": "Only available when logged in.",
|
||||
"search_popout.language_code": "ISO language code",
|
||||
"search_popout.options": "Search options",
|
||||
"search_popout.quick_actions": "Quick actions",
|
||||
|
|
|
|||
|
|
@ -332,13 +332,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
|||
|
||||
def fetch_replies(status)
|
||||
collection = @object['replies']
|
||||
return if collection.nil?
|
||||
return if collection.blank?
|
||||
|
||||
replies = ActivityPub::FetchRepliesService.new.call(status, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
|
||||
return unless replies.nil?
|
||||
|
||||
uri = value_or_id(collection)
|
||||
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri, { 'request_id' => @options[:request_id] }) unless uri.nil?
|
||||
rescue => e
|
||||
Rails.logger.warn "Error fetching replies: #{e}"
|
||||
end
|
||||
|
||||
def conversation_from_uri(uri)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
|
|||
serialized_hash = serialized_hash.select { |k, _| options[:fields].include?(k) } if options[:fields]
|
||||
serialized_hash = self.class.transform_key_casing!(serialized_hash, instance_options)
|
||||
|
||||
{ '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
|
||||
{ '@context': serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ class VideoMetadataExtractor
|
|||
@colorspace = video_stream[:pix_fmt]
|
||||
@width = video_stream[:width]
|
||||
@height = video_stream[:height]
|
||||
@frame_rate = video_stream[:avg_frame_rate] == '0/0' ? nil : Rational(video_stream[:avg_frame_rate])
|
||||
@r_frame_rate = video_stream[:r_frame_rate] == '0/0' ? nil : Rational(video_stream[:r_frame_rate])
|
||||
@frame_rate = parse_framerate(video_stream[:avg_frame_rate])
|
||||
@r_frame_rate = parse_framerate(video_stream[:r_frame_rate])
|
||||
# For some video streams the frame_rate reported by `ffprobe` will be 0/0, but for these streams we
|
||||
# should use `r_frame_rate` instead. Video screencast generated by Gnome Screencast have this issue.
|
||||
@frame_rate ||= @r_frame_rate
|
||||
|
|
@ -55,4 +55,10 @@ class VideoMetadataExtractor
|
|||
|
||||
@invalid = true if @metadata.key?(:error)
|
||||
end
|
||||
|
||||
def parse_framerate(raw)
|
||||
Rational(raw)
|
||||
rescue ZeroDivisionError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ class Webfinger
|
|||
class RedirectError < Error; end
|
||||
|
||||
class Response
|
||||
ACTIVITYPUB_READY_TYPE = ['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].freeze
|
||||
|
||||
attr_reader :uri
|
||||
|
||||
def initialize(uri, body)
|
||||
|
|
@ -20,17 +22,28 @@ class Webfinger
|
|||
end
|
||||
|
||||
def link(rel, attribute)
|
||||
links.dig(rel, attribute)
|
||||
links.dig(rel, 0, attribute)
|
||||
end
|
||||
|
||||
def self_link_href
|
||||
self_link.fetch('href')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def links
|
||||
@links ||= @json['links'].index_by { |link| link['rel'] }
|
||||
@links ||= @json.fetch('links', []).group_by { |link| link['rel'] }
|
||||
end
|
||||
|
||||
def self_link
|
||||
links.fetch('self', []).find do |link|
|
||||
ACTIVITYPUB_READY_TYPE.include?(link['type'])
|
||||
end
|
||||
end
|
||||
|
||||
def validate_response!
|
||||
raise Webfinger::Error, "Missing subject in response for #{@uri}" if subject.blank?
|
||||
raise Webfinger::Error, "Missing self link in response for #{@uri}" if self_link.blank?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class Account < ApplicationRecord
|
|||
BACKGROUND_REFRESH_INTERVAL = 1.week.freeze
|
||||
|
||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i
|
||||
MENTION_RE = %r{(?<![=/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i
|
||||
MENTION_RE = %r{(?<![=/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}
|
||||
URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
|
||||
USERNAME_ONLY_RE = /\A#{USERNAME_RE}\z/i
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ module LdapAuthenticable
|
|||
safe_username = safe_username.gsub(keys, replacement)
|
||||
end
|
||||
|
||||
resource = joins(:account).find_by(accounts: { username: safe_username })
|
||||
resource = joins(:account).merge(Account.where(Account.arel_table[:username].lower.eq safe_username.downcase)).take
|
||||
|
||||
if resource.blank?
|
||||
resource = new(email: attributes[Devise.ldap_mail.to_sym].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ class Report < ApplicationRecord
|
|||
delegate :local?, to: :account
|
||||
|
||||
validates :comment, length: { maximum: 1_000 }, if: :local?
|
||||
validates :rule_ids, absence: true, unless: :violation?
|
||||
validates :rule_ids, absence: true, if: -> { (category_changed? || rule_ids_changed?) && !violation? }
|
||||
|
||||
validate :validate_rule_ids
|
||||
validate :validate_rule_ids, if: -> { (category_changed? || rule_ids_changed?) && violation? }
|
||||
|
||||
# entries here need to be kept in sync with the front-end:
|
||||
# - app/javascript/mastodon/features/notifications/components/report.jsx
|
||||
|
|
@ -154,8 +154,6 @@ class Report < ApplicationRecord
|
|||
end
|
||||
|
||||
def validate_rule_ids
|
||||
return unless violation?
|
||||
|
||||
errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids&.size
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class Tag < ApplicationRecord
|
|||
HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
|
||||
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}"
|
||||
|
||||
HASHTAG_RE = %r{(?<![=/)\w])#(#{HASHTAG_NAME_PAT})}i
|
||||
HASHTAG_RE = %r{(?<![=/)\p{Alnum}])#(#{HASHTAG_NAME_PAT})}
|
||||
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i
|
||||
HASHTAG_INVALID_CHARS_RE = /[^[:alnum:]#{HASHTAG_SEPARATORS}]/
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
|
|||
end
|
||||
|
||||
def process_items(items)
|
||||
return if items.nil?
|
||||
|
||||
process_note_items(items) if @options[:note]
|
||||
process_hashtag_items(items) if @options[:hashtag]
|
||||
end
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class ActivityPub::FetchRemoteActorService < BaseService
|
|||
confirmed_username, confirmed_domain = split_acct(webfinger.subject)
|
||||
|
||||
if @username.casecmp(confirmed_username).zero? && @domain.casecmp(confirmed_domain).zero?
|
||||
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
|
||||
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.self_link_href != @uri
|
||||
|
||||
return
|
||||
end
|
||||
|
|
@ -58,8 +58,7 @@ class ActivityPub::FetchRemoteActorService < BaseService
|
|||
@username, @domain = split_acct(webfinger.subject)
|
||||
|
||||
raise Webfinger::RedirectError, "Too many webfinger redirects for URI #{@uri} (stopped at #{@username}@#{@domain})" unless confirmed_username.casecmp(@username).zero? && confirmed_domain.casecmp(@domain).zero?
|
||||
|
||||
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.link('self', 'href') != @uri
|
||||
raise Error, "Webfinger response for #{@username}@#{@domain} does not loop back to #{@uri}" if webfinger.self_link_href != @uri
|
||||
rescue Webfinger::RedirectError => e
|
||||
raise Error, e.message
|
||||
rescue Webfinger::Error => e
|
||||
|
|
|
|||
|
|
@ -172,9 +172,9 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||
|
||||
as_array(@json['tag']).each do |tag|
|
||||
if equals_or_includes?(tag['type'], 'Hashtag')
|
||||
@raw_tags << tag['name']
|
||||
@raw_tags << tag['name'] if tag['name'].present?
|
||||
elsif equals_or_includes?(tag['type'], 'Mention')
|
||||
@raw_mentions << tag['href']
|
||||
@raw_mentions << tag['href'] if tag['href'].present?
|
||||
elsif equals_or_includes?(tag['type'], 'Emoji')
|
||||
@raw_emojis << tag
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ class BackupService < BaseService
|
|||
|
||||
def build_outbox_json!(file)
|
||||
skeleton = serialize(collection_presenter, ActivityPub::CollectionSerializer)
|
||||
skeleton['@context'] = full_context
|
||||
skeleton['orderedItems'] = ['!PLACEHOLDER!']
|
||||
skeleton[:@context] = full_context
|
||||
skeleton[:orderedItems] = ['!PLACEHOLDER!']
|
||||
skeleton = Oj.dump(skeleton)
|
||||
prepend, append = skeleton.split('"!PLACEHOLDER!"')
|
||||
add_comma = false
|
||||
|
|
@ -33,7 +33,7 @@ class BackupService < BaseService
|
|||
|
||||
file.write(statuses.map do |status|
|
||||
item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer)
|
||||
item.delete('@context')
|
||||
item.delete(:@context)
|
||||
|
||||
unless item[:type] == 'Announce' || item[:object][:attachment].blank?
|
||||
item[:object][:attachment].each do |attachment|
|
||||
|
|
|
|||
|
|
@ -86,7 +86,8 @@ class NotifyService < BaseService
|
|||
end
|
||||
|
||||
def from_staff?
|
||||
@notification.from_account.local? && @notification.from_account.user.present? && @notification.from_account.user_role&.overrides?(@recipient.user_role)
|
||||
sender = @notification.from_account
|
||||
sender.local? && sender.user.present? && sender.user_role&.overrides?(@recipient.user_role) && @sender.user_role&.highlighted? && sender.user_role&.can?(*UserRole::Flags::CATEGORIES[:moderation])
|
||||
end
|
||||
|
||||
def optional_non_following_and_direct?
|
||||
|
|
|
|||
|
|
@ -104,8 +104,6 @@ class ResolveAccountService < BaseService
|
|||
end
|
||||
|
||||
def fetch_account!
|
||||
return unless activitypub_ready?
|
||||
|
||||
with_redis_lock("resolve:#{@username}@#{@domain}") do
|
||||
@account = ActivityPub::FetchRemoteAccountService.new.call(actor_url, suppress_errors: @options[:suppress_errors])
|
||||
end
|
||||
|
|
@ -120,12 +118,8 @@ class ResolveAccountService < BaseService
|
|||
@options[:skip_cache] || @account.nil? || @account.possibly_stale?
|
||||
end
|
||||
|
||||
def activitypub_ready?
|
||||
['application/activity+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'].include?(@webfinger.link('self', 'type'))
|
||||
end
|
||||
|
||||
def actor_url
|
||||
@actor_url ||= @webfinger.link('self', 'href')
|
||||
@actor_url ||= @webfinger.self_link_href
|
||||
end
|
||||
|
||||
def gone_from_origin?
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
.fields-group
|
||||
= f.input :shortcode, wrapper: :with_label, label: t('admin.custom_emojis.shortcode'), hint: t('admin.custom_emojis.shortcode_hint')
|
||||
.fields-group
|
||||
= f.input :image, wrapper: :with_label, input_html: { accept: CustomEmoji::IMAGE_MIME_TYPES.join(' ') }, hint: t('admin.custom_emojis.image_hint', size: number_to_human_size(CustomEmoji::LIMIT))
|
||||
= f.input :image, wrapper: :with_label, input_html: { accept: CustomEmoji::IMAGE_MIME_TYPES.join(',') }, hint: t('admin.custom_emojis.image_hint', size: number_to_human_size(CustomEmoji::LIMIT))
|
||||
|
||||
.actions
|
||||
= f.button :button, t('admin.custom_emojis.upload'), type: :submit
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue