diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index c91b9b71634298..edacbd5adc14fa 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -89,17 +89,17 @@ def set_domain_block def update_params params.require(:domain_block).permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, - :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) + :reject_straight_follow, :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) end def resource_params params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, - :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) + :reject_straight_follow, :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) end def form_domain_block_batch_params params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, - :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous]) + :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous]) end def action_from_button diff --git a/app/controllers/admin/friend_servers_controller.rb b/app/controllers/admin/friend_servers_controller.rb new file mode 100644 index 00000000000000..aeec82429c4241 --- /dev/null +++ b/app/controllers/admin/friend_servers_controller.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module Admin + class FriendServersController < BaseController + before_action :set_friend, except: [:index, :new, :create] + before_action :warn_signatures_not_enabled!, only: [:new, :edit, :create, :follow, :unfollow, :accept, :reject] + + def index + authorize :friend_server, :update? + @friends = FriendDomain.all + end + + def new + authorize :friend_server, :update? + @friend = FriendDomain.new + end + + def edit + authorize :friend_server, :update? + end + + def create + authorize :friend_server, :update? + + @friend = FriendDomain.new(resource_params) + + if @friend.save + @friend.follow! + redirect_to admin_friend_servers_path + else + render action: :new + end + end + + def update + authorize :friend_server, :update? + + if @friend.update(resource_params) + redirect_to admin_friend_servers_path + else + render action: :edit + end + end + + def destroy + authorize :friend_server, :update? + @friend.destroy + redirect_to admin_friend_servers_path + end + + def follow + authorize :friend_server, :update? + @friend.follow! + render action: :edit + end + + def unfollow + authorize :friend_server, :update? + @friend.unfollow! + render action: :edit + end + + def accept + authorize :friend_server, :update? + @friend.accept! + render action: :edit + end + + def reject + authorize :friend_server, :update? + @friend.reject! + render action: :edit + end + + private + + def set_friend + @friend = FriendDomain.find(params[:id]) + end + + def resource_params + params.require(:friend_domain).permit(:domain, :inbox_url, :available, :pseudo_relay, :unlocked, :allow_all_posts) + end + + def warn_signatures_not_enabled! + flash.now[:error] = I18n.t('admin.relays.signatures_not_enabled') if authorized_fetch_mode? + end + end +end diff --git a/app/controllers/api/v1/admin/domain_blocks_controller.rb b/app/controllers/api/v1/admin/domain_blocks_controller.rb index bd0660dbaaa153..e157ed1e1fc965 100644 --- a/app/controllers/api/v1/admin/domain_blocks_controller.rb +++ b/app/controllers/api/v1/admin/domain_blocks_controller.rb @@ -70,7 +70,7 @@ def filtered_domain_blocks def domain_block_params params.permit(:severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_reports, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, - :reject_new_follow, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) + :reject_new_follow, :reject_friend, :detect_invalid_subscription, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) end def insert_pagination_headers @@ -103,6 +103,6 @@ def pagination_params(core_params) def resource_params params.permit(:domain, :severity, :reject_media, :reject_favourite, :reject_reply, :reject_reply_exclude_followers, :reject_send_not_public_searchability, :reject_send_public_unlisted, :reject_send_dissubscribable, :reject_send_media, :reject_send_sensitive, :reject_hashtag, :reject_straight_follow, - :reject_new_follow, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) + :reject_new_follow, :reject_friend, :detect_invalid_subscription, :reject_reports, :private_comment, :public_comment, :obfuscate, :hidden, :hidden_anonymous) end end diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 5126e23c6a9340..494400bffdfc19 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -3,6 +3,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def perform return accept_follow_for_relay if relay_follow? + return accept_follow_for_friend if friend_follow? return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? case @object['type'] @@ -43,6 +44,18 @@ def relay_follow? relay.present? end + def accept_follow_for_friend + friend.update!(active_state: :accepted) + end + + def friend + @friend ||= FriendDomain.find_by(domain: @account.domain, active_follow_activity_id: object_uri, active_state: [:pending, :accepted]) if @account.domain.present? + end + + def friend_follow? + friend.present? + end + def target_uri @target_uri ||= value_or_id(@object['actor']) end diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 48b855f3b94a47..be474a05193a0d 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -447,7 +447,7 @@ def ignore_hashtags? def related_to_local_activity? fetch? || followed_by_local_accounts? || requested_through_relay? || - responds_to_followed_account? || addresses_local_accounts? || quote_local? + responds_to_followed_account? || addresses_local_accounts? || quote_local? || free_friend_domain? end def responds_to_followed_account? @@ -502,6 +502,10 @@ def quote_local? end end + def free_friend_domain? + FriendDomain.free_receivings.exists?(domain: @account.domain) + end + def quote @quote ||= @object['quote'] || @object['quoteUrl'] || @object['quoteURL'] || @object['_misskey_quote'] end diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 61f6ca699775c2..f401714430a419 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -4,6 +4,8 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def perform if @account.uri == object_uri delete_person + elsif object_uri == ActivityPub::TagManager::COLLECTIONS[:public] + delete_friend else delete_note end @@ -42,6 +44,11 @@ def delete_note end end + def delete_friend + friend = FriendDomain.find_by(domain: @account.domain) + friend&.destroy + end + def forwarder @forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status) end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index d1297bcfe85243..d02e9c01c6fd85 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -4,6 +4,8 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity include Payloadable def perform + return request_follow_for_friend if friend_follow? + target_account = account_from_uri(object_uri) return if target_account.nil? || !target_account.local? || delete_arrived_first?(@json['id']) @@ -43,6 +45,36 @@ def reject_follow_request!(target_account) ActivityPub::DeliveryWorker.perform_async(json, target_account.id, @account.inbox_url) end + def request_follow_for_friend + already_accepted = false + + if friend.present? + already_accepted = friend.they_are_accepted? + friend.update!(passive_state: :pending, passive_follow_activity_id: @json['id']) + else + @friend = FriendDomain.create!(domain: @account.domain, passive_state: :pending, passive_follow_activity_id: @json['id']) + end + + if already_accepted || friend.unlocked || Setting.unlocked_friend + friend.accept! + else + # Notify for admin even if unlocked + notify_staff_about_pending_friend_server! + end + end + + def friend + @friend ||= FriendDomain.find_by(domain: @account.domain) if @account.domain.present? + end + + def friend_follow? + @json['object'] == ActivityPub::TagManager::COLLECTIONS[:public] && !block_friend? + end + + def block_friend? + @block_friend ||= DomainBlock.reject_friend?(@account.domain) || DomainBlock.blocked?(@account.domain) + end + def block_straight_follow? @block_straight_follow ||= DomainBlock.reject_straight_follow?(@account.domain) end @@ -73,4 +105,12 @@ def proxyable_software? def instance_info @instance_info ||= InstanceInfo.find_by(domain: @account.domain) end + + def notify_staff_about_pending_friend_server! + User.those_who_can(:manage_federation).includes(:account).find_each do |u| + next unless u.allows_pending_friend_server_emails? + + AdminMailer.with(recipient: u.account).new_pending_friend_server(friend).deliver_later + end + end end diff --git a/app/lib/activitypub/activity/reject.rb b/app/lib/activitypub/activity/reject.rb index 886dddb23557c4..0493400f864d30 100644 --- a/app/lib/activitypub/activity/reject.rb +++ b/app/lib/activitypub/activity/reject.rb @@ -3,6 +3,7 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity def perform return reject_follow_for_relay if relay_follow? + return reject_follow_for_friend if friend_follow? return follow_request_from_object.reject! unless follow_request_from_object.nil? return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil? @@ -37,6 +38,18 @@ def relay_follow? relay.present? end + def reject_follow_for_friend + friend.update!(active_state: :rejected) + end + + def friend + @friend ||= FriendDomain.find_by(domain: @account.domain, active_follow_activity_id: object_uri, active_state: [:pending, :accepted]) if @account.domain.present? + end + + def friend_follow? + friend.present? + end + def target_uri @target_uri ||= value_or_id(@object['actor']) end diff --git a/app/lib/activitypub/activity/undo.rb b/app/lib/activitypub/activity/undo.rb index f070043d155041..2fc6bd25622610 100644 --- a/app/lib/activitypub/activity/undo.rb +++ b/app/lib/activitypub/activity/undo.rb @@ -87,6 +87,8 @@ def undo_accept end def undo_follow + return remove_follow_from_friend if friend_follow? + target_account = account_from_uri(target_uri) return if target_account.nil? || !target_account.local? @@ -100,6 +102,18 @@ def undo_follow end end + def remove_follow_from_friend + friend.update!(passive_state: :idle, passive_follow_activity_id: nil) + end + + def friend + @friend ||= FriendDomain.find_by(domain: @account.domain) if @account.domain.present? && @object['object'] == ActivityPub::TagManager::COLLECTIONS[:public] + end + + def friend_follow? + friend.present? + end + def undo_like_original status = status_from_uri(target_uri) diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index e0a234e1103246..ae37ba2cf2b011 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -76,6 +76,8 @@ def sensitive def visibility if audience_to.any? { |to| ActivityPub::TagManager.instance.public_collection?(to) } :public + elsif audience_to.include?('LocalPublic') + :public_unlisted elsif audience_cc.any? { |cc| ActivityPub::TagManager.instance.public_collection?(cc) } :unlisted elsif audience_to.include?('as:LoginOnly') || audience_to.include?('LoginUser') @@ -198,6 +200,8 @@ def searchability_from_audience :public elsif audience_searchable_by.include?('as:Limited') :limited + elsif audience_searchable_by.include?('LocalPublic') + :public_unlisted elsif audience_searchable_by.include?(@account.followers_url) :private else diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index d4badeb461b6e7..0b608a0adb7b9e 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -126,6 +126,12 @@ def to(status) end end + def to_for_friend(status) + to = to(status) + to << 'LocalPublic' if status.public_unlisted_visibility? + to + end + # Secondary audience of a status # Public statuses go out to followers as well # Unlisted statuses go to the public as well @@ -147,7 +153,7 @@ def cc(status) end def cc_for_misskey(status) - if (status.account.user&.setting_reject_unlisted_subscription && status.visibility == 'unlisted') || (status.account.user&.setting_reject_public_unlisted_subscription && status.visibility == 'public_unlisted') + if (status.account.user&.setting_reject_unlisted_subscription && status.unlisted_visibility?) || (status.account.user&.setting_reject_public_unlisted_subscription && status.public_unlisted_visibility?) cc = cc_private_visibility(status) cc << uri_for(status.reblog.account) if status.reblog? return cc @@ -251,6 +257,12 @@ def searchable_by(status) searchable_by.concat(mentions_uris(status)).compact end + def searchable_by_for_friend(status) + searchable = searchable_by(status) + searchable << 'LocalPublic' if status.compute_searchability_local == 'public_unlisted' + searchable + end + def account_searchable_by(account) case account.compute_searchability_activitypub when 'public' diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 6ce014395361ad..169754e134007f 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -21,6 +21,10 @@ def inboxes_for_misskey end end + def inboxes_for_friend + (reached_account_inboxes_for_friend + followers_inboxes_for_friend + friend_inboxes).uniq + end + private def reached_account_inboxes @@ -32,7 +36,7 @@ def reached_account_inboxes elsif @status.limited_visibility? Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes else - Account.where(id: reached_account_ids).where.not(domain: banned_domains).inboxes + Account.where(id: reached_account_ids).where.not(domain: banned_domains + friend_domains).inboxes end end @@ -42,7 +46,17 @@ def reached_account_inboxes_for_misskey elsif @status.limited_visibility? Account.where(id: mentioned_account_ids).where(domain: banned_domains_for_misskey).inboxes else - 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 - friend_domains).inboxes + end + end + + def reached_account_inboxes_for_friend + if @status.reblog? + [] + elsif @status.limited_visibility? + Account.where(id: mentioned_account_ids).where.not(domain: banned_domains).inboxes + else + Account.where(id: reached_account_ids, domain: friend_domains).where.not(domain: banned_domains - friend_domains).inboxes end end @@ -95,21 +109,31 @@ def quoted_account_id def followers_inboxes if @status.in_reply_to_local_account? && distributable? - @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains).inboxes + @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where.not(domain: banned_domains + friend_domains).inboxes elsif @status.direct_visibility? || @status.limited_visibility? [] else - @status.account.followers.where.not(domain: banned_domains).inboxes + @status.account.followers.where.not(domain: banned_domains + friend_domains).inboxes end end def followers_inboxes_for_misskey if @status.in_reply_to_local_account? && distributable? - @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey).inboxes + @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: banned_domains_for_misskey - friend_domains).inboxes elsif @status.direct_visibility? || @status.limited_visibility? [] else - @status.account.followers.where(domain: banned_domains_for_misskey).inboxes + @status.account.followers.where(domain: banned_domains_for_misskey - friend_domains).inboxes + end + end + + def followers_inboxes_for_friend + if @status.in_reply_to_local_account? && distributable? + @status.account.followers.or(@status.thread.account.followers.not_domain_blocked_by_account(@status.account)).where(domain: friend_domains).inboxes + elsif @status.direct_visibility? || @status.limited_visibility? + [] + else + @status.account.followers.where(domain: friend_domains).inboxes end end @@ -121,6 +145,14 @@ def relay_inboxes end end + def friend_inboxes + if @status.public_visibility? || @status.public_unlisted_visibility? || (@status.unlisted_visibility? && (@status.public_searchability? || @status.public_unlisted_searchability?)) + DeliveryFailureTracker.without_unavailable(FriendDomain.distributables.pluck(:inbox_url)) + else + [] + end + end + def distributable? @status.public_visibility? || @status.unlisted_visibility? || @status.public_unlisted_visibility? end @@ -129,6 +161,13 @@ def unsafe? @options[:unsafe] end + def friend_domains + return @friend_domains if defined?(@friend_domains) + + @friend_domains = FriendDomain.deliver_locals.pluck(:domain) + @friend_domains -= UnavailableDomain.where(domain: @friend_domains).pluck(:domain) + end + def banned_domains return @banned_domains if @banned_domains diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 990b92c3377d2e..11262144be0576 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -35,6 +35,14 @@ def new_pending_account(user) end end + def new_pending_friend_server(friend_server) + @friend = friend_server + + locale_for_account(@me) do + mail subject: default_i18n_subject(instance: @instance, domain: @friend.domain) + end + end + def new_trends(links, tags, statuses) @links = links @tags = tags diff --git a/app/models/concerns/has_user_settings.rb b/app/models/concerns/has_user_settings.rb index ed5a5f429f29a7..820c41360d4efc 100644 --- a/app/models/concerns/has_user_settings.rb +++ b/app/models/concerns/has_user_settings.rb @@ -263,6 +263,10 @@ def allows_pending_account_emails? settings['notification_emails.pending_account'] end + def allows_pending_friend_server_emails? + settings['notification_emails.pending_friend_server'] + end + def allows_appeal_emails? settings['notification_emails.appeal'] end diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index b05fa19476eae0..16d7ac2128d423 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -28,6 +28,7 @@ # hidden_anonymous :boolean default(FALSE), not null # detect_invalid_subscription :boolean default(FALSE), not null # reject_reply_exclude_followers :boolean default(FALSE), not null +# reject_friend :boolean default(FALSE), not null # class DomainBlock < ApplicationRecord @@ -44,7 +45,16 @@ class DomainBlock < ApplicationRecord scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :with_user_facing_limitations, -> { where(hidden: false) } - scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)).or(where(reject_favourite: true)).or(where(reject_reply: true)).or(where(reject_reply_exclude_followers: true)).or(where(reject_new_follow: true)).or(where(reject_straight_follow: true)) } + scope :with_limitations, lambda { + where(severity: [:silence, :suspend]) + .or(where(reject_media: true)) + .or(where(reject_favourite: true)) + .or(where(reject_reply: true)) + .or(where(reject_reply_exclude_followers: true)) + .or(where(reject_new_follow: true)) + .or(where(reject_straight_follow: true)) + .or(where(reject_friend: true)) + } scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) } def to_log_human_identifier @@ -68,6 +78,7 @@ def policies reject_hashtag? ? :reject_hashtag : nil, reject_straight_follow? ? :reject_straight_follow : nil, reject_new_follow? ? :reject_new_follow : nil, + reject_friend? ? :reject_friend : nil, detect_invalid_subscription? ? :detect_invalid_subscription : nil, reject_reports? ? :reject_reports : nil].reject { |policy| policy == :noop || policy.nil? } end @@ -110,6 +121,10 @@ def reject_new_follow?(domain) !!rule_for(domain)&.reject_new_follow? end + def reject_friend?(domain) + !!rule_for(domain)&.reject_friend? + end + def detect_invalid_subscription?(domain) !!rule_for(domain)&.detect_invalid_subscription? end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index a2de73bd14b695..681b13814aa73d 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -48,6 +48,7 @@ class Form::AdminSettings enable_emoji_reaction check_lts_version_only enable_public_unlisted_visibility + unlocked_friend ).freeze INTEGER_KEYS = %i( @@ -76,6 +77,7 @@ class Form::AdminSettings enable_emoji_reaction check_lts_version_only enable_public_unlisted_visibility + unlocked_friend ).freeze UPLOAD_KEYS = %i( diff --git a/app/models/friend_domain.rb b/app/models/friend_domain.rb new file mode 100644 index 00000000000000..a3beb9b3574675 --- /dev/null +++ b/app/models/friend_domain.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: friend_domains +# +# id :bigint(8) not null, primary key +# domain :string default(""), not null +# inbox_url :string default(""), not null +# active_state :integer default("idle"), not null +# passive_state :integer default("idle"), not null +# active_follow_activity_id :string +# passive_follow_activity_id :string +# available :boolean default(TRUE), not null +# pseudo_relay :boolean default(FALSE), not null +# unlocked :boolean default(FALSE), not null +# allow_all_posts :boolean default(TRUE), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class FriendDomain < ApplicationRecord + validates :domain, presence: true, uniqueness: true, if: :will_save_change_to_domain? + validates :inbox_url, presence: true, uniqueness: true, if: :will_save_change_to_inbox_url? + + enum active_state: { idle: 0, pending: 1, accepted: 2, rejected: 3 }, _prefix: :i_am + enum passive_state: { idle: 0, pending: 1, accepted: 2, rejected: 3 }, _prefix: :they_are + + scope :by_domain_and_subdomains, ->(domain) { where(domain: Instance.by_domain_and_subdomains(domain).select(:domain)) } + scope :enabled, -> { where(available: true) } + scope :mutuals, -> { enabled.where(active_state: :accepted, passive_state: :accepted) } + scope :distributables, -> { mutuals.where(pseudo_relay: true) } + scope :deliver_locals, -> { enabled.where(active_state: :accepted) } + scope :free_receivings, -> { mutuals.where(allow_all_posts: true) } + + before_destroy :ensure_disabled + after_commit :set_default_inbox_url + + def mutual? + i_am_accepted? && they_are_accepted? + end + + def follow! + activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) + payload = Oj.dump(follow_activity(activity_id)) + + update!(active_state: :pending, active_follow_activity_id: activity_id) + DeliveryFailureTracker.reset!(inbox_url) + ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) + end + + def unfollow! + activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) + payload = Oj.dump(unfollow_activity(activity_id)) + + update!(active_state: :idle, active_follow_activity_id: nil) + DeliveryFailureTracker.reset!(inbox_url) + ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) + end + + def accept! + return if they_are_idle? + + activity_id = passive_follow_activity_id + payload = Oj.dump(accept_follow_activity(activity_id)) + + update!(passive_state: :accepted) + DeliveryFailureTracker.reset!(inbox_url) + ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) + end + + def reject! + return if they_are_idle? + + activity_id = passive_follow_activity_id + payload = Oj.dump(reject_follow_activity(activity_id)) + + update!(passive_state: :rejected, passive_follow_activity_id: nil) + DeliveryFailureTracker.reset!(inbox_url) + ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) + end + + private + + def default_inbox_url + "https://#{domain}/inbox" + end + + def delete_for_friend! + activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) + payload = Oj.dump(delete_follow_activity(activity_id)) + + DeliveryFailureTracker.reset!(inbox_url) + ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url) + end + + def follow_activity(activity_id) + { + '@context': ActivityPub::TagManager::CONTEXT, + id: activity_id, + type: 'Follow', + actor: ActivityPub::TagManager.instance.uri_for(some_local_account), + object: ActivityPub::TagManager::COLLECTIONS[:public], + } + end + + def unfollow_activity(activity_id) + { + '@context': ActivityPub::TagManager::CONTEXT, + id: activity_id, + type: 'Undo', + actor: ActivityPub::TagManager.instance.uri_for(some_local_account), + object: { + id: active_follow_activity_id, + type: 'Follow', + actor: ActivityPub::TagManager.instance.uri_for(some_local_account), + object: ActivityPub::TagManager::COLLECTIONS[:public], + }, + } + end + + def accept_follow_activity(activity_id) + { + '@context': ActivityPub::TagManager::CONTEXT, + id: "#{activity_id}#accepts/friends", + type: 'Accept', + actor: ActivityPub::TagManager.instance.uri_for(some_local_account), + object: activity_id, + } + end + + def reject_follow_activity(activity_id) + { + '@context': ActivityPub::TagManager::CONTEXT, + id: "#{activity_id}#rejects/friends", + type: 'Reject', + actor: ActivityPub::TagManager.instance.uri_for(some_local_account), + object: activity_id, + } + end + + def delete_follow_activity(activity_id) + { + '@context': ActivityPub::TagManager::CONTEXT, + id: "#{activity_id}#delete/friends", + type: 'Delete', + actor: ActivityPub::TagManager.instance.uri_for(some_local_account), + object: ActivityPub::TagManager::COLLECTIONS[:public], + } + end + + def some_local_account + @some_local_account ||= Account.representative + end + + def ensure_disabled + delete_for_friend! unless i_am_idle? && they_are_idle? + end + + def set_default_inbox_url + self.inbox_url = default_inbox_url if inbox_url.blank? + end +end diff --git a/app/models/instance.rb b/app/models/instance.rb index 0fb1d3e96a336f..09e823bfbbc6b3 100644 --- a/app/models/instance.rb +++ b/app/models/instance.rb @@ -20,6 +20,7 @@ class Instance < ApplicationRecord belongs_to :domain_allow belongs_to :unavailable_domain # skipcq: RB-RL1031 belongs_to :instance_info + belongs_to :friend_domain end scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) } diff --git a/app/models/public_feed.rb b/app/models/public_feed.rb index a641e77039194d..0ca7621060a8a6 100644 --- a/app/models/public_feed.rb +++ b/app/models/public_feed.rb @@ -19,7 +19,7 @@ def initialize(account, options = {}) # @param [Integer] min_id # @return [Array] def get(limit, max_id = nil, since_id = nil, min_id = nil) - scope = local_only? ? public_scope : global_timeline_only_scope + scope = public_scope scope.merge!(without_replies_scope) unless with_replies? scope.merge!(without_reblogs_scope) unless with_reblogs? @@ -70,10 +70,6 @@ def public_scope Status.with_public_visibility.joins(:account).merge(Account.without_suspended.without_silenced) end - def global_timeline_only_scope - Status.with_global_timeline_visibility.joins(:account).merge(Account.without_suspended.without_silenced) - end - def public_search_scope Status.with_public_search_visibility.joins(:account).merge(Account.without_suspended.without_silenced) end diff --git a/app/models/status.rb b/app/models/status.rb index 5a33411aba0f85..26baac1b680d0d 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -130,7 +130,6 @@ class Status < ApplicationRecord scope :without_reblogs, -> { where(statuses: { reblog_of_id: nil }) } scope :with_public_visibility, -> { where(visibility: [:public, :public_unlisted, :login]) } scope :with_public_search_visibility, -> { merge(where(visibility: [:public, :public_unlisted, :login]).or(Status.where(searchability: [:public, :public_unlisted]))) } - scope :with_global_timeline_visibility, -> { where(visibility: [:public, :login]) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :excluding_silenced_accounts, -> { left_outer_joins(:account).where(accounts: { silenced_at: nil }) } scope :including_silenced_accounts, -> { left_outer_joins(:account).where.not(accounts: { silenced_at: nil }) } diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 8518e2abf224a7..4bf64e338bbe01 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -80,6 +80,7 @@ class KeyError < Error; end setting :follow_request, default: true setting :report, default: true setting :pending_account, default: true + setting :pending_friend_server, default: true setting :trends, default: true setting :appeal, default: true setting :software_updates, default: 'critical', in: %w(none critical patch all) diff --git a/app/policies/friend_server_policy.rb b/app/policies/friend_server_policy.rb new file mode 100644 index 00000000000000..c84b2b825a8e05 --- /dev/null +++ b/app/policies/friend_server_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class FriendServerPolicy < ApplicationPolicy + def update? + role.can?(:manage_federation) + end +end diff --git a/app/presenters/activitypub/activity_presenter.rb b/app/presenters/activitypub/activity_presenter.rb index 5066a57f8c3756..46105df07397be 100644 --- a/app/presenters/activitypub/activity_presenter.rb +++ b/app/presenters/activitypub/activity_presenter.rb @@ -4,13 +4,13 @@ class ActivityPub::ActivityPresenter < ActiveModelSerializers::Model attributes :id, :type, :actor, :published, :to, :cc, :virtual_object class << self - def from_status(status, use_bearcap: true, allow_inlining: true, for_misskey: false) + def from_status(status, use_bearcap: true, allow_inlining: true, for_misskey: false, for_friend: false) new.tap do |presenter| presenter.id = ActivityPub::TagManager.instance.activity_uri_for(status) presenter.type = status.reblog? ? 'Announce' : 'Create' presenter.actor = ActivityPub::TagManager.instance.uri_for(status.account) presenter.published = status.created_at - presenter.to = ActivityPub::TagManager.instance.to(status) + presenter.to = for_friend ? ActivityPub::TagManager.instance.to_for_friend(status) : ActivityPub::TagManager.instance.to(status) presenter.cc = for_misskey ? ActivityPub::TagManager.instance.cc_for_misskey(status) : ActivityPub::TagManager.instance.cc(status) presenter.virtual_object = begin diff --git a/app/serializers/activitypub/activity_for_friend_serializer.rb b/app/serializers/activitypub/activity_for_friend_serializer.rb new file mode 100644 index 00000000000000..b968e00fa62885 --- /dev/null +++ b/app/serializers/activitypub/activity_for_friend_serializer.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ActivityPub::ActivityForFriendSerializer < ActivityPub::Serializer + def self.serializer_for(model, options) + case model.class.name + when 'Status' + ActivityPub::NoteForFriendSerializer + when 'DeliverToDeviceService::EncryptedMessage' + ActivityPub::EncryptedMessageSerializer + else + super + end + end + + attributes :id, :type, :actor, :published, :to, :cc + + has_one :virtual_object, key: :object + + def published + object.published.iso8601 + end +end diff --git a/app/serializers/activitypub/note_for_friend_serializer.rb b/app/serializers/activitypub/note_for_friend_serializer.rb new file mode 100644 index 00000000000000..fdceaf421cf2ce --- /dev/null +++ b/app/serializers/activitypub/note_for_friend_serializer.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class ActivityPub::NoteForFriendSerializer < ActivityPub::NoteSerializer + def to + ActivityPub::TagManager.instance.to_for_friend(object) + end + + def searchable_by + ActivityPub::TagManager.instance.searchable_by_for_friend(object) + end +end diff --git a/app/services/block_domain_service.rb b/app/services/block_domain_service.rb index 76cc36ff6b0d00..511068bdf9ee38 100644 --- a/app/services/block_domain_service.rb +++ b/app/services/block_domain_service.rb @@ -24,6 +24,9 @@ def process_domain_block! silence_accounts! elsif domain_block.suspend? suspend_accounts! + remove_friends! + elsif domain_block.reject_friend? + remove_friends! end DomainClearMediaWorker.perform_async(domain_block.id) if domain_block.reject_media? @@ -41,6 +44,10 @@ def suspend_accounts! end end + def remove_friends! + blocked_friends.find_each(&:destroy) + end + def blocked_domain domain_block.domain end @@ -48,4 +55,8 @@ def blocked_domain def blocked_domain_accounts Account.by_domain_and_subdomains(blocked_domain) end + + def blocked_friends + @blocked_friends ||= FriendDomain.by_domain_and_subdomains(blocked_domain) + end end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index b56474ddf61a9e..89e1b6c9c5376b 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -21,9 +21,6 @@ def call(status, options = {}) if broadcastable? fan_out_to_public_recipients! fan_out_to_public_streams! - elsif broadcastable_unlisted? - fan_out_to_public_recipients! - fan_out_to_public_unlisted_streams! elsif broadcastable_unlisted2? fan_out_to_unlisted_streams! end @@ -75,11 +72,6 @@ def fan_out_to_public_streams! broadcast_to_public_streams! end - def fan_out_to_public_unlisted_streams! - broadcast_to_hashtag_streams! - broadcast_to_public_unlisted_streams! - end - def fan_out_to_unlisted_streams! broadcast_to_hashtag_streams! end @@ -176,16 +168,6 @@ def broadcast_to_public_streams! end end - def broadcast_to_public_unlisted_streams! - return if @status.reply? && @status.in_reply_to_account_id != @account.id - - redis.publish(@status.local? ? 'timeline:public:local' : 'timeline:public:remote', anonymous_payload) - - if @status.with_media? - redis.publish(@status.local? ? 'timeline:public:local:media' : 'timeline:public:remote:media', anonymous_payload) - end - end - def deliver_to_conversation! AccountConversation.add_status(@account, @status) unless update? end @@ -210,11 +192,7 @@ def update? end def broadcastable? - (@status.public_visibility? || @status.login_visibility?) && !@status.reblog? && !@account.silenced? - end - - def broadcastable_unlisted? - @status.public_unlisted_visibility? && !@status.reblog? && !@account.silenced? + (@status.public_visibility? || @status.public_unlisted_visibility? || @status.login_visibility?) && !@status.reblog? && !@account.silenced? end def broadcastable_unlisted2? diff --git a/app/views/admin/domain_blocks/edit.html.haml b/app/views/admin/domain_blocks/edit.html.haml index 8a064415080510..cf83d383e9fcee 100644 --- a/app/views/admin/domain_blocks/edit.html.haml +++ b/app/views/admin/domain_blocks/edit.html.haml @@ -47,6 +47,9 @@ .fields-group = f.input :reject_new_follow, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_new_follow'), hint: I18n.t('admin.domain_blocks.reject_new_follow_hint') + .fields-group + = f.input :reject_friend, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_friend'), hint: I18n.t('admin.domain_blocks.reject_friend_hint') + .fields-group = f.input :detect_invalid_subscription, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.detect_invalid_subscription'), hint: I18n.t('admin.domain_blocks.detect_invalid_subscription_hint') diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml index 606a784e127eda..ed5142934fd675 100644 --- a/app/views/admin/domain_blocks/new.html.haml +++ b/app/views/admin/domain_blocks/new.html.haml @@ -47,6 +47,9 @@ .fields-group = f.input :reject_new_follow, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_new_follow'), hint: I18n.t('admin.domain_blocks.reject_new_follow_hint') + .fields-group + = f.input :reject_friend, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_friend'), hint: I18n.t('admin.domain_blocks.reject_friend_hint') + .fields-group = f.input :detect_invalid_subscription, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.detect_invalid_subscription'), hint: I18n.t('admin.domain_blocks.detect_invalid_subscription_hint') diff --git a/app/views/admin/friend_servers/_friend_domain.html.haml b/app/views/admin/friend_servers/_friend_domain.html.haml new file mode 100644 index 00000000000000..a24ae0516a5f0b --- /dev/null +++ b/app/views/admin/friend_servers/_friend_domain.html.haml @@ -0,0 +1,41 @@ +%tr + %td + - unless friend.available + %span.negative-hint + = fa_icon('times') + = ' ' + = t 'admin.friend_servers.disabled' + %samp= friend.domain + %td + - if friend.i_am_accepted? + %span.positive-hint + = fa_icon('check') + = ' ' + = t 'admin.friend_servers.enabled' + - elsif friend.i_am_pending? + = fa_icon('hourglass') + = ' ' + = t 'admin.friend_servers.pending' + - else + %span.negative-hint + = fa_icon('times') + = ' ' + = t 'admin.friend_servers.disabled' + %td + - if friend.they_are_accepted? + %span.positive-hint + = fa_icon('check') + = ' ' + = t 'admin.friend_servers.enabled' + - elsif friend.they_are_pending? + = fa_icon('hourglass') + = ' ' + = t 'admin.friend_servers.pending' + - else + %span.negative-hint + = fa_icon('times') + = ' ' + = t 'admin.friend_servers.disabled' + %td + = table_link_to 'pencil', t('admin.friend_servers.edit_friend'), edit_admin_friend_server_path(friend) + = table_link_to 'times', t('admin.friend_servers.delete'), admin_friend_server_path(friend), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } diff --git a/app/views/admin/friend_servers/_friend_fields.html.haml b/app/views/admin/friend_servers/_friend_fields.html.haml new file mode 100644 index 00000000000000..b3e56f75c2e9e7 --- /dev/null +++ b/app/views/admin/friend_servers/_friend_fields.html.haml @@ -0,0 +1,20 @@ +%p= t 'admin.friend_servers.edit.description' +%hr.spacer/ + +.fields-group + = f.input :domain, as: :string, wrapper: :with_label, required: true, disabled: !friend.id.nil?, label: t('admin.friend_servers.edit.domain') + +.fields-group + = f.input :inbox_url, as: :string, wrapper: :with_label, label: t('admin.friend_servers.edit.inbox_url'), hint: t('admin.friend_servers.edit.inbox_url_hint') + +.fields-group + = f.input :available, as: :boolean, wrapper: :with_label, label: t('admin.friend_servers.edit.available') + +.fields-group + = f.input :pseudo_relay, as: :boolean, wrapper: :with_label, label: t('admin.friend_servers.edit.pseudo_relay') + +.fields-group + = f.input :unlocked, as: :boolean, wrapper: :with_label, label: t('admin.friend_servers.edit.unlocked') + +.fields-group + = f.input :allow_all_posts, as: :boolean, wrapper: :with_label, label: t('admin.friend_servers.edit.allow_all_posts') diff --git a/app/views/admin/friend_servers/edit.html.haml b/app/views/admin/friend_servers/edit.html.haml new file mode 100644 index 00000000000000..ae057c296300b4 --- /dev/null +++ b/app/views/admin/friend_servers/edit.html.haml @@ -0,0 +1,52 @@ +- content_for :page_title do + = t('admin.friend_servers.edit_friend') + += simple_form_for @friend, url: admin_friend_server_path(@friend), method: :put do |f| + = render 'shared/error_messages', object: @friend + = render 'friend_fields', f: f, friend: @friend + + .fields-group + %h4= t('admin.friend_servers.active_status') + .fields-group + - if @friend.i_am_accepted? + %span.positive-hint + = fa_icon('check') + = ' ' + = t 'admin.friend_servers.enabled' + - elsif @friend.i_am_pending? + = fa_icon('hourglass') + = ' ' + = t 'admin.friend_servers.pending' + - else + %span.negative-hint + = fa_icon('times') + = ' ' + = t 'admin.friend_servers.disabled' + .action-buttons + %div + = link_to t('admin.friend_servers.follow'), follow_admin_friend_server_path(@friend), class: 'button', method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if @friend.i_am_idle? || @friend.i_am_rejected? + = link_to t('admin.friend_servers.unfollow'), unfollow_admin_friend_server_path(@friend), class: 'button', method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if @friend.i_am_pending? || @friend.i_am_accepted? + + %h4= t('admin.friend_servers.passive_status') + .fields-gtoup + - if @friend.they_are_accepted? + %span.positive-hint + = fa_icon('check') + = ' ' + = t 'admin.friend_servers.enabled' + - elsif @friend.they_are_pending? + = fa_icon('hourglass') + = ' ' + = t 'admin.friend_servers.pending' + - else + %span.negative-hint + = fa_icon('times') + = ' ' + = t 'admin.friend_servers.disabled' + .action-buttons + %div + = link_to t('admin.friend_servers.accept'), accept_admin_friend_server_path(@friend), class: 'button', method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if @friend.they_are_pending? + = link_to t('admin.friend_servers.reject'), reject_admin_friend_server_path(@friend), class: 'button', method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if @friend.they_are_pending? + + .actions + = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/friend_servers/index.html.haml b/app/views/admin/friend_servers/index.html.haml new file mode 100644 index 00000000000000..6dd16e397e1fa5 --- /dev/null +++ b/app/views/admin/friend_servers/index.html.haml @@ -0,0 +1,21 @@ +- content_for :page_title do + = t('admin.friend_servers.title') + +.simple_form + %p.hint= t('admin.friend_servers.description_html') + = link_to @friends.empty? ? t('admin.friend_servers.setup') : t('admin.friend_servers.add_new'), new_admin_friend_server_path, class: 'block-button' + +- unless @friends.empty? + %hr.spacer + + .table-wrapper + %table.table + %thead + %tr + %th= t('admin.friend_servers.domain') + %th= t('admin.friend_servers.active_status') + %th= t('admin.friend_servers.passive_status') + %th + %tbody + - @friends.each do |friend| + = render 'friend_domain', friend: friend diff --git a/app/views/admin/friend_servers/new.html.haml b/app/views/admin/friend_servers/new.html.haml new file mode 100644 index 00000000000000..1cb5b06b6f325b --- /dev/null +++ b/app/views/admin/friend_servers/new.html.haml @@ -0,0 +1,9 @@ +- content_for :page_title do + = t('admin.friend_servers.add_new') + += simple_form_for @friend, url: admin_friend_servers_path do |f| + = render 'shared/error_messages', object: @friend + = render 'friend_fields', f: f, friend: @friend + + .actions + = f.button :button, t('admin.friend_servers.save_and_enable'), type: :submit diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index ee7d72ad320e9c..bedafdb49943a8 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -45,6 +45,11 @@ .fields-group = f.input :enable_public_unlisted_visibility, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false + %h4= t('admin.settings.discovery.friend_servers') + + .fields-group + = f.input :unlocked_friend, as: :boolean, wrapper: :with_label, kmyblue: true, hint: false + %h4= t('admin.settings.discovery.publish_statistics') .fields-group diff --git a/app/views/admin_mailer/new_pending_friend_server.text.erb b/app/views/admin_mailer/new_pending_friend_server.text.erb new file mode 100644 index 00000000000000..89c9ec1b09ccd5 --- /dev/null +++ b/app/views/admin_mailer/new_pending_friend_server.text.erb @@ -0,0 +1,5 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('admin_mailer.new_pending_friend_server.body', domain: @friend.domain) %> + +<%= raw t('application_mailer.view')%> <%= admin_friend_servers_url %> diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index 06af9c136059f8..0f29221edec39e 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -22,13 +22,14 @@ .fields-group = ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails') - - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)) + - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies, :manage_federation) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)) %h4= t 'notifications.administration_emails' .fields-group = ff.input :'notification_emails.report', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.report') if current_user.can?(:manage_reports) = ff.input :'notification_emails.appeal', as: :boolean, wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.appeal') if current_user.can?(:manage_appeals) = ff.input :'notification_emails.pending_account', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.pending_account') if current_user.can?(:manage_users) + = ff.input :'notification_emails.pending_friend_server', as: :boolean, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.notification_emails.pending_friend_server') if current_user.can?(:manage_federation) = ff.input :'notification_emails.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_tag') if current_user.can?(:manage_taxonomies) - if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops) diff --git a/app/workers/activitypub/distribution_worker.rb b/app/workers/activitypub/distribution_worker.rb index 34b6f6e32f83b9..12cb66aeb412f5 100644 --- a/app/workers/activitypub/distribution_worker.rb +++ b/app/workers/activitypub/distribution_worker.rb @@ -22,6 +22,10 @@ def inboxes_for_misskey @inboxes_for_misskey ||= status_reach_finder.inboxes_for_misskey end + def inboxes_for_friend + @inboxes_for_friend ||= status_reach_finder.inboxes_for_friend + end + def status_reach_finder @status_reach_finder ||= StatusReachFinder.new(@status) end @@ -34,6 +38,10 @@ def payload_for_misskey @payload_for_misskey ||= Oj.dump(serialize_payload(activity_for_misskey, ActivityPub::ActivityForMisskeySerializer, signer: @account)) end + def payload_for_friend + @payload_for_friend ||= Oj.dump(serialize_payload(activity_for_friend, ActivityPub::ActivityForFriendSerializer, signer: @account)) + end + def activity ActivityPub::ActivityPresenter.from_status(@status) end @@ -42,6 +50,10 @@ def activity_for_misskey ActivityPub::ActivityPresenter.from_status(@status, for_misskey: true) end + def activity_for_friend + ActivityPub::ActivityPresenter.from_status(@status, for_friend: true) + end + def options { 'synchronize_followers' => @status.private_visibility? } end diff --git a/app/workers/activitypub/raw_distribution_worker.rb b/app/workers/activitypub/raw_distribution_worker.rb index 611b5210d8b926..a1fc7785597a2e 100644 --- a/app/workers/activitypub/raw_distribution_worker.rb +++ b/app/workers/activitypub/raw_distribution_worker.rb @@ -29,6 +29,12 @@ def distribute! end end + unless inboxes_for_friend.empty? + ActivityPub::DeliveryWorker.push_bulk(inboxes_for_friend, limit: 1_000) do |inbox_url| + [payload_for_friend, source_account_id, inbox_url, options] + end + end + return if inboxes.empty? ActivityPub::DeliveryWorker.push_bulk(inboxes, limit: 1_000) do |inbox_url| @@ -44,6 +50,10 @@ def payload_for_misskey payload end + def payload_for_friend + payload + end + def source_account_id @account.id end @@ -56,6 +66,10 @@ def inboxes_for_misskey [] end + def inboxes_for_friend + [] + end + def options {} end diff --git a/config/locales/en.yml b/config/locales/en.yml index 8633f4dfc03097..c86c0a89b8a454 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -426,6 +426,8 @@ en: public_comment_hint: Comment about this domain limitation for the general public, if advertising the list of domain limitations is enabled. reject_favourite: Reject favorites reject_favourite_hint: Reject favorites or emoji-reaction in the future + reject_friend: Reject friend server applications + reject_friend_hint: Reject friend server application in the future reject_hashtag: Reject hashtags reject_hashtag_hint: Reject hashtags in the future reject_media: Reject media files @@ -497,6 +499,35 @@ en: suppressed: Suppressed title: Follow recommendations unsuppress: Restore follow recommendation + friend_servers: + accept: Accept + active_status: My status + add_new: Add and make a new application + delete: Delete + description_html: フレンドサーバーとは、お互いのローカル公開・ローカル検索許可の投稿をそのまま交換するシステムです。 + disabled: Disabled + domain: Domain + edit: + allow_all_posts: Receive all posts + available: Available + description: フレンドサーバーは、登録と同時に相手方のサーバーへ申請されます。 + domain: Domain + inbox_url: Friend server inbox URL + inbox_url_hint: Default value is https://domain/inbox if you input empty (For example, https://example.com/inbox) + pseudo_relay: Send all public or searchable posts + unlocked: Approve automatically receiving new request + edit_friend: Edit + enabled: Enabled + follow: Request + passive_status: Partner status + pending: Pending + reject: Reject + save_and_enable: Save and enable + setup: Add and make a new application + signatures_not_enabled: セキュアモードまたは連合制限モードが有効の場合、フレンドサーバーの動作を確認していないため正常に動作しない可能性があります + status: Status + title: Friend server + unfollow: Cancel request instances: availability: description_html: @@ -520,6 +551,7 @@ en: limited_federation_mode_description_html: You can chose whether to allow federation with this domain. policies: reject_favourite: Reject favorite + reject_friend: Reject friend server application reject_hashtag: Reject hashtags reject_media: Reject media reject_new_follow: Reject follows @@ -810,6 +842,7 @@ en: discovery: emoji_reactions: Stamp follow_recommendations: Follow recommendations + friend_servers: Friend servers preamble: Surfacing interesting content is instrumental in onboarding new users who may not know anyone Mastodon. Control how various discovery features work on your server. profile_directory: Profile directory public_timelines: Public timelines @@ -1050,6 +1083,9 @@ en: new_pending_account: body: The details of the new account are below. You can approve or reject this application. subject: New account up for review on %{instance} (%{username}) + new_pending_friend_server: + body: The new friend server %{domain} is waiting for your review. You can approve or reject this application. + subject: New friend server up for review on %{instance} (%{domain}) new_report: body: "%{reporter} has reported %{target}" body_remote: Someone from %{domain} has reported %{target} diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 87de99e51528f8..f1a32ede38d3f5 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -422,6 +422,8 @@ ja: public_comment_hint: ドメインブロックの公開を有効にしている場合、このコメントも公開されます。 reject_favourite: お気に入り、スタンプを拒否 reject_favourite_hint: 今後のお気に入り、スタンプを拒否します。停止とは無関係です + reject_friend: フレンドサーバー申請を拒否 + reject_friend_hint: 今後のフレンドサーバー申請を全て拒否します。停止とは無関係です reject_hashtag: ハッシュタグを拒否 reject_hashtag_hint: ハッシュタグで検索できなくなり、トレンドにも影響しなくなります。停止とは無関係です reject_media: メディアファイルを拒否 @@ -492,6 +494,35 @@ ja: suppressed: 非表示 title: おすすめフォロー unsuppress: おすすめフォローを復元 + friend_servers: + accept: 相手の申請を承認する + active_status: 自分の状態 + add_new: フレンドサーバーを追加・申請 + delete: 削除 + description_html: フレンドサーバーとは、お互いのローカル公開・ローカル検索許可の投稿をそのまま交換するシステムです。 + disabled: 無効 + domain: ドメイン + edit: + allow_all_posts: このサーバーからの全ての投稿を受け入れる + available: 有効にする + description: フレンドサーバーは、登録と同時に相手方のサーバーへ申請されます。 + domain: ドメイン + inbox_url: フレンドサーバーの inbox URL + inbox_url_hint: 空欄にした場合、自動で「https://ドメイン名/inbox」に設定されます。(例:https://example.com/inbox)相手のサーバーがinbox URLを特別に指定している場合、入力してください。 + pseudo_relay: 全ての公開・ローカル公開・非収載かつ検索可能な投稿を送信する + unlocked: このサーバーからの申請を自動で承認する + edit_friend: 編集 + enabled: 有効 + follow: こちらから申請する + passive_status: 相手の状態 + pending: 承認待ち + reject: 相手からの申請を却下する + save_and_enable: 保存して有効にする + setup: フレンドサーバーを追加・申請 + signatures_not_enabled: セキュアモードまたは連合制限モードが有効の場合、フレンドサーバーの動作を確認していないため正常に動作しない可能性があります + status: ステータス + title: フレンドサーバー + unfollow: こちらの申請を取り消す instances: availability: description_html: @@ -514,6 +545,7 @@ ja: policies: detect_invalid_subscription: 購読のプライバシーなし reject_favourite: お気に入りを拒否 + reject_friend: フレンドサーバー申請を拒否 reject_hashtag: ハッシュタグを拒否 reject_media: メディアを拒否する reject_new_follow: 新規フォローを拒否 @@ -807,6 +839,7 @@ ja: discovery: emoji_reactions: スタンプ follow_recommendations: おすすめフォロー + friend_servers: フレンドサーバー preamble: Mastodon を知らないユーザーを取り込むには、興味深いコンテンツを浮上させることが重要です。サーバー上で様々なディスカバリー機能がどのように機能するかを制御します。 profile_directory: ディレクトリ public_timelines: 公開タイムライン @@ -1043,6 +1076,9 @@ ja: new_pending_account: body: 新しいアカウントの詳細は以下の通りです。この申請を承認または却下することができます。 subject: '%{instance}で新しいアカウント (%{username}) が承認待ちです' + new_pending_friend_server: + body: 新しいフレンドサーバー %{domain} の申請が届いています。この申請を承認または却下することができます。 + subject: '%{instance}で新しいフレンドサーバー (%{domain}) が承認待ちです' new_report: body: "%{reporter}さんが%{target}さんを通報しました" body_remote: "%{domain}の誰かが%{target}さんを通報しました" diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index c37a6de56abf54..2b777aaa2b00c7 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -354,6 +354,7 @@ en: trendable_by_default: Allow trends without prior review trends: Enable trends trends_as_landing_page: Use trends as the landing page + unlocked_friend: Accept all friend server follows automatically interactions: must_be_follower: Block notifications from non-followers must_be_following: Block notifications from people you don't follow @@ -378,6 +379,7 @@ en: follow_request: Someone requested to follow you mention: Someone mentioned you pending_account: New account needs review + pending_friend_server: New friend server needs review reblog: Someone boosted your post report: New report is submitted software_updates: diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index 3904f757518bf6..41057cbed1b052 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -369,6 +369,7 @@ ja: trendable_by_default: 審査前のトレンドの掲載を許可する trends: トレンドを有効にする trends_as_landing_page: 新規登録画面にトレンドを表示する + unlocked_friend: 全てのフレンドサーバー申請を自動承認する interactions: must_be_follower: フォロワー以外からの通知をブロック must_be_following: フォローしていないユーザーからの通知をブロック @@ -393,6 +394,7 @@ ja: follow_request: フォローリクエストを受けた時 mention: 返信が来た時 pending_account: 新しいアカウントの承認が必要な時 + pending_friend_server: 新しいフレンドサーバーの承認が必要な時 reblog: 投稿がブーストされた時 report: 新しい通報が送信された時 software_updates: diff --git a/config/navigation.rb b/config/navigation.rb index e9552f4f7a6c64..25bc5ecd435b6d 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -66,6 +66,7 @@ s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_path, highlights_on: %r{/admin/custom_emojis}, if: -> { current_user.can?(:manage_custom_emojis) } s.item :webhooks, safe_join([fa_icon('inbox fw'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}, if: -> { current_user.can?(:manage_webhooks) } s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_path, highlights_on: %r{/admin/relays}, if: -> { !limited_federation_mode? && current_user.can?(:manage_federation) } + s.item :friend_servers, safe_join([fa_icon('users fw'), t('admin.friend_servers.title')]), admin_friend_servers_path, highlights_on: %r{/admin/friend_servers}, if: -> { current_user.can?(:manage_federation) } end n.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_path, link_html: { target: 'sidekiq' }, if: -> { current_user.can?(:view_devops) } diff --git a/config/routes/admin.rb b/config/routes/admin.rb index c3ae2efa939e6b..8c10f5935b21cb 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -69,6 +69,15 @@ end end + resources :friend_servers, only: [:index, :new, :edit, :create, :update, :destroy] do + member do + post :follow + post :unfollow + post :accept + post :reject + end + end + resources :instances, only: [:index, :show, :destroy], constraints: { id: %r{[^/]+} }, format: 'html' do member do post :clear_delivery_errors diff --git a/config/settings.yml b/config/settings.yml index c0b5f4109bd6aa..1fb106e680a2e9 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -43,6 +43,7 @@ defaults: &defaults enable_emoji_reaction: true check_lts_version_only: true enable_public_unlisted_visibility: true + unlocked_friend: false development: <<: *defaults diff --git a/db/migrate/20231005074832_create_friend_domains.rb b/db/migrate/20231005074832_create_friend_domains.rb new file mode 100644 index 00000000000000..95eaf8a74c2b1f --- /dev/null +++ b/db/migrate/20231005074832_create_friend_domains.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class CreateFriendDomains < ActiveRecord::Migration[7.0] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def change + create_table :friend_domains do |t| + t.string :domain, null: false, default: '', index: { unique: true } + t.string :inbox_url, null: false, default: '', index: { unique: true } + t.integer :active_state, null: false, default: 0 + t.integer :passive_state, null: false, default: 0 + t.string :active_follow_activity_id, null: true + t.string :passive_follow_activity_id, null: true + t.boolean :available, null: false, default: true + t.boolean :pseudo_relay, null: false, default: false + t.boolean :unlocked, null: false, default: false + t.boolean :allow_all_posts, null: false, default: true + t.datetime :created_at, null: false + t.datetime :updated_at, null: false + end + end +end diff --git a/db/migrate/20231006030102_add_reject_friend_to_domain_blocks.rb b/db/migrate/20231006030102_add_reject_friend_to_domain_blocks.rb new file mode 100644 index 00000000000000..01204db5f112ff --- /dev/null +++ b/db/migrate/20231006030102_add_reject_friend_to_domain_blocks.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class AddRejectFriendToDomainBlocks < ActiveRecord::Migration[7.0] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + def change + safety_assured do + add_column_with_default :domain_blocks, :reject_friend, :boolean, default: false, allow_null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 32cf51a16c92a2..4a1401efbc69ee 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -584,6 +584,7 @@ t.boolean "hidden_anonymous", default: false, null: false t.boolean "detect_invalid_subscription", default: false, null: false t.boolean "reject_reply_exclude_followers", default: false, null: false + t.boolean "reject_friend", default: false, null: false t.index ["domain"], name: "index_domain_blocks_on_domain", unique: true end @@ -676,6 +677,23 @@ t.index ["target_account_id"], name: "index_follows_on_target_account_id" end + create_table "friend_domains", force: :cascade do |t| + t.string "domain", default: "", null: false + t.string "inbox_url", default: "", null: false + t.integer "active_state", default: 0, null: false + t.integer "passive_state", default: 0, null: false + t.string "active_follow_activity_id" + t.string "passive_follow_activity_id" + t.boolean "available", default: true, null: false + t.boolean "pseudo_relay", default: false, null: false + t.boolean "unlocked", default: false, null: false + t.boolean "allow_all_posts", default: true, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["domain"], name: "index_friend_domains_on_domain", unique: true + t.index ["inbox_url"], name: "index_friend_domains_on_inbox_url", unique: true + end + create_table "identities", force: :cascade do |t| t.string "provider", default: "", null: false t.string "uid", default: "", null: false diff --git a/lib/tasks/tests.rake b/lib/tasks/tests.rake index 7f8e72dd8f43b9..980823731e9e1f 100644 --- a/lib/tasks/tests.rake +++ b/lib/tasks/tests.rake @@ -136,7 +136,7 @@ namespace :tests do INSERT INTO "settings" (id, thing_type, thing_id, var, value, created_at, updated_at) VALUES - (3, 'User', 1, 'notification_emails', E'--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nfollow: false\nreblog: true\nfavourite: true\nmention: false\nfollow_request: true\ndigest: true\nreport: true\npending_account: false\ntrending_tag: true\nappeal: true\n', now(), now()), + (3, 'User', 1, 'notification_emails', E'--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nfollow: false\nreblog: true\nfavourite: true\nmention: false\nfollow_request: true\ndigest: true\nreport: true\npending_account: false\npending_friend_server: true\ntrending_tag: true\nappeal: true\n', now(), now()), (4, 'User', 1, 'trends', E'--- false\n', now(), now()); INSERT INTO "accounts" diff --git a/spec/fabricators/friend_domain_fabricator.rb b/spec/fabricators/friend_domain_fabricator.rb new file mode 100644 index 00000000000000..840f79ea3e4bd1 --- /dev/null +++ b/spec/fabricators/friend_domain_fabricator.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +Fabricator(:friend_domain) do + domain 'example.com' + inbox_url 'https://example.com/inbox' + active_state :idle + passive_state :idle + available true + before_create { |friend_domain, _| friend_domain.inbox_url = "https://#{friend_domain.domain}/inbox" if friend_domain.inbox_url.blank? } +end diff --git a/spec/lib/activitypub/activity/accept_spec.rb b/spec/lib/activitypub/activity/accept_spec.rb index d6b60712794e9e..60275570928e5b 100644 --- a/spec/lib/activitypub/activity/accept_spec.rb +++ b/spec/lib/activitypub/activity/accept_spec.rb @@ -43,6 +43,35 @@ end end + context 'when sender is from friend server' do + subject { described_class.new(json, sender) } + + let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } + let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', active_state: :pending, active_follow_activity_id: 'https://abc-123/456') } + + before do + allow(RemoteAccountRefreshWorker).to receive(:perform_async) + Fabricate(:follow_request, account: recipient, target_account: sender) + subject.perform + end + + it 'creates a follow relationship' do + expect(recipient.following?(sender)).to be true + end + + it 'removes the follow request' do + expect(recipient.requested?(sender)).to be false + end + + it 'queues a refresh' do + expect(RemoteAccountRefreshWorker).to have_received(:perform_async).with(sender.id) + end + + it 'friend server is not changed' do + expect(friend.reload.i_am_pending?).to be true + end + end + context 'when given a relay' do subject { described_class.new(json, sender) } @@ -68,4 +97,26 @@ expect(relay.reload.accepted?).to be true end end + + context 'when given a friend server' do + subject { described_class.new(json, sender) } + + let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } + let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', active_state: :pending, active_follow_activity_id: 'https://abc-123/456') } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Accept', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://abc-123/456', + }.with_indifferent_access + end + + it 'marks the friend as accepted' do + subject.perform + expect(friend.reload.i_am_accepted?).to be true + end + end end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 24a404d7264ae4..58aca0757327cc 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -234,6 +234,25 @@ end end + context 'when public_unlisted with LocalPublic' do + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + to: ['http://example.com/followers', 'LocalPublic'], + cc: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.visibility).to eq 'public_unlisted' + end + end + context 'when private' do let(:object_json) do { @@ -411,6 +430,17 @@ end end + context 'with public_unlisted with LocalPublic' do + let(:searchable_by) { ['http://example.com/followers', 'LocalPublic'] } + + it 'create status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.searchability).to eq 'public_unlisted' + end + end + context 'with private' do let(:searchable_by) { 'http://example.com/followers' } @@ -1462,6 +1492,30 @@ end end + context 'when sender is in friend server' do + subject { described_class.new(json, sender, delivery: true) } + + before do + Fabricate(:friend_domain, domain: sender.domain, active_state: :accepted, passive_state: :accepted) + subject.perform + end + + let(:object_json) do + { + id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join, + type: 'Note', + content: 'Lorem ipsum', + } + end + + it 'creates status' do + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.text).to eq 'Lorem ipsum' + end + end + context 'when the sender has no relevance to local activity' do subject { described_class.new(json, sender, delivery: true) } diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index 3a73b3726c984e..f0c957c8a162c0 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -73,4 +73,30 @@ end end end + + context 'when given a friend server' do + subject { described_class.new(json, sender) } + + before do + Fabricate(:friend_domain, domain: 'abc.com', inbox_url: 'https://abc.com/inbox', passive_state: :accepted) + stub_request(:post, 'https://abc.com/inbox') + end + + let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Delete', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://www.w3.org/ns/activitystreams#Public', + }.with_indifferent_access + end + + it 'marks the friend as deleted' do + subject.perform + expect(FriendDomain.find_by(domain: 'abc.com')).to be_nil + end + end end diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index 57f2b077182dcc..a69fa9b5634995 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -37,6 +37,23 @@ end end + context 'with an unlocked account from friend server' do + let!(:friend) { Fabricate(:friend_domain, domain: sender.domain, passive_state: :idle) } + + before do + subject.perform + end + + it 'creates a follow from sender to recipient' do + expect(sender.following?(recipient)).to be true + expect(sender.active_relationships.find_by(target_account: recipient).uri).to eq 'foo' + end + + it 'does not change friend server passive status' do + expect(friend.they_are_idle?).to be true + end + end + context 'when silenced account following an unlocked account' do before do sender.touch(:silenced_at) @@ -285,4 +302,148 @@ end end end + + context 'when given a friend server' do + subject { described_class.new(json, sender) } + + let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } + let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', passive_state: :idle) } + let!(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) } + let!(:patch_user) { Fabricate(:user, role: Fabricate(:user_role, name: 'OhagiOps', permissions: UserRole::FLAGS[:manage_federation])) } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Follow', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://www.w3.org/ns/activitystreams#Public', + }.with_indifferent_access + end + + it 'marks the friend as pending' do + subject.perform + expect(friend.reload.they_are_pending?).to be true + expect(friend.passive_follow_activity_id).to eq 'foo' + end + + context 'when no record' do + before do + friend.update(domain: 'def.com') + end + + it 'marks the friend as pending' do + subject.perform + + friend = FriendDomain.find_by(domain: 'abc.com') + expect(friend).to_not be_nil + expect(friend.they_are_pending?).to be true + expect(friend.passive_follow_activity_id).to eq 'foo' + end + end + + context 'with sending email' do + around do |example| + queue_adapter = ActiveJob::Base.queue_adapter + ActiveJob::Base.queue_adapter = :test + + example.run + + ActiveJob::Base.queue_adapter = queue_adapter + end + + it 'perform' do + expect { subject.perform }.to have_enqueued_mail(AdminMailer, :new_pending_friend_server) + .with(hash_including(params: { recipient: owner_user.account })).once + .and(have_enqueued_mail(AdminMailer, :new_pending_friend_server).with(hash_including(params: { recipient: patch_user.account })).once) + .and(have_enqueued_mail.at_most(2)) + end + end + + context 'when after rejected' do + before do + friend.update(passive_state: :rejected) + end + + it 'marks the friend as pending' do + subject.perform + expect(friend.reload.they_are_pending?).to be true + expect(friend.passive_follow_activity_id).to eq 'foo' + end + end + + context 'when unlocked' do + before do + friend.update(unlocked: true) + stub_request(:post, 'https://example.com/inbox') + end + + it 'marks the friend as accepted' do + subject.perform + + friend = FriendDomain.find_by(domain: 'abc.com') + expect(friend).to_not be_nil + expect(friend.they_are_accepted?).to be true + expect(a_request(:post, 'https://example.com/inbox').with(body: hash_including({ + id: 'foo#accepts/friends', + type: 'Accept', + object: 'foo', + }))).to have_been_made.once + end + end + + context 'when unlocked on admin settings' do + before do + Form::AdminSettings.new(unlocked_friend: '1').save + stub_request(:post, 'https://example.com/inbox') + end + + it 'marks the friend as accepted' do + subject.perform + + friend = FriendDomain.find_by(domain: 'abc.com') + expect(friend).to_not be_nil + expect(friend.they_are_accepted?).to be true + expect(a_request(:post, 'https://example.com/inbox').with(body: hash_including({ + id: 'foo#accepts/friends', + type: 'Accept', + object: 'foo', + }))).to have_been_made.once + end + end + + context 'when already accepted' do + before do + friend.update(passive_state: :accepted) + stub_request(:post, 'https://example.com/inbox') + end + + it 'marks the friend as accepted' do + subject.perform + + friend = FriendDomain.find_by(domain: 'abc.com') + expect(friend).to_not be_nil + expect(friend.they_are_accepted?).to be true + expect(a_request(:post, 'https://example.com/inbox').with(body: hash_including({ + id: 'foo#accepts/friends', + type: 'Accept', + object: 'foo', + }))).to have_been_made.once + end + end + + context 'when domain blocked' do + before do + friend.update(domain: 'def.com') + end + + it 'marks the friend rejected' do + Fabricate(:domain_block, domain: 'abc.com', reject_friend: true) + subject.perform + + friend = FriendDomain.find_by(domain: 'abc.com') + expect(friend).to be_nil + end + end + end end diff --git a/spec/lib/activitypub/activity/reject_spec.rb b/spec/lib/activitypub/activity/reject_spec.rb index 0a4243cd16afd7..1eee37db35c965 100644 --- a/spec/lib/activitypub/activity/reject_spec.rb +++ b/spec/lib/activitypub/activity/reject_spec.rb @@ -122,6 +122,30 @@ end end + context 'when sender is from friend server' do + subject { described_class.new(json, sender) } + + let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } + let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', active_state: :pending, active_follow_activity_id: 'https://abc-123/456') } + + before do + Fabricate(:follow_request, account: recipient, target_account: sender) + subject.perform + end + + it 'does not create a follow relationship' do + expect(recipient.following?(sender)).to be false + end + + it 'removes the follow request' do + expect(recipient.requested?(sender)).to be false + end + + it 'friend server is not changed' do + expect(friend.reload.i_am_pending?).to be true + end + end + context 'when given a relay' do subject { described_class.new(json, sender) } @@ -147,4 +171,26 @@ expect(relay.reload.rejected?).to be true end end + + context 'when given a friend' do + subject { described_class.new(json, sender) } + + let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } + let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', active_state: :pending, active_follow_activity_id: 'https://abc-123/456') } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Reject', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://abc-123/456', + }.with_indifferent_access + end + + it 'marks the friend as rejected' do + subject.perform + expect(friend.reload.i_am_rejected?).to be true + end + end end diff --git a/spec/lib/activitypub/activity/undo_spec.rb b/spec/lib/activitypub/activity/undo_spec.rb index 58e71fc4e8de71..1671f04b4b0182 100644 --- a/spec/lib/activitypub/activity/undo_spec.rb +++ b/spec/lib/activitypub/activity/undo_spec.rb @@ -145,6 +145,13 @@ expect(sender.following?(recipient)).to be false end + it 'deletes follow from sender to recipient when has friend' do + friend = Fabricate(:friend_domain, domain: sender.domain, passive_state: :accepted) + subject.perform + expect(sender.following?(recipient)).to be false + expect(friend.they_are_accepted?).to be true + end + context 'with only object uri' do let(:object_json) { 'bar' } @@ -153,6 +160,25 @@ expect(sender.following?(recipient)).to be false end end + + context 'when for a friend' do + let(:sender) { Fabricate(:account, domain: 'abc.com', url: 'https://abc.com/#actor') } + let!(:friend) { Fabricate(:friend_domain, domain: 'abc.com', passive_state: :accepted, passive_follow_activity_id: 'bar') } + let(:object_json) do + { + id: 'bar', + type: 'Follow', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: 'https://www.w3.org/ns/activitystreams#Public', + } + end + + it 'deletes follow from this server to friend' do + subject.perform + expect(friend.reload.they_are_idle?).to be true + expect(friend.passive_follow_activity_id).to be_nil + end + end end context 'with Like' do diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 2bff125a6ae952..bec6233fd531b8 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -27,6 +27,11 @@ expect(subject.to(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] end + it 'returns followers collection for public_unlisted status' do + status = Fabricate(:status, visibility: :public_unlisted) + expect(subject.to(status)).to eq [account_followers_url(status.account)] + end + it 'returns followers collection for unlisted status' do status = Fabricate(:status, visibility: :unlisted) expect(subject.to(status)).to eq [account_followers_url(status.account)] @@ -69,12 +74,34 @@ end end + describe '#to_for_friend' do + it 'returns followers collection for public_unlisted status' do + status = Fabricate(:status, visibility: :public_unlisted) + expect(subject.to_for_friend(status)).to eq [account_followers_url(status.account), 'LocalPublic'] + end + + it 'returns followers collection for unlisted status' do + status = Fabricate(:status, visibility: :unlisted) + expect(subject.to_for_friend(status)).to eq [account_followers_url(status.account)] + end + + it 'returns followers collection for private status' do + status = Fabricate(:status, visibility: :private) + expect(subject.to_for_friend(status)).to eq [account_followers_url(status.account)] + end + end + describe '#cc' do it 'returns followers collection for public status' do status = Fabricate(:status, visibility: :public) expect(subject.cc(status)).to eq [account_followers_url(status.account)] end + it 'returns public collection for public_unlisted status' do + status = Fabricate(:status, visibility: :public_unlisted) + expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] + end + it 'returns public collection for unlisted status' do status = Fabricate(:status, visibility: :unlisted) expect(subject.cc(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] @@ -114,6 +141,74 @@ end end + describe '#cc_for_misskey' do + let(:user) { Fabricate(:user) } + + before do + user.settings.update(reject_unlisted_subscription: true, reject_public_unlisted_subscription: true) + user.save + end + + it 'returns public collection for public status' do + status = Fabricate(:status, visibility: :public) + expect(subject.cc_for_misskey(status)).to eq [account_followers_url(status.account)] + end + + it 'returns empty array for public_unlisted status' do + status = Fabricate(:status, account: user.account, visibility: :public_unlisted) + expect(subject.cc_for_misskey(status)).to eq [] + end + + it 'returns empty array for unlisted status' do + status = Fabricate(:status, account: user.account, visibility: :unlisted) + expect(subject.cc_for_misskey(status)).to eq [] + end + end + + describe '#searchable_by' do + it 'returns public collection for public status' do + status = Fabricate(:status, searchability: :public) + expect(subject.searchable_by(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] + end + + it 'returns followers collection for public_unlisted status' do + status = Fabricate(:status, searchability: :public_unlisted) + expect(subject.searchable_by(status)).to eq [account_followers_url(status.account)] + end + + it 'returns followers collection for private status' do + status = Fabricate(:status, searchability: :private) + expect(subject.searchable_by(status)).to eq [account_followers_url(status.account)] + end + + it 'returns empty array for direct status' do + status = Fabricate(:status, searchability: :direct) + expect(subject.searchable_by(status)).to eq [] + end + + it 'returns as:Limited array for limited status' do + status = Fabricate(:status, searchability: :limited) + expect(subject.searchable_by(status)).to eq ['as:Limited'] + end + end + + describe '#searchable_by_for_friend' do + it 'returns public collection for public status' do + status = Fabricate(:status, account: Fabricate(:account, searchability: :public), searchability: :public) + expect(subject.searchable_by_for_friend(status)).to eq ['https://www.w3.org/ns/activitystreams#Public'] + end + + it 'returns public collection for public_unlisted status' do + status = Fabricate(:status, account: Fabricate(:account, searchability: :public), searchability: :public_unlisted) + expect(subject.searchable_by_for_friend(status)).to eq [account_followers_url(status.account), 'LocalPublic'] + end + + it 'returns followers collection for private status' do + status = Fabricate(:status, account: Fabricate(:account, searchability: :public), searchability: :private) + expect(subject.searchable_by_for_friend(status)).to eq [account_followers_url(status.account)] + end + end + describe '#local_uri?' do it 'returns false for non-local URI' do expect(subject.local_uri?('http://example.com/123')).to be false diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb index 3d51fedb1458af..44b5bb39e86a52 100644 --- a/spec/lib/status_reach_finder_spec.rb +++ b/spec/lib/status_reach_finder_spec.rb @@ -24,12 +24,14 @@ it 'send status' do expect(subject.inboxes).to include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to_not include 'https://foo.bar/inbox' end end context 'with non-follower' do it 'send status' do expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to_not include 'https://foo.bar/inbox' end end end @@ -92,6 +94,103 @@ expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox' end end + + context 'when has distributable friend server' do + let(:sender_software) { 'misskey' } + let(:searchability) { :public } + + before { Fabricate(:friend_domain, domain: 'foo.bar', inbox_url: 'https://foo.bar/inbox', available: true, active_state: :accepted, passive_state: :accepted, pseudo_relay: true) } + + it 'send status without friend server' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to include 'https://foo.bar/inbox' + end + end + end + + context 'when this server has a friend' do + let(:bob) { Fabricate(:account, username: 'bob', domain: 'foo.bar', protocol: :activitypub, inbox_url: 'https://foo.bar/inbox') } + + context 'with follower' do + before do + Fabricate(:friend_domain, domain: 'foo.bar', active_state: :accepted) + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to include 'https://foo.bar/inbox' + end + end + + context 'with non-follower' do + before do + Fabricate(:friend_domain, domain: 'foo.bar', active_state: :accepted) + end + + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to_not include 'https://foo.bar/inbox' + end + end + + context 'with pending' do + before do + Fabricate(:friend_domain, domain: 'foo.bar', active_state: :pending) + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to_not include 'https://foo.bar/inbox' + end + end + + context 'when unavailable' do + before do + Fabricate(:friend_domain, domain: 'foo.bar', active_state: :accepted, available: false) + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to_not include 'https://foo.bar/inbox' + end + end + + context 'when distributable' do + before do + Fabricate(:friend_domain, domain: 'foo.bar', active_state: :accepted, passive_state: :accepted, pseudo_relay: true) + bob.follow!(alice) + end + + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to include 'https://foo.bar/inbox' + end + end + + context 'when distributable and not following' do + before do + Fabricate(:friend_domain, domain: 'foo.bar', inbox_url: 'https://foo.bar/inbox', active_state: :accepted, passive_state: :accepted, pseudo_relay: true) + end + + it 'send status' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to include 'https://foo.bar/inbox' + end + end + end + + context 'when it contains distributable friend server' do + before { Fabricate(:friend_domain, domain: 'foo.bar', inbox_url: 'https://foo.bar/inbox', available: true, active_state: :accepted, passive_state: :accepted, pseudo_relay: true) } + + it 'includes the inbox of the mentioned account' do + expect(subject.inboxes).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_misskey).to_not include 'https://foo.bar/inbox' + expect(subject.inboxes_for_friend).to include 'https://foo.bar/inbox' + end end context 'when it contains mentions of remote accounts' do @@ -255,4 +354,99 @@ end end end + + describe '#inboxes_for_friend and distributables' do + subject { described_class.new(status).inboxes_for_friend } + + let(:visibility) { :public } + let(:searchability) { :public } + let(:alice) { Fabricate(:account, username: 'alice') } + let(:status) { Fabricate(:status, account: alice, visibility: visibility, searchability: searchability) } + + context 'when a simple case' do + before do + Fabricate(:friend_domain, domain: 'abc.com', inbox_url: 'https://abc.com/inbox', active_state: :accepted, passive_state: :accepted, pseudo_relay: true, available: true) + Fabricate(:friend_domain, domain: 'def.com', inbox_url: 'https://def.com/inbox', active_state: :accepted, passive_state: :accepted, pseudo_relay: true, available: true) + Fabricate(:friend_domain, domain: 'ghi.com', inbox_url: 'https://ghi.com/inbox', active_state: :accepted, passive_state: :accepted, pseudo_relay: true, available: false) + Fabricate(:friend_domain, domain: 'jkl.com', inbox_url: 'https://jkl.com/inbox', active_state: :accepted, passive_state: :accepted, pseudo_relay: false, available: true) + Fabricate(:friend_domain, domain: 'mno.com', inbox_url: 'https://mno.com/inbox', active_state: :accepted, passive_state: :pending, pseudo_relay: true, available: true) + Fabricate(:friend_domain, domain: 'pqr.com', inbox_url: 'https://pqr.com/inbox', active_state: :accepted, passive_state: :accepted, pseudo_relay: true, available: true) + Fabricate(:unavailable_domain, domain: 'pqr.com') + end + + it 'returns friend servers' do + expect(subject).to include 'https://abc.com/inbox' + expect(subject).to include 'https://def.com/inbox' + end + + it 'not contains unavailable friends' do + expect(subject).to_not include 'https://ghi.com/inbox' + end + + it 'not contains no-relay friends' do + expect(subject).to_not include 'https://jkl.com/inbox' + end + + it 'not contains no-mutual friends' do + expect(subject).to_not include 'https://mno.com/inbox' + end + + it 'not contains unavailable domain friends' do + expect(subject).to_not include 'https://pqr.com/inbox' + end + + context 'when public visibility' do + let(:visibility) { :public } + let(:searchability) { :direct } + + it 'returns friend servers' do + expect(subject).to_not eq [] + end + end + + context 'when public_unlsited visibility' do + let(:visibility) { :public_unlisted } + let(:searchability) { :direct } + + it 'returns friend servers' do + expect(subject).to_not eq [] + end + end + + context 'when unlsited visibility with public searchability' do + let(:visibility) { :unlisted } + let(:searchability) { :public } + + it 'returns friend servers' do + expect(subject).to_not eq [] + end + end + + context 'when unlsited visibility with public_unlisted searchability' do + let(:visibility) { :unlisted } + let(:searchability) { :public_unlisted } + + it 'returns friend servers' do + expect(subject).to_not eq [] + end + end + + context 'when unlsited visibility with private searchability' do + let(:visibility) { :unlisted } + let(:searchability) { :private } + + it 'returns empty servers' do + expect(subject).to eq [] + end + end + + context 'when private visibility' do + let(:visibility) { :private } + + it 'returns friend servers' do + expect(subject).to eq [] + end + end + end + end end diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index 423dce88ab09ee..23b99a68cc1619 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -64,6 +64,26 @@ end end + describe '.new_pending_friend_server' do + let(:recipient) { Fabricate(:account, username: 'Barklums') } + let(:friend) { Fabricate(:friend_domain, passive_state: :pending, domain: 'abc.com') } + let(:mail) { described_class.with(recipient: recipient).new_pending_friend_server(friend) } + + before do + recipient.user.update(locale: :en) + end + + it 'renders the headers' do + expect(mail.subject).to eq('New friend server up for review on cb6e6126.ngrok.io (abc.com)') + expect(mail.to).to eq [recipient.user_email] + expect(mail.from).to eq ['notifications@localhost'] + end + + it 'renders the body' do + expect(mail.body.encoded).to match 'The new friend server abc.com is waiting for your review. You can approve or reject this application.' + end + end + describe '.new_trends' do let(:recipient) { Fabricate(:account, username: 'Snurf') } let(:links) { [] } diff --git a/spec/mailers/previews/admin_mailer_preview.rb b/spec/mailers/previews/admin_mailer_preview.rb index bc8f0193b9cd7b..7ba6f08239ecb8 100644 --- a/spec/mailers/previews/admin_mailer_preview.rb +++ b/spec/mailers/previews/admin_mailer_preview.rb @@ -8,6 +8,11 @@ def new_pending_account AdminMailer.with(recipient: Account.first).new_pending_account(User.pending.first) end + # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_pending_friend_server + def new_pending_friend_server + AdminMailer.with(recipient: Account.first).new_pending_friend_server(User.pending.first) + end + # Preview this email at http://localhost:3000/rails/mailers/admin_mailer/new_trends def new_trends AdminMailer.with(recipient: Account.first).new_trends(PreviewCard.joins(:trend).limit(3), Tag.limit(3), Status.joins(:trend).where(reblog_of_id: nil).limit(3)) diff --git a/spec/models/friend_domain_spec.rb b/spec/models/friend_domain_spec.rb new file mode 100644 index 00000000000000..c3fa128b1621b6 --- /dev/null +++ b/spec/models/friend_domain_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe FriendDomain do + let(:friend) { Fabricate(:friend_domain, domain: 'foo.bar', inbox_url: 'https://foo.bar/inbox') } + + before do + stub_request(:post, 'https://foo.bar/inbox') + end + + describe '#follow!' do + it 'call inbox' do + friend.follow! + expect(friend.active_follow_activity_id).to_not be_nil + expect(friend.i_am_pending?).to be true + expect(a_request(:post, 'https://foo.bar/inbox').with(body: hash_including({ + id: friend.active_follow_activity_id, + type: 'Follow', + actor: 'https://cb6e6126.ngrok.io/actor', + object: 'https://www.w3.org/ns/activitystreams#Public', + }))).to have_been_made.once + end + end + + describe '#unfollow!' do + it 'call inbox' do + friend.update(active_follow_activity_id: 'ohagi') + friend.unfollow! + expect(friend.active_follow_activity_id).to be_nil + expect(friend.i_am_idle?).to be true + expect(a_request(:post, 'https://foo.bar/inbox').with(body: hash_including({ + type: 'Undo', + object: { + id: 'ohagi', + type: 'Follow', + actor: 'https://cb6e6126.ngrok.io/actor', + object: 'https://www.w3.org/ns/activitystreams#Public', + }, + }))).to have_been_made.once + end + end + + describe '#accept!' do + it 'call inbox' do + friend.update(passive_follow_activity_id: 'ohagi', passive_state: :pending) + friend.accept! + expect(friend.they_are_accepted?).to be true + expect(a_request(:post, 'https://foo.bar/inbox').with(body: hash_including({ + id: 'ohagi#accepts/friends', + type: 'Accept', + actor: 'https://cb6e6126.ngrok.io/actor', + object: 'ohagi', + }))).to have_been_made.once + end + end + + describe '#reject!' do + it 'call inbox' do + friend.update(passive_follow_activity_id: 'ohagi', passive_state: :pending) + friend.reject! + expect(friend.they_are_rejected?).to be true + expect(a_request(:post, 'https://foo.bar/inbox').with(body: hash_including({ + id: 'ohagi#rejects/friends', + type: 'Reject', + actor: 'https://cb6e6126.ngrok.io/actor', + object: 'ohagi', + }))).to have_been_made.once + end + end + + describe '#delete!' do + it 'call inbox' do + friend.update(active_state: :pending) + friend.destroy + expect(a_request(:post, 'https://foo.bar/inbox').with(body: hash_including({ + type: 'Delete', + actor: 'https://cb6e6126.ngrok.io/actor', + object: 'https://www.w3.org/ns/activitystreams#Public', + }))).to have_been_made.once + end + end +end diff --git a/spec/models/public_feed_spec.rb b/spec/models/public_feed_spec.rb index 965e293c5e0ab6..3d13783a50f103 100644 --- a/spec/models/public_feed_spec.rb +++ b/spec/models/public_feed_spec.rb @@ -89,7 +89,7 @@ end it 'excludes public_unlisted statuses' do - expect(subject).to_not include(public_unlisted_status.id) + expect(subject).to include(public_unlisted_status.id) end end @@ -105,7 +105,7 @@ end it 'excludes public_unlisted statuses' do - expect(subject).to_not include(public_unlisted_status.id) + expect(subject).to include(public_unlisted_status.id) end end end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 96f6bb56c9db95..136b30193333e5 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -114,7 +114,7 @@ end end - describe '#searchability' do + describe '#compute_searchability' do subject { Fabricate(:status, account: account, searchability: status_searchability) } let(:account_searchability) { :public } @@ -146,6 +146,18 @@ end end + context 'when public-public_unlisted' do + let(:status_searchability) { :public_unlisted } + + it 'returns public' do + expect(subject.compute_searchability).to eq 'public' + end + + it 'returns public_unlisted for local' do + expect(subject.compute_searchability_local).to eq 'public_unlisted' + end + end + context 'when public-private' do let(:status_searchability) { :private } diff --git a/spec/services/block_domain_service_spec.rb b/spec/services/block_domain_service_spec.rb index 36dce9d1963f2d..30fb760487f21b 100644 --- a/spec/services/block_domain_service_spec.rb +++ b/spec/services/block_domain_service_spec.rb @@ -10,9 +10,13 @@ let!(:bad_status_with_attachment) { Fabricate(:status, account: bad_account, text: 'Hahaha') } let!(:bad_attachment) { Fabricate(:media_attachment, account: bad_account, status: bad_status_with_attachment, file: attachment_fixture('attachment.jpg')) } let!(:already_banned_account) { Fabricate(:account, username: 'badguy', domain: 'evil.org', suspended: true, silenced: true) } + let!(:bad_friend) { Fabricate(:friend_domain, domain: 'evil.org', inbox_url: 'https://evil.org/inbox', active_state: :accepted, passive_state: :accepted) } describe 'for a suspension' do before do + stub_request(:post, 'https://evil.org/inbox').with(body: hash_including({ + type: 'Delete', + })) subject.call(DomainBlock.create!(domain: 'evil.org', severity: :suspend)) end @@ -41,6 +45,21 @@ expect { bad_status_with_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound expect { bad_attachment.reload }.to raise_exception ActiveRecord::RecordNotFound end + + it 'removes remote friend from that domain' do + expect(FriendDomain.find_by(domain: 'evil.org')).to be_nil + end + end + + describe 'for rejecting friend only' do + before do + stub_request(:post, 'https://evil.org/inbox') + subject.call(DomainBlock.create!(domain: 'evil.org', severity: :noop, reject_friend: true)) + end + + it 'removes remote friend from that domain' do + expect(FriendDomain.find_by(domain: 'evil.org')).to be_nil + end end describe 'for a silence with reject media' do diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb index 34169fcb714c79..17d9f9125260d8 100644 --- a/spec/services/fan_out_on_write_service_spec.rb +++ b/spec/services/fan_out_on_write_service_spec.rb @@ -278,7 +278,7 @@ def antenna_with_options(owner, **options) it 'is broadcast publicly' do expect(redis).to have_received(:publish).with('timeline:hashtag:hoge', anything) expect(redis).to have_received(:publish).with('timeline:public:local', anything) - expect(redis).to_not have_received(:publish).with('timeline:public', anything) + expect(redis).to have_received(:publish).with('timeline:public', anything) end context 'with list' do