Add limited_scope support

This commit is contained in:
KMY 2023-08-12 22:49:28 +09:00
parent ec16074def
commit c1f6d22ad2
17 changed files with 107 additions and 10 deletions

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

@ -70,6 +70,7 @@ const messages = defineMessages({
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}' },
});
@ -400,10 +401,11 @@ class Status extends ImmutablePureComponent {
'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 ? {} : {
@ -564,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

@ -21,6 +21,7 @@ const messages = defineMessages({
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' },
});
@ -53,10 +54,11 @@ class StatusCheckBox extends PureComponent {
'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

@ -31,6 +31,7 @@ const messages = defineMessages({
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' },
@ -251,10 +252,11 @@ class DetailedStatus extends ImmutablePureComponent {
'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

@ -28,6 +28,7 @@ const messages = defineMessages({
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' },
});
@ -96,10 +97,11 @@ class BoostModal extends ImmutablePureComponent {
'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

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

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

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,6 +148,10 @@ 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

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
@ -69,6 +69,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.visibility
end
def limited_scope
!object.none_limited? && object.limited_visibility? ? object.limited_scope : nil
end
def searchability
object.compute_searchability
end

View file

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

View file

@ -247,6 +247,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

@ -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_12_083752) 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"
@ -1122,6 +1122,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_12_083752) 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)"

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