respect 'don't @ me' requests

staging
multiple creatures 2019-08-03 13:47:20 -05:00
parent cd52f75006
commit b644f1c505
12 changed files with 82 additions and 11 deletions

View File

@ -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')} />
)}

View File

@ -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>

View File

@ -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,
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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],

View File

@ -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]

View File

@ -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}"'

View File

@ -0,0 +1,5 @@
class AddRejectRepliesToStatuses < ActiveRecord::Migration[5.2]
def change
add_column :statuses, :reject_replies, :boolean
end
end

View File

@ -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"