Add: フレンドサーバー (#61)

* Fix mastodon version

* テーブル作成

* Wip: フレンドサーバーフォローの承認を受信

* Wip: フレンド申請拒否を受信

* Wip: フォローリクエストを受理

* Wip: 相手からのフォロー・アンフォローを受理

* 普通のフォローとフレンドサーバーのフォローを区別するテストを追加

* ドメインブロックによるフォロー拒否

* ドメインブロックしたあと、申請中のフォロリクを取り下げる処理

* スタブに条件を追加

* Wip: 相手からのDelete信号に対応

* DB定義が消えていたので修正

* Wip: ローカル公開投稿をフレンドに送信する処理など

* Wip: 未収載+誰でもの投稿をフレンドに送る設定

* Wip: ローカル公開をそのまま送信する設定を考慮

* Fix test

* Wip: 他サーバーからのローカル公開投稿の受け入れ

* Wip: Web画面作成

* Fix test

* Wip: ローカル公開を連合TLに流す

* Wip: フレンドサーバーの削除ボタン

* Wip: メール通知や設定のテストなど

* Wip: 翻訳を作成

* Fix: 却下されたあとフォローボタンが表示されない問題

* Wip: 編集できない問題

* 有効にしていないフレンドサーバーをリストで無効表示
This commit is contained in:
KMY(雪あすか) 2023-10-09 11:51:15 +09:00 committed by GitHub
parent acb29e5b11
commit 87e858a202
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 1638 additions and 51 deletions

View file

@ -263,6 +263,10 @@ module HasUserSettings
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

View file

@ -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 @@ class DomainBlock < ApplicationRecord
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 @@ class DomainBlock < ApplicationRecord
!!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

View file

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

163
app/models/friend_domain.rb Normal file
View file

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

View file

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

View file

@ -19,7 +19,7 @@ class PublicFeed
# @param [Integer] min_id
# @return [Array<Status>]
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 @@ class PublicFeed
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

View file

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

View file

@ -80,6 +80,7 @@ class UserSettings
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)