diff options
-rw-r--r-- | app/javascript/flavours/glitch/components/status_icons.js | 3 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/status/components/detailed_status.js | 11 | ||||
-rw-r--r-- | app/lib/activitypub/activity/create.rb | 11 | ||||
-rw-r--r-- | app/lib/activitypub/adapter.rb | 4 | ||||
-rw-r--r-- | app/models/status.rb | 14 | ||||
-rw-r--r-- | app/serializers/activitypub/note_serializer.rb | 10 | ||||
-rw-r--r-- | app/serializers/rest/status_serializer.rb | 6 | ||||
-rw-r--r-- | app/services/post_status_service.rb | 24 | ||||
-rw-r--r-- | app/workers/post_status_worker.rb | 1 | ||||
-rw-r--r-- | config/locales/en.yml | 1 | ||||
-rw-r--r-- | db/migrate/20190803170051_add_reject_replies_to_statuses.rb | 5 | ||||
-rw-r--r-- | db/schema.rb | 3 |
12 files changed, 82 insertions, 11 deletions
diff --git a/app/javascript/flavours/glitch/components/status_icons.js b/app/javascript/flavours/glitch/components/status_icons.js index 9a3b2b745..08ebfaea9 100644 --- a/app/javascript/flavours/glitch/components/status_icons.js +++ b/app/javascript/flavours/glitch/components/status_icons.js @@ -62,6 +62,9 @@ export default class StatusIcons extends React.PureComponent { {status.get('delete_after') ? ( <i className='fa fa-clock-o' title={new Date(status.get('delete_after'))} aria-hidden='true' /> ) : null} + {status.get('reject_replies') ? ( + <i className='fa fa-microphone-slash' title='Rejecting replies' aria-hidden='true' /> + ) : null} {( <VisibilityIcon visibility={status.get('visibility')} /> )} diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index f8f5e053c..8a332b33b 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -129,6 +129,7 @@ export default class DetailedStatus extends ImmutablePureComponent { let favouriteLink = ''; let sharekeyLinks = ''; let destructIcon = ''; + let rejectIcon = ''; if (this.props.measureHeight) { outerStyle.height = `${this.state.height}px`; @@ -251,6 +252,14 @@ export default class DetailedStatus extends ImmutablePureComponent { ) } + if (status.get('reject_replies')) { + rejectIcon = ( + <span> + <i className='fa fa-microphone-slash' title='Rejecting replies' /> · + </span> + ) + } + return ( <div style={outerStyle}> <div ref={this.setRef} className={classNames('detailed-status', { compact })} data-status-by={status.getIn(['account', 'acct'])}> @@ -272,7 +281,7 @@ export default class DetailedStatus extends ImmutablePureComponent { /> <div className='detailed-status__meta'> - {sharekeyLinks} {reblogLink} · {favouriteLink} · {destructIcon} <VisibilityIcon visibility={status.get('visibility')} /> + {sharekeyLinks} {reblogLink} · {favouriteLink} · {destructIcon} {rejectIcon} <VisibilityIcon visibility={status.get('visibility')} /> <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> </a> diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 1c20d3844..140226481 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -46,7 +46,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity @params = {} process_status_params - return reject_payload! if twitter_retweet? + return reject_payload! if twitter_retweet? || recipient_rejects_replies? process_tags process_audience @@ -86,7 +86,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def twitter_retweet? - @params[:text] =~ /^RT / || '🐦🔗:'.in?(@params[:text]) + @params[:text] =~ /^(?:<p> *)?RT / || '🐦🔗:'.in?(@params[:text]) + end + + def recipient_rejects_replies? + @params[:thread].present? && + @params[:thread]&.reject_replies && + @params[:thread]&.account_id != @account.id end def process_status_params @@ -105,6 +111,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity visibility: visibility_from_audience, thread: replied_to_status, conversation: conversation_from_uri(@object['conversation']), + reject_replies: @object['rejectReplies'] || false, media_attachment_ids: process_attachments.take(6).map(&:id), poll: process_poll, } diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb index 73e2f271b..ec352f367 100644 --- a/app/lib/activitypub/adapter.rb +++ b/app/lib/activitypub/adapter.rb @@ -40,6 +40,10 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base 'mp' => 'https://monsterpit.net/ns#', 'froze' => 'mp:froze' }, + reject_replies: { + 'mp' => 'https://monsterpit.net/ns#', + 'rejectReplies' => 'mp:rejectReplies', + } }.freeze def self.default_key_transform diff --git a/app/models/status.rb b/app/models/status.rb index 5fba7b38a..42de5d4c0 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -32,6 +32,7 @@ # imported :boolean # origin :string # boostable :boolean +# reject_replies :boolean # class Status < ApplicationRecord @@ -46,6 +47,7 @@ class Status < ApplicationRecord # match both with and without U+FE0F (the emoji variation selector) LOCAL_ONLY_TOKENS = /(?:#!|\u{1f441}\ufe0f?)\u200b?\z/ + REJECT_REPLIES_TOKENS = /\b(?:\:ms_dont_at_me\:|no replies|(?:don't|do not) (?:@|at|reply)(?: (?:me|us))?)\b/i # If `override_timestamps` is set at creation time, Snowflake ID creation # will be based on current time instead of `created_at` @@ -318,6 +320,7 @@ class Status < ApplicationRecord before_validation :set_visibility before_validation :set_conversation before_validation :set_local + before_validation :infer_reject_replies after_create :set_poll_id after_create :process_bangtags, if: :local? @@ -544,6 +547,13 @@ class Status < ApplicationRecord LOCAL_ONLY_TOKENS.match?(content) end + def marked_reject_replies? + return true if reject_replies + return true if spoiler_text.present? && REJECT_REPLIES_TOKENS.match?(spoiler_text) + return true if content.present? && REJECT_REPLIES_TOKENS.match?(content.lines.first) + content.present? && REJECT_REPLIES_TOKENS.match?(content.lines.last) + end + private def update_status_stat!(attrs) @@ -581,6 +591,10 @@ class Status < ApplicationRecord end end + def infer_reject_replies + self.reject_replies = marked_reject_replies? + end + def process_bangtags Bangtags.new(self).process end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 56975ed80..ff39c97c4 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -2,12 +2,14 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer context_extensions :conversation, :sensitive, :big, - :hashtag, :emoji, :focal_point, :blurhash + :hashtag, :emoji, :focal_point, :blurhash, + :reject_replies attributes :id, :type, :summary, :in_reply_to, :published, :url, :attributed_to, :to, :cc, :sensitive, - :conversation, :source, :tails_never_fail + :conversation, :source, :tails_never_fail, + :reject_replies attribute :content attribute :content_map, if: :language? @@ -143,6 +145,10 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer object.preloadable_poll&.expired? end + def reject_replies + object.reject_replies == true + end + def tails_never_fail true end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 58ee81d1a..7146d8108 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :spoiler_text, :visibility, :language, :uri, :url, :replies_count, :reblogs_count, - :favourites_count, :network, :curated + :favourites_count, :network, :curated, :reject_replies attribute :favourited, if: :current_user? attribute :reblogged, if: :current_user? @@ -140,6 +140,10 @@ class REST::StatusSerializer < ActiveModel::Serializer object.delete_after end + def reject_replies + object.reject_replies == true + end + class ApplicationSerializer < ActiveModel::Serializer attributes :name, :website end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 0dd2841e5..04eda5f33 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -30,6 +30,7 @@ class PostStatusService < BaseService # @option [String] :language # @option [String] :scheduled_at # @option [String] :delete_after + # @option [Boolean] :noreplies Author does not accept replies # @option [Boolean] :nocrawl Optional skip link card generation # @option [Boolean] :nomentions Optional skip mention processing # @option [Boolean] :delayed Optional publishing delay of 30 secs @@ -49,6 +50,8 @@ class PostStatusService < BaseService @sensitive = (@account.force_sensitive? ? true : @options[:sensitive]) @preloaded_tags = @options[:preloaded_tags] || [] + raise Mastodon::LengthValidationError, I18n.t('statuses.replies_rejected') if recipient_rejects_replies? + return idempotency_duplicate if idempotency_given? && idempotency_duplicate? validate_media! @@ -69,6 +72,7 @@ class PostStatusService < BaseService nocrawl: @options[:nocrawl], nomentions: @options[:nomentions], delete_after: @delete_after.nil? ? nil : @delete_after + 1.minute, + reject_replies: @options[:noreplies] || false, }.compact PostStatusWorker.perform_at(delay_until, @status.id, opts) @@ -86,6 +90,10 @@ class PostStatusService < BaseService private + def recipient_rejects_replies? + @in_reply_to.present? && @in_reply_to.reject_replies && @in_reply_to.account_id != @account.id + end + def set_footer_from_i_am return if @footer.present? || @options[:no_footer] name = @account.user.vars['_they:are'] @@ -102,16 +110,19 @@ class PostStatusService < BaseService end def limit_visibility_to_reply - return if @in_reply_to.nil? @visibility = @in_reply_to.visibility if @visibility.nil? || VISIBILITY_RANK[@visibility] < VISIBILITY_RANK[@in_reply_to.visibility] end def unfilter_thread_on_reply - return if @in_reply_to.nil? Redis.current.srem("filtered_threads:#{@account.id}", @in_reply_to.conversation_id) end + def inherit_reply_rejection + return unless @in_reply_to.reject_replies && @in_reply_to.account_id == @account.id + @options[:noreplies] = true + end + def set_local_only @local_only = true if @account.user_always_local_only? || @in_reply_to&.local_only end @@ -140,8 +151,12 @@ class PostStatusService < BaseService set_local_only set_initial_visibility limit_visibility_if_silenced - limit_visibility_to_reply - unfilter_thread_on_reply + + unless @in_reply_to.nil? + inherit_reply_rejection + limit_visibility_to_reply + unfilter_thread_on_reply + end @sensitive = (@account.user_defaults_to_sensitive? || @options[:spoiler_text].present?) if @sensitive.nil? @@ -280,6 +295,7 @@ class PostStatusService < BaseService visibility: @visibility, local_only: @local_only, delete_after: @delete_after, + reject_replies: @options[:noreplies] || false, sharekey: @sharekey, language: language_from_option(@options[:language]) || @account.user_default_language&.presence || 'en', application: @options[:application], diff --git a/app/workers/post_status_worker.rb b/app/workers/post_status_worker.rb index 630d83fae..99f2c2e99 100644 --- a/app/workers/post_status_worker.rb +++ b/app/workers/post_status_worker.rb @@ -11,6 +11,7 @@ class PostStatusWorker status.visibility = options[:visibility] if options[:visibility] status.local_only = options[:local_only] if options[:local_only] + status.reject_replies = options[:reject_replies] if options[:reject_replies] status.save! process_mentions_service.call(status) unless options[:nomentions] diff --git a/config/locales/en.yml b/config/locales/en.yml index c9a363ec4..95b20571a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -956,6 +956,7 @@ en: one: "%{count} vote" other: "%{count} votes" vote: Vote + replies_rejected: 'The author is not accepting replies to this roar.' show_more: Show more sign_in_to_participate: Sign in to participate in the conversation title: '%{name}: "%{quote}"' diff --git a/db/migrate/20190803170051_add_reject_replies_to_statuses.rb b/db/migrate/20190803170051_add_reject_replies_to_statuses.rb new file mode 100644 index 000000000..b10c6ec00 --- /dev/null +++ b/db/migrate/20190803170051_add_reject_replies_to_statuses.rb @@ -0,0 +1,5 @@ +class AddRejectRepliesToStatuses < ActiveRecord::Migration[5.2] + def change + add_column :statuses, :reject_replies, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 9197404aa..2ff7cfc28 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_01_222823) do +ActiveRecord::Schema.define(version: 2019_08_03_170051) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -669,6 +669,7 @@ ActiveRecord::Schema.define(version: 2019_08_01_222823) do t.string "origin" t.tsvector "tsv" t.boolean "boostable" + t.boolean "reject_replies" t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc } t.index ["account_id", "id", "visibility"], name: "index_statuses_on_account_id_and_id_and_visibility", order: { id: :desc }, where: "(visibility = ANY (ARRAY[0, 1, 2, 4]))" t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id" |