diff --git a/app/controllers/settings/privacy_controller.rb b/app/controllers/settings/privacy_controller.rb index e5c4e5a905..1102c89fad 100644 --- a/app/controllers/settings/privacy_controller.rb +++ b/app/controllers/settings/privacy_controller.rb @@ -18,7 +18,7 @@ class Settings::PrivacyController < Settings::BaseController private def account_params - params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, :dissubscribable, settings: UserSettings.keys) + params.require(:account).permit(:discoverable, :unlocked, :indexable, :show_collections, settings: UserSettings.keys) end def set_account diff --git a/app/controllers/settings/privacy_extra_controller.rb b/app/controllers/settings/privacy_extra_controller.rb index 54cedf2c4b..85364ec35d 100644 --- a/app/controllers/settings/privacy_extra_controller.rb +++ b/app/controllers/settings/privacy_extra_controller.rb @@ -18,7 +18,7 @@ class Settings::PrivacyExtraController < Settings::BaseController private def account_params - params.require(:account).permit(settings: UserSettings.keys) + params.require(:account).permit(:dissubscribable, settings: UserSettings.keys) end def set_account diff --git a/app/helpers/kmyblue_capabilities_helper.rb b/app/helpers/kmyblue_capabilities_helper.rb index 8653a3f9da..455561a597 100644 --- a/app/helpers/kmyblue_capabilities_helper.rb +++ b/app/helpers/kmyblue_capabilities_helper.rb @@ -30,4 +30,29 @@ module KmyblueCapabilitiesHelper capabilities end + + def capabilities_for_nodeinfo + capabilities = %i( + wide_emoji + status_reference + quote + kmyblue_quote + searchability + kmyblue_searchability + visibility_mutual + visibility_limited + kmyblue_antenna + kmyblue_bookmark_category + kmyblue_searchability_limited + kmyblue_circle_history + ) + + capabilities << :full_text_search if Chewy.enabled? + if Setting.enable_emoji_reaction + capabilities << :emoji_reaction + capabilities << :enable_wide_emoji_reaction + end + + capabilities + end end diff --git a/app/lib/activitypub/activity/follow.rb b/app/lib/activitypub/activity/follow.rb index f5a0250163..fdded98440 100644 --- a/app/lib/activitypub/activity/follow.rb +++ b/app/lib/activitypub/activity/follow.rb @@ -53,7 +53,7 @@ class ActivityPub::Activity::Follow < ActivityPub::Activity friend.update!(passive_state: :pending, active_state: :idle, passive_follow_activity_id: @json['id']) else @friend = FriendDomain.new(domain: @account.domain, passive_state: :pending, passive_follow_activity_id: @json['id']) - @friend.initialize_inbox_url! + @friend.inbox_url = @json['inboxUrl'].presence || @friend.default_inbox_url @friend.save! end diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 924884d39d..13b33bcbbf 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -207,6 +207,7 @@ class FeedManager # also tagged with another followed hashtag or from a followed user scope = from_tag.statuses .where(id: timeline_status_ids) + .where.not(account: into_account) .where.not(account: into_account.following) .tagged_with_none(TagFollow.where(account: into_account).pluck(:tag_id)) diff --git a/app/lib/status_reach_finder.rb b/app/lib/status_reach_finder.rb index 5a33418fab..22b0d94ff6 100644 --- a/app/lib/status_reach_finder.rb +++ b/app/lib/status_reach_finder.rb @@ -147,7 +147,7 @@ class StatusReachFinder 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.where(delivery_local: true).pluck(:inbox_url)) + DeliveryFailureTracker.without_unavailable(FriendDomain.distributables.where(delivery_local: true).where.not(domain: AccountDomainBlock.where(account: @status.account).select(:domain)).pluck(:inbox_url)) else [] end @@ -155,7 +155,7 @@ class StatusReachFinder def nolocal_friend_inboxes if @status.public_visibility? - DeliveryFailureTracker.without_unavailable(FriendDomain.distributables.where(delivery_local: false).pluck(:inbox_url)) + DeliveryFailureTracker.without_unavailable(FriendDomain.distributables.where(delivery_local: false).where.not(domain: AccountDomainBlock.where(account: @status.account).select(:domain)).pluck(:inbox_url)) else [] end diff --git a/app/models/emoji_reaction.rb b/app/models/emoji_reaction.rb index 5d6d58513c..e9dc6a37d3 100644 --- a/app/models/emoji_reaction.rb +++ b/app/models/emoji_reaction.rb @@ -39,12 +39,12 @@ class EmojiReaction < ApplicationRecord custom_emoji.present? end - def remote_custom_emoji? - custom_emoji? && !custom_emoji.local? + def sign? + true end - def sign? - status&.distributable_friend? + def object_type + :emoji_reaction end private diff --git a/app/models/friend_domain.rb b/app/models/friend_domain.rb index 28cc59b334..0842500033 100644 --- a/app/models/friend_domain.rb +++ b/app/models/friend_domain.rb @@ -93,16 +93,12 @@ class FriendDomain < ApplicationRecord destroy! end - def initialize_inbox_url! - self.inbox_url = default_inbox_url - end - - private - def default_inbox_url "https://#{domain}/inbox" end + private + def delete_for_friend! activity_id = ActivityPub::TagManager.instance.generate_uri_for(nil) payload = Oj.dump(delete_follow_activity(activity_id)) @@ -118,6 +114,9 @@ class FriendDomain < ApplicationRecord type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(some_local_account), object: ActivityPub::TagManager::COLLECTIONS[:public], + + # Cannot use inbox_url method because this model also has inbox_url column + inboxUrl: "https://#{Rails.configuration.x.web_domain}/inbox", } end diff --git a/app/serializers/nodeinfo/serializer.rb b/app/serializers/nodeinfo/serializer.rb index 3555f0bd8d..83d63bb397 100644 --- a/app/serializers/nodeinfo/serializer.rb +++ b/app/serializers/nodeinfo/serializer.rb @@ -40,7 +40,7 @@ class NodeInfo::Serializer < ActiveModel::Serializer def metadata { - features: fedibird_capabilities, + features: capabilities_for_nodeinfo, } end diff --git a/app/services/emoji_react_service.rb b/app/services/emoji_react_service.rb index 250868b3bc..a827ddc93e 100644 --- a/app/services/emoji_react_service.rb +++ b/app/services/emoji_react_service.rb @@ -23,7 +23,10 @@ class EmojiReactService < BaseService raise Mastodon::ValidationError, I18n.t('reactions.errors.duplication') unless emoji_reaction.nil? shortcode, domain = name.split('@') + domain = nil if TagManager.instance.local_domain?(domain) custom_emoji = CustomEmoji.find_by(shortcode: shortcode, domain: domain) + return if domain.present? && !EmojiReaction.exists?(status: status, custom_emoji: custom_emoji) + emoji_reaction = EmojiReaction.create!(account: account, status: status, name: shortcode, custom_emoji: custom_emoji) status.touch # rubocop:disable Rails/SkipsModelValidations @@ -62,7 +65,6 @@ class EmojiReactService < BaseService status = emoji_reaction.status return unless status.account.local? - return if emoji_reaction.remote_custom_emoji? ActivityPub::RawDistributionWorker.perform_async(build_json(emoji_reaction), status.account_id) end diff --git a/app/views/settings/privacy/show.html.haml b/app/views/settings/privacy/show.html.haml index 619429392e..9656ada005 100644 --- a/app/views/settings/privacy/show.html.haml +++ b/app/views/settings/privacy/show.html.haml @@ -24,9 +24,6 @@ .fields-group = ff.input :noai, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_noai'), hint: I18n.t('simple_form.hints.defaults.setting_noai') - .fields-group - = f.input :dissubscribable, as: :boolean, wrapper: :with_label, kmyblue: true, hint: t('simple_form.hints.defaults.dissubscribable') - .fields-group = f.input :unlocked, as: :boolean, wrapper: :with_label diff --git a/app/views/settings/privacy_extra/show.html.haml b/app/views/settings/privacy_extra/show.html.haml index c688ad6373..5e102f6565 100644 --- a/app/views/settings/privacy_extra/show.html.haml +++ b/app/views/settings/privacy_extra/show.html.haml @@ -21,6 +21,9 @@ .fields-group = ff.input :link_preview, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_link_preview'), hint: I18n.t('simple_form.hints.defaults.setting_link_preview') + .fields-group + = f.input :dissubscribable, as: :boolean, wrapper: :with_label, kmyblue: true, hint: t('simple_form.hints.defaults.dissubscribable') + .fields-group = ff.input :allow_quote, wrapper: :with_label, kmyblue: true, label: I18n.t('simple_form.labels.defaults.setting_allow_quote'), hint: false diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index e1f1ed87dc..57f61bcacf 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -9,7 +9,7 @@ module Mastodon end def kmyblue_minor - 0 + 1 end def kmyblue_flag diff --git a/spec/lib/activitypub/activity/follow_spec.rb b/spec/lib/activitypub/activity/follow_spec.rb index c27696ebaf..53ce9a29a6 100644 --- a/spec/lib/activitypub/activity/follow_spec.rb +++ b/spec/lib/activitypub/activity/follow_spec.rb @@ -310,6 +310,7 @@ RSpec.describe ActivityPub::Activity::Follow do 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(:inbox_url) { nil } let(:json) do { @@ -318,6 +319,7 @@ RSpec.describe ActivityPub::Activity::Follow do type: 'Follow', actor: ActivityPub::TagManager.instance.uri_for(sender), object: 'https://www.w3.org/ns/activitystreams#Public', + inboxUrl: inbox_url, }.with_indifferent_access end @@ -343,6 +345,24 @@ RSpec.describe ActivityPub::Activity::Follow do end end + context 'when no record and inbox_url is specified' do + let(:inbox_url) { 'https://ohagi.com/inbox' } + + 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' + expect(friend.inbox_url).to eq 'https://ohagi.com/inbox' + end + end + context 'when my server is pending' do before do friend.update(active_state: :pending) diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index d2e5da6b92..f5a3367b2b 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -562,6 +562,44 @@ RSpec.describe FeedManager do end end + describe '#unmerge_tag_from_home' do + let(:receiver) { Fabricate(:account) } + let(:tag) { Fabricate(:tag) } + + it 'leaves a tagged status' do + status = Fabricate(:status) + status.tags << tag + described_class.instance.push_to_home(receiver, status) + + described_class.instance.unmerge_tag_from_home(tag, receiver) + + expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to_not include(status.id.to_s) + end + + it 'remains a tagged status written by receiver\'s followee' do + followee = Fabricate(:account) + receiver.follow!(followee) + + status = Fabricate(:status, account: followee) + status.tags << tag + described_class.instance.push_to_home(receiver, status) + + described_class.instance.unmerge_tag_from_home(tag, receiver) + + expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) + end + + it 'remains a tagged status written by receiver' do + status = Fabricate(:status, account: receiver) + status.tags << tag + described_class.instance.push_to_home(receiver, status) + + described_class.instance.unmerge_tag_from_home(tag, receiver) + + expect(redis.zrange("feed:home:#{receiver.id}", 0, -1)).to include(status.id.to_s) + end + end + describe '#clear_from_home' do let(:account) { Fabricate(:account) } let(:followed_account) { Fabricate(:account) } diff --git a/spec/lib/status_reach_finder_spec.rb b/spec/lib/status_reach_finder_spec.rb index 2d8e075d5b..8b333de0c4 100644 --- a/spec/lib/status_reach_finder_spec.rb +++ b/spec/lib/status_reach_finder_spec.rb @@ -239,6 +239,18 @@ describe StatusReachFinder do expect(subject.inboxes_for_friend).to_not include 'https://foo.bar/inbox' end end + + context 'when distributable but domain blocked by account' do + before do + Fabricate(:account_domain_block, account: alice, domain: 'foo.bar') + Fabricate(:friend_domain, domain: 'foo.bar', inbox_url: 'https://foo.bar/inbox', 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_not include 'https://foo.bar/inbox' + end + end end context 'when it contains distributable friend server' do diff --git a/spec/models/friend_domain_spec.rb b/spec/models/friend_domain_spec.rb index 647c39e5a8..65d090f9a7 100644 --- a/spec/models/friend_domain_spec.rb +++ b/spec/models/friend_domain_spec.rb @@ -21,6 +21,7 @@ describe FriendDomain do type: 'Follow', actor: 'https://cb6e6126.ngrok.io/actor', object: 'https://www.w3.org/ns/activitystreams#Public', + inboxUrl: 'https://cb6e6126.ngrok.io/inbox', }))).to have_been_made.once end end diff --git a/spec/serializers/nodeinfo/serializer_spec.rb b/spec/serializers/nodeinfo/serializer_spec.rb index c43b0b569f..8352a44143 100644 --- a/spec/serializers/nodeinfo/serializer_spec.rb +++ b/spec/serializers/nodeinfo/serializer_spec.rb @@ -28,5 +28,10 @@ describe NodeInfo::Serializer do # rubocop:disable RSpec/FilePath it 'returns features' do expect(serialization['metadata']['features']).to include 'emoji_reaction' end + + it 'returns nodeinfo own features' do + expect(serialization['metadata']['features']).to include 'quote' + expect(serialization['metadata']['features']).to_not include 'kmyblue_markdown' + end end end diff --git a/spec/serializers/rest/instance_serializer_spec.rb b/spec/serializers/rest/instance_serializer_spec.rb index a9a3259aa8..4b33d15daa 100644 --- a/spec/serializers/rest/instance_serializer_spec.rb +++ b/spec/serializers/rest/instance_serializer_spec.rb @@ -22,5 +22,9 @@ describe REST::InstanceSerializer do it 'returns fedibird_capabilities' do expect(serialization['fedibird_capabilities']).to include 'emoji_reaction' end + + it 'returns api own fedibird_capabilities' do + expect(serialization['fedibird_capabilities']).to include 'kmyblue_markdown' + end end end diff --git a/spec/services/emoji_react_service_spec.rb b/spec/services/emoji_react_service_spec.rb new file mode 100644 index 0000000000..629a7818d4 --- /dev/null +++ b/spec/services/emoji_react_service_spec.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe EmojiReactService, type: :service do + subject do + described_class.new.call(sender, status, name) + EmojiReaction.where(status: status, account: sender) + end + + let(:name) { '😀' } + let(:sender) { Fabricate(:user).account } + let(:author) { Fabricate(:user).account } + let(:status) { Fabricate(:status, account: author) } + + it 'with a simple case' do + expect(subject.count).to eq 1 + expect(subject.first.name).to eq '😀' + expect(subject.first.custom_emoji_id).to be_nil + end + + context 'with name duplication on same account' do + before { Fabricate(:emoji_reaction, status: status, name: '😀') } + + it 'react with emoji' do + expect(subject.count).to eq 1 + expect(subject.first.name).to eq '😀' + end + end + + context 'when multiple reactions by same account' do + let(:name) { '😂' } + + before { Fabricate(:emoji_reaction, account: sender, status: status, name: '😀') } + + it 'react with emoji' do + expect(subject.count).to eq 2 + expect(subject.pluck(:name)).to contain_exactly('😀', '😂') + end + end + + context 'when already reacted by other account' do + let(:name) { '😂' } + + before { Fabricate(:emoji_reaction, status: status, name: '😀') } + + it 'react with emoji' do + expect(subject.count).to eq 1 + expect(subject.pluck(:name)).to contain_exactly('😂') + end + end + + context 'when already reacted same emoji by other account', :tag do + before { Fabricate(:emoji_reaction, status: status, name: '😀') } + + it 'react with emoji' do + expect(subject.count).to eq 1 + expect(subject.first.name).to eq '😀' + end + end + + context 'when over limit' do + let(:name) { '🚗' } + + before do + Fabricate(:emoji_reaction, status: status, account: sender, name: '😀') + Fabricate(:emoji_reaction, status: status, account: sender, name: '😎') + Fabricate(:emoji_reaction, status: status, account: sender, name: '🐟') + end + + it 'react with emoji' do + expect { subject.count }.to raise_error Mastodon::ValidationError + + reactions = EmojiReaction.where(status: status, account: sender).pluck(:name) + expect(reactions.size).to eq 3 + expect(reactions).to contain_exactly('😀', '😎', '🐟') + end + end + + context 'with custom emoji of local' do + let(:name) { 'ohagi' } + let!(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'ohagi') } + + it 'react with emoji' do + expect(subject.count).to eq 1 + expect(subject.first.name).to eq 'ohagi' + expect(subject.first.custom_emoji.id).to eq custom_emoji.id + end + end + + context 'with custom emoji but not existing' do + let(:name) { 'ohagi' } + + it 'react with emoji' do + expect { subject.count }.to raise_error ActiveRecord::RecordInvalid + expect(EmojiReaction.exists?(status: status, account: sender, name: 'ohagi')).to be false + end + end + + context 'with custom emoji of remote' do + let(:name) { 'ohagi@foo.bar' } + let!(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'ohagi', domain: 'foo.bar', uri: 'https://foo.bar/emoji/ohagi') } + + before { Fabricate(:emoji_reaction, status: status, name: 'ohagi', custom_emoji: custom_emoji) } + + it 'react with emoji' do + expect(subject.count).to eq 1 + expect(subject.first.name).to eq 'ohagi' + expect(subject.first.custom_emoji.id).to eq custom_emoji.id + end + end + + context 'with custom emoji of remote without existing one' do + let(:name) { 'ohagi@foo.bar' } + + before { Fabricate(:custom_emoji, shortcode: 'ohagi', domain: 'foo.bar', uri: 'https://foo.bar/emoji/ohagi') } + + it 'react with emoji' do + expect(subject.count).to eq 0 + end + end + + context 'with custom emoji of remote but local has same name emoji' do + let(:name) { 'ohagi@foo.bar' } + let!(:custom_emoji) { Fabricate(:custom_emoji, shortcode: 'ohagi', domain: 'foo.bar', uri: 'https://foo.bar/emoji/ohagi') } + + before do + Fabricate(:custom_emoji, shortcode: 'ohagi', domain: nil) + Fabricate(:emoji_reaction, status: status, name: 'ohagi', custom_emoji: custom_emoji) + end + + it 'react with emoji' do + expect(subject.count).to eq 1 + expect(subject.first.name).to eq 'ohagi' + expect(subject.first.custom_emoji.id).to eq custom_emoji.id + expect(subject.first.custom_emoji.domain).to eq 'foo.bar' + end + end +end