Skip to content

Commit

Permalink
Add conversation-based forwarding for limited visibility statuses thr…
Browse files Browse the repository at this point in the history
…ough bearcaps
  • Loading branch information
Gargron authored and noellabo committed Sep 5, 2020
1 parent 561abc6 commit 66b8396
Show file tree
Hide file tree
Showing 26 changed files with 430 additions and 78 deletions.
16 changes: 16 additions & 0 deletions app/controllers/activitypub/contexts_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

class ActivityPub::ContextsController < ActivityPub::BaseController
before_action :set_conversation

def show
expires_in 3.minutes, public: public_fetch_mode?
render_with_cache json: @conversation, serializer: ActivityPub::ContextSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end

private

def set_conversation
@conversation = Conversation.local.find(params[:id])
end
end
2 changes: 1 addition & 1 deletion app/controllers/concerns/cache_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def render_with_cache(**options)
end

def set_cache_headers
response.headers['Vary'] = public_fetch_mode? ? 'Accept' : 'Accept, Signature'
response.headers['Vary'] = public_fetch_mode? ? 'Accept, Authorization' : 'Accept, Signature, Authorization'
end

def cache_collection(raw, klass)
Expand Down
7 changes: 6 additions & 1 deletion app/controllers/statuses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,12 @@ def set_link_headers

def set_status
@status = @account.statuses.find(params[:id])
authorize @status, :show?

if request.authorization.present? && request.authorization.match(/^Bearer /i)
raise Mastodon::NotPermittedError unless @status.capability_tokens.find_by(token: request.authorization.gsub(/^Bearer /i, ''))
else
authorize @status, :show?
end
rescue Mastodon::NotPermittedError
not_found
end
Expand Down
11 changes: 5 additions & 6 deletions app/helpers/jsonld_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,12 @@ def unsupported_uri_scheme?(uri)
!uri.start_with?('http://', 'https://')
end

def invalid_origin?(url)
return true if unsupported_uri_scheme?(url)

needle = Addressable::URI.parse(url).host
haystack = Addressable::URI.parse(@account.uri).host
def same_origin?(url_a, url_b)
Addressable::URI.parse(url_a).host.casecmp(Addressable::URI.parse(url_b).host).zero?
end

!haystack.casecmp(needle).zero?
def invalid_origin?(url)
unsupported_uri_scheme?(url) || !same_origin?(url, @account.uri)
end

def canonicalize(json)
Expand Down
57 changes: 48 additions & 9 deletions app/lib/activitypub/activity/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def process_status
fetch_replies(@status)
check_for_spam
distribute(@status)
forward_for_conversation
forward_for_reply
end

Expand All @@ -114,16 +115,18 @@ def process_status_params
sensitive: @object['sensitive'] || false,
visibility: visibility_from_audience,
thread: replied_to_status,
conversation: conversation_from_uri(@object['conversation']),
conversation: conversation_from_context,
media_attachment_ids: process_attachments.take(4).map(&:id),
poll: process_poll,
}
end
end

def process_audience
conversation_uri = value_or_id(@object['context'])

(audience_to + audience_cc).uniq.each do |audience|
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]
next if audience == ActivityPub::TagManager::COLLECTIONS[:public] || audience == conversation_uri

# Unlike with tags, there is no point in resolving accounts we don't already
# know here, because silent mentions would only be used for local access
Expand Down Expand Up @@ -340,15 +343,45 @@ def fetch_replies(status)
ActivityPub::FetchRepliesWorker.perform_async(status.id, uri) unless uri.nil?
end

def conversation_from_uri(uri)
return nil if uri.nil?
return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri)
def conversation_from_context
atom_uri = @object['conversation']

begin
Conversation.find_or_create_by!(uri: uri)
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
retry
conversation = begin
if atom_uri.present? && OStatus::TagManager.instance.local_id?(atom_uri)
Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(atom_uri, 'Conversation'))
elsif atom_uri.present? && @object['context'].present?
Conversation.find_by(uri: atom_uri)
elsif atom_uri.present?
begin
Conversation.find_or_create_by!(uri: atom_uri)
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
retry
end
end
end

return conversation if @object['context'].nil?

uri = value_or_id(@object['context'])
conversation ||= ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation)

return conversation if (conversation.present? && conversation.uri == uri) || !uri.start_with?('https://')

conversation_json = begin
if @object['context'].is_a?(Hash) && !invalid_origin?(uri)
@object['context']
else
fetch_resource(uri, true)
end
end

return conversation if conversation_json.blank?

conversation ||= Conversation.new
conversation.uri = uri
conversation.inbox_url = conversation_json['inbox']
conversation.save! if conversation.changed?
conversation
end

def visibility_from_audience
Expand Down Expand Up @@ -492,6 +525,12 @@ def check_for_spam
SpamCheck.perform(@status)
end

def forward_for_conversation
return unless audience_to.include?(value_or_id(@object['context'])) && @json['signature'].present? && @status.conversation.local?

ActivityPub::ForwardDistributionWorker.perform_async(@status.conversation_id, Oj.dump(@json))
end

def forward_for_reply
return unless @status.distributable? && @json['signature'].present? && reply_to_local?

Expand Down
22 changes: 16 additions & 6 deletions app/lib/activitypub/tag_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ def url_for(target)
when :person
target.instance_actor? ? about_more_url(instance_actor: true) : short_account_url(target)
when :note, :comment, :activity
return activity_account_status_url(target.account, target) if target.reblog?
short_account_status_url(target.account, target)
if target.reblog?
activity_account_status_url(target.account, target)
else
short_account_status_url(target.account, target)
end
end
end

Expand All @@ -33,10 +36,15 @@ def uri_for(target)
when :person
target.instance_actor? ? instance_actor_url : account_url(target)
when :note, :comment, :activity
return activity_account_status_url(target.account, target) if target.reblog?
account_status_url(target.account, target)
if target.reblog?
activity_account_status_url(target.account, target)
else
account_status_url(target.account, target)
end
when :emoji
emoji_url(target)
when :conversation
context_url(target)
end
end

Expand Down Expand Up @@ -66,7 +74,9 @@ def to(status)
[COLLECTIONS[:public]]
when 'unlisted', 'private'
[account_followers_url(status.account)]
when 'direct', 'limited'
when 'limited'
status.conversation_id.present? ? [uri_for(status.conversation)] : []
when 'direct'
if status.account.silenced?
# Only notify followers if the account is locally silenced
account_ids = status.active_mentions.pluck(:account_id)
Expand Down Expand Up @@ -104,7 +114,7 @@ def cc(status)
cc << COLLECTIONS[:public]
end

unless status.direct_visibility? || status.limited_visibility?
unless status.direct_visibility?
if status.account.silenced?
# Only notify followers if the account is locally silenced
account_ids = status.active_mentions.pluck(:account_id)
Expand Down
36 changes: 31 additions & 5 deletions app/models/conversation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,44 @@
#
# Table name: conversations
#
# id :bigint(8) not null, primary key
# uri :string
# created_at :datetime not null
# updated_at :datetime not null
# id :bigint(8) not null, primary key
# uri :string
# created_at :datetime not null
# updated_at :datetime not null
# parent_status_id :bigint(8)
# parent_account_id :bigint(8)
# inbox_url :string
#

class Conversation < ApplicationRecord
validates :uri, uniqueness: true, if: :uri?

has_many :statuses
belongs_to :parent_status, class_name: 'Status', optional: true, inverse_of: :conversation
belongs_to :parent_account, class_name: 'Account', optional: true

has_many :statuses, inverse_of: :conversation

scope :local, -> { where(uri: nil) }

before_validation :set_parent_account, on: :create

after_create :set_conversation_on_parent_status

def local?
uri.nil?
end

def object_type
:conversation
end

private

def set_parent_account
self.parent_account = parent_status.account if parent_status.present?
end

def set_conversation_on_parent_status
parent_status.update_column(:conversation_id, id) if parent_status.present?
end
end
21 changes: 13 additions & 8 deletions app/models/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,11 @@ class Status < ApplicationRecord

belongs_to :account, inverse_of: :statuses
belongs_to :in_reply_to_account, foreign_key: 'in_reply_to_account_id', class_name: 'Account', optional: true
belongs_to :conversation, optional: true
belongs_to :conversation, optional: true, inverse_of: :statuses
belongs_to :preloadable_poll, class_name: 'Poll', foreign_key: 'poll_id', optional: true

has_one :owned_conversation, class_name: 'Conversation', foreign_key: 'parent_status_id', inverse_of: :parent_status

belongs_to :thread, foreign_key: 'in_reply_to_id', class_name: 'Status', inverse_of: :replies, optional: true
belongs_to :reblog, foreign_key: 'reblog_of_id', class_name: 'Status', inverse_of: :reblogs, optional: true

Expand All @@ -63,6 +65,7 @@ class Status < ApplicationRecord
has_many :mentions, dependent: :destroy, inverse_of: :status
has_many :active_mentions, -> { active }, class_name: 'Mention', inverse_of: :status
has_many :media_attachments, dependent: :nullify
has_many :capability_tokens, class_name: 'StatusCapabilityToken', inverse_of: :status, dependent: :destroy

has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards
Expand Down Expand Up @@ -205,7 +208,9 @@ def distributable?
public_visibility? || unlisted_visibility?
end

alias sign? distributable?
def sign?
distributable? || limited_visibility?
end

def with_media?
media_attachments.any?
Expand Down Expand Up @@ -264,11 +269,11 @@ def decrement_count!(key)

around_create Mastodon::Snowflake::Callbacks

before_validation :prepare_contents, if: :local?
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_conversation
before_validation :set_local
before_validation :prepare_contents, on: :create, if: :local?
before_validation :set_reblog, on: :create
before_validation :set_visibility, on: :create
before_validation :set_conversation, on: :create
before_validation :set_local, on: :create

after_create :set_poll_id

Expand Down Expand Up @@ -464,7 +469,7 @@ def set_conversation
self.in_reply_to_account_id = carried_over_reply_to_account_id
self.conversation_id = thread.conversation_id if conversation_id.nil?
elsif conversation_id.nil?
self.conversation = Conversation.new
build_owned_conversation
end
end

Expand Down
25 changes: 25 additions & 0 deletions app/models/status_capability_token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

# == Schema Information
#
# Table name: status_capability_tokens
#
# id :bigint(8) not null, primary key
# status_id :bigint(8)
# token :string
# created_at :datetime not null
# updated_at :datetime not null
#
class StatusCapabilityToken < ApplicationRecord
belongs_to :status

validates :token, presence: true

before_validation :generate_token, on: :create

private

def generate_token
self.token = Doorkeeper::OAuth::Helpers::UniqueToken.generate
end
end
2 changes: 2 additions & 0 deletions app/presenters/activitypub/activity_presenter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ def from_status(status)
else
ActivityPub::TagManager.instance.uri_for(status.proper)
end
elsif status.limited_visibility?
"bear:?#{{ u: ActivityPub::TagManager.instance.uri_for(status.proper), t: status.capability_tokens.first.token }.to_query}"
else
status.proper
end
Expand Down
19 changes: 19 additions & 0 deletions app/serializers/activitypub/context_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

class ActivityPub::ContextSerializer < ActivityPub::Serializer
include RoutingHelper

attributes :id, :type, :inbox

def id
ActivityPub::TagManager.instance.uri_for(object)
end

def type
'Group'
end

def inbox
account_inbox_url(object.parent_account)
end
end
8 changes: 7 additions & 1 deletion app/serializers/activitypub/note_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
:in_reply_to, :published, :url,
:attributed_to, :to, :cc, :sensitive,
:atom_uri, :in_reply_to_atom_uri,
:conversation
:conversation, :context

attribute :content
attribute :content_map, if: :language?
Expand Down Expand Up @@ -121,6 +121,12 @@ def conversation
end
end

def context
return if object.conversation.nil?

ActivityPub::TagManager.instance.uri_for(object.conversation)
end

def local?
object.account.local?
end
Expand Down
Loading

0 comments on commit 66b8396

Please sign in to comment.