Add mutual visibility support

This commit is contained in:
KMY 2023-08-12 18:22:22 +09:00
parent 9662e6ae32
commit 79062bfc2f
14 changed files with 85 additions and 9 deletions

View file

@ -62,7 +62,12 @@ class StatusesController < ApplicationController
def set_status def set_status
@status = @account.statuses.find(params[:id]) @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 rescue Mastodon::NotPermittedError
not_found not_found
end end

View file

@ -24,6 +24,8 @@ const messages = defineMessages({
login_long: { id: 'privacy.login.long', defaultMessage: 'Login user only' }, login_long: { id: 'privacy.login.long', defaultMessage: 'Login user only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual' },
mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
@ -119,7 +121,7 @@ class PrivacyDropdownMenu extends PureComponent {
setFocusRef = c => { setFocusRef = c => {
this.focusedItem = c; this.focusedItem = c;
}; };
render () { render () {
const { style, items, value } = this.props; const { style, items, value } = this.props;
@ -231,6 +233,7 @@ class PrivacyDropdown extends PureComponent {
{ icon: 'cloud', value: 'public_unlisted', text: formatMessage(messages.public_unlisted_short), meta: formatMessage(messages.public_unlisted_long) }, { icon: 'cloud', value: 'public_unlisted', text: formatMessage(messages.public_unlisted_short), meta: formatMessage(messages.public_unlisted_long) },
{ icon: 'key', value: 'login', text: formatMessage(messages.login_short), meta: formatMessage(messages.login_long) }, { icon: 'key', value: 'login', text: formatMessage(messages.login_short), meta: formatMessage(messages.login_long) },
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, { icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'exchange', value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, { icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) }, { icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
]; ];

View file

@ -29,7 +29,7 @@ class StatusReachFinder
if @status.reblog? if @status.reblog?
[] []
else elsif !@status.limited_visibility?
Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes
end end
end end
@ -37,7 +37,7 @@ class StatusReachFinder
def reached_account_inboxes_for_misskey def reached_account_inboxes_for_misskey
if @status.reblog? if @status.reblog?
[] []
else elsif !@status.limited_visibility?
Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey).inboxes Account.where(id: reached_account_ids).where(domain: banned_domains_for_misskey).inboxes
end end
end end

View file

@ -303,6 +303,10 @@ module AccountInteractions
end end
end end
def mutuals
followers.merge(Account.where(id: following))
end
def relations_map(account_ids, domains = nil, **options) def relations_map(account_ids, domains = nil, **options)
relations = { relations = {
blocked_by: Account.blocked_by_map(account_ids, id), blocked_by: Account.blocked_by_map(account_ids, id),

View file

@ -79,6 +79,7 @@ class Status < ApplicationRecord
has_many :references, through: :reference_objects, class_name: 'Status', source: :target_status has_many :references, through: :reference_objects, class_name: 'Status', source: :target_status
has_many :referenced_by_status_objects, foreign_key: 'target_status_id', class_name: 'StatusReference', inverse_of: :target_status, dependent: :destroy has_many :referenced_by_status_objects, foreign_key: 'target_status_id', class_name: 'StatusReference', inverse_of: :target_status, dependent: :destroy
has_many :referenced_by_statuses, through: :referenced_by_status_objects, class_name: 'Status', source: :status has_many :referenced_by_statuses, through: :referenced_by_status_objects, class_name: 'Status', source: :status
has_many :capability_tokens, class_name: 'StatusCapabilityToken', inverse_of: :status, dependent: :destroy
has_and_belongs_to_many :tags has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards has_and_belongs_to_many :preview_cards

View file

@ -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) not null
# 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

View file

@ -4,7 +4,7 @@ class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
attributes :id, :type, :actor, :published, :to, :cc, :virtual_object attributes :id, :type, :actor, :published, :to, :cc, :virtual_object
class << self class << self
def from_status(status, allow_inlining: true, for_misskey: false) def from_status(status, use_bearcap: true, allow_inlining: true, for_misskey: false)
new.tap do |presenter| new.tap do |presenter|
presenter.id = ActivityPub::TagManager.instance.activity_uri_for(status) presenter.id = ActivityPub::TagManager.instance.activity_uri_for(status)
presenter.type = status.reblog? ? 'Announce' : 'Create' presenter.type = status.reblog? ? 'Announce' : 'Create'
@ -20,6 +20,8 @@ class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model
else else
ActivityPub::TagManager.instance.uri_for(status.proper) ActivityPub::TagManager.instance.uri_for(status.proper)
end end
elsif status.limited_visibility? && use_bearcap
"bear:?#{{ u: ActivityPub::TagManager.instance.uri_for(status.proper), t: status.capability_tokens.first.token }.to_query}"
else else
status.proper status.proper
end end

View file

@ -118,6 +118,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
:kmyblue_reaction_deck, :kmyblue_reaction_deck,
:kmyblue_visibility_login, :kmyblue_visibility_login,
:status_reference, :status_reference,
:visibility_mutual,
] ]
capabilities << :profile_search unless Chewy.enabled? capabilities << :profile_search unless Chewy.enabled?

View file

@ -127,6 +127,7 @@ class REST::V1::InstanceSerializer < ActiveModel::Serializer
:kmyblue_reaction_deck, :kmyblue_reaction_deck,
:kmyblue_visibility_login, :kmyblue_visibility_login,
:status_reference, :status_reference,
:visibility_mutual,
] ]
capabilities << :profile_search unless Chewy.enabled? capabilities << :profile_search unless Chewy.enabled?

View file

@ -32,7 +32,7 @@ class BackupService < BaseService
add_comma = true add_comma = true
file.write(statuses.map do |status| file.write(statuses.map do |status|
item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status), ActivityPub::ActivitySerializer) item = serialize_payload(ActivityPub::ActivityPresenter.from_status(status, use_bearcap: false), ActivityPub::ActivitySerializer)
item.delete('@context') item.delete('@context')
unless item[:type] == 'Announce' || item[:object][:attachment].blank? unless item[:type] == 'Announce' || item[:object][:attachment].blank?

View file

@ -74,6 +74,7 @@ class PostStatusService < BaseService
end) || @options[:spoiler_text].present? end) || @options[:spoiler_text].present?
@text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present? @text = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
@visibility = @options[:visibility] || @account.user&.setting_default_privacy @visibility = @options[:visibility] || @account.user&.setting_default_privacy
@visibility = :limited if @options[:visibility] == 'mutual'
@visibility = :unlisted if (@visibility&.to_sym == :public || @visibility&.to_sym == :public_unlisted || @visibility&.to_sym == :login) && @account.silenced? @visibility = :unlisted if (@visibility&.to_sym == :public || @visibility&.to_sym == :public_unlisted || @visibility&.to_sym == :login) && @account.silenced?
@visibility = :public_unlisted if @visibility&.to_sym == :public && !@options[:force_visibility] && !@options[:application]&.superapp && @account.user&.setting_public_post_to_unlisted @visibility = :public_unlisted if @visibility&.to_sym == :public && !@options[:force_visibility] && !@options[:application]&.superapp && @account.user&.setting_public_post_to_unlisted
@searchability = searchability @searchability = searchability
@ -113,7 +114,7 @@ class PostStatusService < BaseService
def process_status! def process_status!
@status = @account.statuses.new(status_attributes) @status = @account.statuses.new(status_attributes)
process_mentions_service.call(@status, save_records: false) process_mentions_service.call(@status, limited_type: @status.limited_visibility? ? 'mutual' : '', save_records: false)
safeguard_mentions!(@status) safeguard_mentions!(@status)
UpdateStatusExpirationService.new.call(@status) UpdateStatusExpirationService.new.call(@status)
@ -122,6 +123,7 @@ class PostStatusService < BaseService
# the media attachments when the status is created # the media attachments when the status is created
ApplicationRecord.transaction do ApplicationRecord.transaction do
@status.save! @status.save!
@status.capability_tokens.create! if @status.limited_visibility?
end end
end end

View file

@ -7,8 +7,9 @@ class ProcessMentionsService < BaseService
# and create local mention pointers # and create local mention pointers
# @param [Status] status # @param [Status] status
# @param [Boolean] save_records Whether to save records in database # @param [Boolean] save_records Whether to save records in database
def call(status, save_records: true) def call(status, limited_type: '', save_records: true)
@status = status @status = status
@limited_type = limited_type
@save_records = save_records @save_records = save_records
return unless @status.local? return unless @status.local?
@ -62,6 +63,8 @@ class ProcessMentionsService < BaseService
"@#{mentioned_account.acct}" "@#{mentioned_account.acct}"
end end
process_mutual! if @limited_type == 'mutual'
@status.save! if @save_records @status.save! if @save_records
end end
@ -92,4 +95,12 @@ class ProcessMentionsService < BaseService
def mention_undeliverable?(mentioned_account) def mention_undeliverable?(mentioned_account)
mentioned_account.nil? || (!mentioned_account.local? && !mentioned_account.activitypub?) mentioned_account.nil? || (!mentioned_account.local? && !mentioned_account.activitypub?)
end end
def process_mutual!
mentioned_account_ids = @current_mentions.map(&:account_id)
@status.account.mutuals.find_each do |target_account|
@current_mentions << @status.mentions.new(silent: true, account: target_account) unless mentioned_account_ids.include?(target_account.id)
end
end
end end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateStatusCapabilityToken < ActiveRecord::Migration[7.0]
def change
create_table :status_capability_tokens do |t|
t.belongs_to :status, null: false, foreign_key: { on_delete: :cascade }
t.string :token
t.datetime :created_at, null: false
t.datetime :updated_at, null: false
end
end
end

View file

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_08_04_222017) do ActiveRecord::Schema[7.0].define(version: 2023_08_12_083752) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
@ -1029,6 +1029,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_04_222017) do
t.index ["var"], name: "index_site_uploads_on_var", unique: true t.index ["var"], name: "index_site_uploads_on_var", unique: true
end end
create_table "status_capability_tokens", force: :cascade do |t|
t.bigint "status_id", null: false
t.string "token"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["status_id"], name: "index_status_capability_tokens_on_status_id"
end
create_table "status_edits", force: :cascade do |t| create_table "status_edits", force: :cascade do |t|
t.bigint "status_id", null: false t.bigint "status_id", null: false
t.bigint "account_id" t.bigint "account_id"
@ -1395,6 +1403,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_04_222017) do
add_foreign_key "scheduled_statuses", "accounts", on_delete: :cascade add_foreign_key "scheduled_statuses", "accounts", on_delete: :cascade
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade
add_foreign_key "status_capability_tokens", "statuses", on_delete: :cascade
add_foreign_key "status_edits", "accounts", on_delete: :nullify add_foreign_key "status_edits", "accounts", on_delete: :nullify
add_foreign_key "status_edits", "statuses", on_delete: :cascade add_foreign_key "status_edits", "statuses", on_delete: :cascade
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade