Merge branch 'kb_development' into kb_migration

This commit is contained in:
KMY 2023-08-13 19:05:32 +09:00
commit c7fe057f92
40 changed files with 338 additions and 29 deletions

View file

@ -62,7 +62,12 @@ class StatusesController < ApplicationController
def set_status
@status = @account.statuses.find(params[:id])
authorize @status, :show?
if request.authorization.present? && request.authorization.match(/^Bearer /i)
raise Mastodon::NotPermittedError unless @status.capability_tokens.find_by(token: request.authorization.gsub(/^Bearer /i, ''))
else
authorize @status, :show?
end
rescue Mastodon::NotPermittedError
not_found
end

View file

@ -24,6 +24,7 @@ module ContextHelper
emoji_reactions: { 'fedibird' => 'http://fedibird.com/ns#', 'emojiReactions' => { '@id' => 'fedibird:emojiReactions', '@type' => '@id' } },
searchable_by: { 'fedibird' => 'http://fedibird.com/ns#', 'searchableBy' => { '@id' => 'fedibird:searchableBy', '@type' => '@id' } },
subscribable_by: { 'kmyblue' => 'http://kmy.blue/ns#', 'subscribableBy' => { '@id' => 'kmyblue:subscribableBy', '@type' => '@id' } },
limited_scope: { 'kmyblue' => 'http://kmy.blue/ns#', 'limitedScope' => { '@id' => 'kmyblue:limitedScope', '@type' => '@id' } },
other_setting: { 'fedibird' => 'http://fedibird.com/ns#', 'otherSetting' => 'fedibird:otherSetting' },
references: { 'fedibird' => 'http://fedibird.com/ns#', 'references' => { '@id' => 'fedibird:references', '@type' => '@id' } },
olm: {

View file

@ -77,6 +77,8 @@ module StatusesHelper
fa_icon 'key fw'
when 'private'
fa_icon 'lock fw'
when 'limited'
fa_icon 'get-pocket fw'
when 'direct'
fa_icon 'at fw'
end

View file

@ -69,6 +69,8 @@ const messages = defineMessages({
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});
@ -398,10 +400,12 @@ class Status extends ImmutablePureComponent {
'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) },
'login': { icon: 'key', text: intl.formatMessage(messages.login_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
};
let visibilityIcon = visibilityIconInfo[status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
let visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
if (this.state.forceFilter === undefined ? matchedFilters : this.state.forceFilter) {
const minHandlers = this.props.muted ? {} : {
@ -562,7 +566,7 @@ class Status extends ImmutablePureComponent {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
visibilityIcon = visibilityIconInfo[status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')] || visibilityIconInfo[status.get('visibility')];
let emojiReactionsBar = null;
if (!this.props.withoutEmojiReactions && status.get('emoji_reactions')) {

View file

@ -24,6 +24,8 @@ const messages = defineMessages({
login_long: { id: 'privacy.login.long', defaultMessage: 'Login user only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual' },
mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
@ -232,6 +234,7 @@ class PrivacyDropdown extends PureComponent {
{ icon: 'key', value: 'login', text: formatMessage(messages.login_short), meta: formatMessage(messages.login_long) },
{ icon: 'unlock', value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
{ icon: 'lock', value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
{ icon: 'exchange', value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long) },
{ icon: 'at', value: 'direct', text: formatMessage(messages.direct_short), meta: formatMessage(messages.direct_long) },
];
this.selectableOptions = [...this.options];

View file

@ -20,6 +20,8 @@ const messages = defineMessages({
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
});
@ -51,10 +53,12 @@ class StatusCheckBox extends PureComponent {
'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) },
'login': { icon: 'key', text: intl.formatMessage(messages.login_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility_ex')];
const visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')];
const labelComponent = (
<div className='status-check-box__status poll__option__text'>

View file

@ -30,6 +30,8 @@ const messages = defineMessages({
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
searchability_public_short: { id: 'searchability.public.short', defaultMessage: 'Public' },
searchability_private_short: { id: 'searchability.unlisted.short', defaultMessage: 'Followers' },
@ -249,10 +251,12 @@ class DetailedStatus extends ImmutablePureComponent {
'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) },
'login': { icon: 'key', text: intl.formatMessage(messages.login_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility_ex')];
const visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')];
const visibilityLink = <> · <Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></>;
const searchabilityIconInfo = {

View file

@ -27,6 +27,8 @@ const messages = defineMessages({
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited menbers only' },
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual followers only' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
});
@ -94,10 +96,12 @@ class BoostModal extends ImmutablePureComponent {
'public_unlisted': { icon: 'cloud', text: intl.formatMessage(messages.public_unlisted_short) },
'login': { icon: 'key', text: intl.formatMessage(messages.login_short) },
'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
'limited': { icon: 'get-pocket', text: intl.formatMessage(messages.limited_short) },
'mutual': { icon: 'exchange', text: intl.formatMessage(messages.mutual_short) },
'direct': { icon: 'at', text: intl.formatMessage(messages.direct_short) },
};
const visibilityIcon = visibilityIconInfo[status.get('visibility_ex')];
const visibilityIcon = visibilityIconInfo[status.get('limited_scope') || status.get('visibility_ex')];
return (
<div className='modal-root__modal boost-modal'>

View file

@ -520,8 +520,11 @@
"privacy.change": "Change post privacy",
"privacy.direct.long": "Visible for mentioned users only",
"privacy.direct.short": "Mentioned people only",
"privacy.limited.short": "Limited",
"privacy.login.long": "Visible for login users only",
"privacy.login.short": "Login only",
"privacy.mutual.long": "Mutual followers only",
"privacy.mutual.short": "Mutual",
"privacy.private.long": "Visible for followers only",
"privacy.private.short": "Followers only",
"privacy.public.long": "Visible for all",

View file

@ -529,8 +529,11 @@
"privacy.change": "公開範囲を変更",
"privacy.direct.long": "指定された相手のみ閲覧可",
"privacy.direct.short": "指定された相手のみ",
"privacy.limited.short": "限定投稿",
"privacy.login.long": "ログインユーザーのみ閲覧可、公開",
"privacy.login.short": "ログインユーザーのみ",
"privacy.mutual.long": "相互フォローさんのみ閲覧可、限定投稿",
"privacy.mutual.short": "相互のみ",
"privacy.private.long": "フォロワーのみ閲覧可",
"privacy.private.short": "フォロワーのみ",
"privacy.public.long": "誰でも閲覧可、ホームローカル連合TL",

View file

@ -119,7 +119,10 @@ class ActivityPub::Activity
dereferencer = ActivityPub::Dereferencer.new(@object, permitted_origin: @account.uri, signature_actor: signed_fetch_actor)
@object = dereferencer.object unless dereferencer.object.nil?
return if dereferencer.object.nil?
@object = dereferencer.object
@json = @object
end
def signed_fetch_actor

View file

@ -133,6 +133,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
reply: @status_parser.reply,
sensitive: @account.sensitized? || @status_parser.sensitive || false,
visibility: @status_parser.visibility,
limited_scope: @status_parser.limited_scope,
searchability: searchability,
thread: replied_to_status,
conversation: conversation_from_uri(@object['conversation']),

View file

@ -86,6 +86,14 @@ class ActivityPub::Parser::StatusParser
end
end
def limited_scope
if @object['limitedScope'] == 'Mutual'
:mutual
else
:none
end
end
def language
if content_language_map?
@object['contentMap'].keys.first

View file

@ -100,7 +100,7 @@ class ActivityPub::TagManager
[account_followers_url(status.account)]
when 'login'
[account_followers_url(status.account), 'as:LoginOnly', 'LoginUser']
when 'direct', 'limited'
when 'direct'
if status.account.silenced?
# Only notify followers if the account is locally silenced
account_ids = status.active_mentions.pluck(:account_id)
@ -118,6 +118,11 @@ class ActivityPub::TagManager
result << followers_uri_for(mention.account) if mention.account.group?
end.compact
end
when 'limited'
status.mentions.each_with_object([]) do |mention, result|
result << uri_for(mention.account)
result << followers_uri_for(mention.account) if mention.account.group?
end.compact
end
end
@ -216,6 +221,10 @@ class ActivityPub::TagManager
nil
end
def limited_scope(status)
status.mutual_limited? ? 'Mutual' : ''
end
def subscribable_by(account)
account.dissubscribable ? [] : [COLLECTIONS[:public]]
end

View file

@ -29,6 +29,8 @@ class StatusReachFinder
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).where.not(domain: banned_domains).inboxes
end
@ -37,6 +39,8 @@ class StatusReachFinder
def reached_account_inboxes_for_misskey
if @status.reblog?
[]
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
end

View file

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

View file

@ -55,6 +55,10 @@ module HasUserSettings
settings['send_without_domain_blocks']
end
def setting_unsafe_limited_distribution
settings['unsafe_limited_distribution']
end
def setting_stop_emoji_reaction_streaming
settings['stop_emoji_reaction_streaming']
end

View file

@ -29,6 +29,7 @@
# ordered_media_attachment_ids :bigint(8) is an Array
# searchability :integer
# markdown :boolean default(FALSE)
# limited_scope :integer
#
require 'ostruct'
@ -54,6 +55,7 @@ class Status < ApplicationRecord
enum visibility: { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10, login: 11 }, _suffix: :visibility
enum searchability: { public: 0, private: 1, direct: 2, limited: 3, unsupported: 4, public_unlisted: 10 }, _suffix: :searchability
enum limited_scope: { none: 0, mutual: 1 }, _suffix: :limited
belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
@ -79,6 +81,7 @@ class Status < ApplicationRecord
has_many :references, through: :reference_objects, class_name: 'Status', source: :target_status
has_many :referenced_by_status_objects, foreign_key: 'target_status_id', class_name: 'StatusReference', inverse_of: :target_status, dependent: :destroy
has_many :referenced_by_statuses, through: :referenced_by_status_objects, class_name: 'Status', source: :status
has_many :capability_tokens, class_name: 'StatusCapabilityToken', inverse_of: :status, dependent: :destroy
has_and_belongs_to_many :tags
has_and_belongs_to_many :preview_cards

View file

@ -0,0 +1,25 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: status_capability_tokens
#
# id :bigint(8) not null, primary key
# status_id :bigint(8) not null
# token :string
# created_at :datetime not null
# updated_at :datetime not null
#
class StatusCapabilityToken < ApplicationRecord
belongs_to :status
validates :token, presence: true
before_validation :generate_token, on: :create
private
def generate_token
self.token = Doorkeeper::OAuth::Helpers::UniqueToken.generate
end
end

View file

@ -31,6 +31,7 @@ class UserSettings
setting :reaction_deck, default: nil
setting :stop_emoji_reaction_streaming, default: false
setting :emoji_reaction_streaming_notify_impl2, default: false
setting :unsafe_limited_distribution, default: false
namespace :web do
setting :advanced_layout, default: false

View file

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

View file

@ -3,13 +3,13 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer
include FormattingHelper
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :searchable_by, :references
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :searchable_by, :references, :limited_scope
attributes :id, :type, :summary,
:in_reply_to, :published, :url,
:attributed_to, :to, :cc, :sensitive,
:atom_uri, :in_reply_to_atom_uri,
:conversation, :searchable_by
:conversation, :searchable_by, :limited_scope
attribute :references, if: :not_private_post?
@ -148,12 +148,16 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
ActivityPub::TagManager.instance.searchable_by(object)
end
def limited_scope
ActivityPub::TagManager.instance.limited_scope(object)
end
def local?
object.account.local?
end
def not_private_post?
!object.private_visibility?
!object.private_visibility? && !object.direct_visibility? && !object.limited_visibility?
end
def poll_options

View file

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

View file

@ -4,7 +4,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
include FormattingHelper
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id,
:sensitive, :spoiler_text, :visibility, :visibility_ex, :language,
:sensitive, :spoiler_text, :visibility, :visibility_ex, :limited_scope, :language,
:uri, :url, :replies_count, :reblogs_count, :searchability, :markdown,
:status_reference_ids, :status_references_count, :status_referred_by_count,
:favourites_count, :emoji_reactions, :emoji_reactions_count, :reactions, :edited_at
@ -66,11 +66,11 @@ class REST::StatusSerializer < ActiveModel::Serializer
end
def visibility_ex
if object.limited_visibility?
'private'
else
object.visibility
end
object.visibility
end
def limited_scope
!object.none_limited? && object.limited_visibility? ? object.limited_scope : nil
end
def searchability

View file

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

View file

@ -209,7 +209,7 @@ class ActivityPub::ProcessAccountService < BaseService
end
def fetch_instance_info
FetchInstanceInfoWorker.perform_async(@account.domain) unless InstanceInfo.exists?(domain: @account.domain)
ActivityPub::FetchInstanceInfoWorker.perform_async(@account.domain) unless InstanceInfo.exists?(domain: @account.domain)
end
def actor_type

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
class FetchInstanceInfoWorker
class ActivityPub::FetchInstanceInfoWorker
include Sidekiq::Worker
include JsonLdHelper
include Redisable
@ -64,9 +64,9 @@ class FetchInstanceInfoWorker
body_to_json(response.body_with_limit)
elsif response.code == 410
raise FetchInstanceInfoWorker::GoneError, "#{domain} is gone from the server"
raise ActivityPub::FetchInstanceInfoWorker::GoneError, "#{@instance.domain} is gone from the server"
else
raise FetchInstanceInfoWorker::RequestError, "Request for #{domain} returned HTTP #{response.code}"
raise ActivityPub::FetchInstanceInfoWorker::RequestError, "Request for #{@instance.domain} returned HTTP #{response.code}"
end
end
end

View file

@ -7,7 +7,7 @@ class Scheduler::UpdateInstanceInfoScheduler
def perform
Instance.select(:domain).reorder(nil).find_in_batches do |instances|
FetchInstanceInfoWorker.push_bulk(instances) do |instance|
ActivityPub::FetchInstanceInfoWorker.push_bulk(instances) do |instance|
[instance.domain]
end
end

View file

@ -247,6 +247,7 @@ en:
setting_theme: Site theme
setting_trends: Show today's trends
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
setting_unsafe_limited_distribution: Send limit posts with unsafe way to other servers
setting_use_blurhash: Show colorful gradients for hidden media
setting_use_pending_items: Slow mode
severity: Severity

View file

@ -73,6 +73,7 @@ ja:
setting_reject_unlisted_subscription: Misskeyやそのフォーク(Calckeyなど)は、フォローしていないアカウントの「未収載」投稿を **購読・検索** することができます。これはkmyblueの挙動と異なります。そのようなサーバーのうち管理人が指定したものに、指定した公開範囲の投稿を「フォロワーのみ」として配送します。ただし構造上、完璧な対応は困難でたまに未収載として配信されること、ご理解ください
setting_show_application: 投稿するのに使用したアプリが投稿の詳細ビューに表示されるようになります
setting_stop_emoji_reaction_streaming: 通信容量の節約に役立ちます
setting_unsafe_limited_distribution: Mastodon 3.5、4.0、4.1のサーバーにも限定投稿(相互のみ)が届くようになりますが、安全でない方法で送信します
setting_use_blurhash: ぼかしはメディアの色を元に生成されますが、細部は見えにくくなっています
setting_use_pending_items: 新着があってもタイムラインを自動的にスクロールしないようにします
username: アルファベット大文字と小文字、数字、アンダーバー「_」が使えます
@ -255,6 +256,7 @@ ja:
setting_theme: サイトテーマ
setting_trends: 本日のトレンドタグを表示する
setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する
setting_unsafe_limited_distribution: 安全でない方法で限定投稿を他サーバーに配信する (非推奨)
setting_use_blurhash: 非表示のメディアを色付きのぼかしで表示する
setting_use_pending_items: 手動更新モード
severity: 重大性

View file

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

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddLimitedScopeToStatuses < ActiveRecord::Migration[7.0]
def change
add_column :statuses, :limited_scope, :integer
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2023_08_04_222017) do
ActiveRecord::Schema[7.0].define(version: 2023_08_12_130612) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -1029,6 +1029,14 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_04_222017) do
t.index ["var"], name: "index_site_uploads_on_var", unique: true
end
create_table "status_capability_tokens", force: :cascade do |t|
t.bigint "status_id", null: false
t.string "token"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["status_id"], name: "index_status_capability_tokens_on_status_id"
end
create_table "status_edits", force: :cascade do |t|
t.bigint "status_id", null: false
t.bigint "account_id"
@ -1114,6 +1122,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_04_222017) do
t.bigint "ordered_media_attachment_ids", array: true
t.integer "searchability"
t.boolean "markdown", default: false
t.integer "limited_scope"
t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
t.index ["account_id"], name: "index_statuses_on_account_id"
t.index ["deleted_at"], name: "index_statuses_on_deleted_at", where: "(deleted_at IS NOT NULL)"
@ -1395,6 +1404,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_04_222017) do
add_foreign_key "scheduled_statuses", "accounts", on_delete: :cascade
add_foreign_key "session_activations", "oauth_access_tokens", column: "access_token_id", name: "fk_957e5bda89", on_delete: :cascade
add_foreign_key "session_activations", "users", name: "fk_e5fda67334", on_delete: :cascade
add_foreign_key "status_capability_tokens", "statuses", on_delete: :cascade
add_foreign_key "status_edits", "accounts", on_delete: :nullify
add_foreign_key "status_edits", "statuses", on_delete: :cascade
add_foreign_key "status_pins", "accounts", name: "fk_d4cb435b62", on_delete: :cascade

View file

@ -290,6 +290,7 @@ RSpec.describe ActivityPub::Activity::Create do
expect(status).to_not be_nil
expect(status.visibility).to eq 'limited'
expect(status.limited_scope).to eq 'none'
end
it 'creates silent mention' do
@ -298,6 +299,50 @@ RSpec.describe ActivityPub::Activity::Create do
end
end
context 'when limited_scope' do
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(recipient),
limitedScope: 'Mutual',
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'limited'
expect(status.limited_scope).to eq 'mutual'
end
end
context 'when invalid limited_scope' do
let(:recipient) { Fabricate(:account) }
let(:object_json) do
{
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
type: 'Note',
content: 'Lorem ipsum',
to: ActivityPub::TagManager.instance.uri_for(recipient),
limitedScope: 'IdosdsazsF',
}
end
it 'creates status' do
status = sender.statuses.first
expect(status).to_not be_nil
expect(status.visibility).to eq 'limited'
expect(status.limited_scope).to eq 'none'
end
end
context 'when direct' do
let(:recipient) { Fabricate(:account) }

View file

@ -168,7 +168,17 @@ RSpec.describe PostStatusService, type: :service do
status = subject.call(account, text: 'test status update')
expect(ProcessMentionsService).to have_received(:new)
expect(mention_service).to have_received(:call).with(status, save_records: false)
expect(mention_service).to have_received(:call).with(status, limited_type: '', save_records: false)
end
it 'mutual visibility' do
account = Fabricate(:account)
text = 'This is an English text.'
status = subject.call(account, text: text, visibility: 'mutual')
expect(status.visibility).to eq 'limited'
expect(status.limited_scope).to eq 'mutual'
end
it 'safeguards mentions' do

View file

@ -0,0 +1,97 @@
# frozen_string_literal: true
require 'rails_helper'
describe ActivityPub::FetchInstanceInfoWorker do
subject { described_class.new }
let(:wellknown_nodeinfo) do
{
links: [
{
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
href: 'https://example.com/nodeinfo/2.0',
},
],
}
end
let(:nodeinfo) do
{
version: '2.0',
software: {
name: 'mastodon',
version: '4.2.0-beta1',
},
protocols: ['activitypub'],
}
end
let(:wellknown_nodeinfo_json) { Oj.dump(wellknown_nodeinfo) }
let(:nodeinfo_json) { Oj.dump(nodeinfo) }
context 'when success' do
before do
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 200, body: wellknown_nodeinfo_json)
stub_request(:get, 'https://example.com/nodeinfo/2.0').to_return(status: 200, body: nodeinfo_json)
Fabricate(:account, domain: 'example.com')
Instance.refresh
end
it 'performs a mastodon instance' do
subject.perform('example.com')
info = InstanceInfo.find_by(domain: 'example.com')
expect(info).to_not be_nil
expect(info.software).to eq 'mastodon'
expect(info.version).to eq '4.2.0-beta1'
end
end
context 'when update' do
let(:new_nodeinfo) do
{
version: '2.0',
software: {
name: 'mastodon',
version: '4.2.0-beta3',
},
protocols: ['activitypub'],
}
end
let(:new_nodeinfo_json) { Oj.dump(new_nodeinfo) }
before do
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 200, body: wellknown_nodeinfo_json)
Fabricate(:account, domain: 'example.com')
Instance.refresh
end
it 'performs a mastodon instance' do
stub_request(:get, 'https://example.com/nodeinfo/2.0').to_return(status: 200, body: nodeinfo_json)
subject.perform('example.com')
stub_request(:get, 'https://example.com/nodeinfo/2.0').to_return(status: 200, body: new_nodeinfo_json)
subject.perform('example.com')
info = InstanceInfo.find_by(domain: 'example.com')
expect(info).to_not be_nil
expect(info.software).to eq 'mastodon'
expect(info.version).to eq '4.2.0-beta3'
end
end
context 'when failed' do
before do
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 404)
Fabricate(:account, domain: 'example.com')
Instance.refresh
end
it 'performs a mastodon instance' do
expect { subject.perform('example.com') }.to raise_error(ActivityPub::FetchInstanceInfoWorker::RequestError, 'Request for example.com returned HTTP 404')
info = InstanceInfo.find_by(domain: 'example.com')
expect(info).to be_nil
end
end
end

View file

@ -5,6 +5,12 @@ require 'rails_helper'
describe Scheduler::UpdateInstanceInfoScheduler do
let(:worker) { described_class.new }
before do
stub_request(:get, 'https://example.com/.well-known/nodeinfo').to_return(status: 200, body: '{}')
Fabricate(:account, domain: 'example.com')
Instance.refresh
end
describe 'perform' do
it 'runs without error' do
expect { worker.perform }.to_not raise_error