diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml index 10e7e53458..10421eed7b 100644 --- a/.github/ISSUE_TEMPLATE/1.bug_report.yml +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -1,5 +1,5 @@ name: バグ報告 -description: kmyblueのバグ報告 +description: kmyblueのバグ報告(ただし情報改竄、秘密情報の漏洩、システムの破損などが発生するバグは、こちらではなく「Security」タブよりセキュリティインシデントとして報告してください) labels: [bug] body: - type: textarea diff --git a/SECURITY.md b/SECURITY.md index 3e13377db6..d5b27adfac 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,22 +1,25 @@ -# Security Policy +# セキュリティポリシー -If you believe you've identified a security vulnerability in Mastodon (a bug that allows something to happen that shouldn't be possible), you can either: +kmyblueのプログラムにおいてセキュリティインシデントを発見した場合、kmyblueに報告してください。 -- open a [Github security issue on the Mastodon project](https://github.com/mastodon/mastodon/security/advisories/new) -- reach us at <security@joinmastodon.org> +kmyblueにセキュリティインシデントを報告する場合、以下の手順を踏んでください。 -You should _not_ report such issues on public GitHub issues or in other public spaces to give us time to publish a fix for the issue without exposing Mastodon's users to increased risk. +- [こちらのリンクから新規インシデントを起票してください](https://github.com/kmycode/mastodon/security/advisories/new) +- メール <tt@kmycode.net>、または[@askyq@kmy.blue](https://kmy.blue/@askyq)宛に、**セキュリティインシデントを起票したことだけ**を連絡してください。セキュリティインシデントの内容は、絶対に連絡に含めないでください(リンクくらいなら含めていいかな) -## Scope +他のkmyblueフォークの利用者の安全のために少しでも時間稼ぎをしなければいけないので、この問題をIssueを含む公開された場所で記述しないでください。 -A "vulnerability in Mastodon" is a vulnerability in the code distributed through our main source code repository on GitHub. Vulnerabilities that are specific to a given installation (e.g. misconfiguration) should be reported to the owner of that installation and not us. +## 範囲 -## Supported Versions +こちらが対応できる範囲は、当リポジトリで公開しているソースコードのみとなります。当リポジトリの依存パッケージ内に問題がある場合は、そちらに報告してください。 -| Version | Supported | -| ------- | ---------------- | -| 4.2.x | Yes | -| 4.1.x | Yes | -| 4.0.x | Until 2023-10-31 | -| 3.5.x | Until 2023-12-31 | -| < 3.5 | No | +もしあなたに専門知識があり、それが本家Mastodon由来の問題であると信じるに足る根拠がある場合、kmyblueではなくMastodonのほうに報告してください。kmyblueに報告されても、Mastodonより先に修正してしまうことでMastodonにセキュリティリスクを発生させる可能性がありますし、本家Mastodonの対応を待つにしてもkmyblueのほうに来てしまったセキュリティインシデントの対応に困ります(本家がなかなか対応してくれない可能性を考えると削除しづらい)。もし間違ってkmyblueに来た場合、kmyblue開発者の責任で振り分けを行います。 + +## サポートするバージョン + +下記以外のバージョンは、セキュリティインシデントを起票されても対応しません。 + +- 最新メジャーバージョン、かつ、最新マイナーバージョン + - 最新メジャーバージョンのサポートは、次のメジャーバージョンが出た時点で終了します +- LTS + - LTSのサポートは、次のLTSが出た時点で終了します(ただし移行期間があってもいいと思ってるので、1〜3ヶ月以内ならセキュリティインシデントの程度に応じて対応する可能性があります) diff --git a/app/helpers/context_helper.rb b/app/helpers/context_helper.rb index 69a8767f0f..ab0d44ca69 100644 --- a/app/helpers/context_helper.rb +++ b/app/helpers/context_helper.rb @@ -30,6 +30,8 @@ module ContextHelper other_setting: { 'fedibird' => 'http://fedibird.com/ns#', 'otherSetting' => 'fedibird:otherSetting' }, references: { 'fedibird' => 'http://fedibird.com/ns#', 'references' => { '@id' => 'fedibird:references', '@type' => '@id' } }, quote_uri: { 'fedibird' => 'http://fedibird.com/ns#', 'quoteUri' => 'fedibird:quoteUri' }, + keywords: { 'schema' => 'http://schema.org#', 'keywords' => 'schema:keywords' }, + license: { 'schema' => 'http://schema.org#', 'license' => 'schema:license' }, olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index fe37c3234b..204899fa39 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -274,6 +274,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity emoji.image_remote_url = custom_emoji_parser.image_remote_url emoji.license = custom_emoji_parser.license emoji.is_sensitive = custom_emoji_parser.is_sensitive + emoji.aliases = custom_emoji_parser.aliases emoji.save rescue Seahorse::Client::NetworkingError => e Rails.logger.warn "Error storing emoji: #{e}" diff --git a/app/lib/activitypub/activity/like.rb b/app/lib/activitypub/activity/like.rb index ba103b1fa7..8be8345613 100644 --- a/app/lib/activitypub/activity/like.rb +++ b/app/lib/activitypub/activity/like.rb @@ -127,6 +127,7 @@ class ActivityPub::Activity::Like < ActivityPub::Activity emoji.image_remote_url = custom_emoji_parser.image_remote_url emoji.license = custom_emoji_parser.license emoji.is_sensitive = custom_emoji_parser.is_sensitive + emoji.aliases = custom_emoji_parser.aliases emoji.save rescue Seahorse::Client::NetworkingError => e Rails.logger.warn "Error storing emoji: #{e}" diff --git a/app/lib/activitypub/parser/custom_emoji_parser.rb b/app/lib/activitypub/parser/custom_emoji_parser.rb index e217b5ec96..e2c48b0a90 100644 --- a/app/lib/activitypub/parser/custom_emoji_parser.rb +++ b/app/lib/activitypub/parser/custom_emoji_parser.rb @@ -15,6 +15,10 @@ class ActivityPub::Parser::CustomEmojiParser @json['name']&.delete(':') end + def aliases + as_array(@json['keywords']) + end + def image_remote_url @json.dig('icon', 'url') end diff --git a/app/models/account.rb b/app/models/account.rb index 3b3dc09f0e..7170cbe1e2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -399,6 +399,7 @@ class Account < ApplicationRecord def allow_emoji_reaction?(account) return false if account.nil? + return true unless local? || account.local? show_emoji_reaction?(account) end diff --git a/app/serializers/activitypub/emoji_serializer.rb b/app/serializers/activitypub/emoji_serializer.rb index 98525d3131..a8dfff58f6 100644 --- a/app/serializers/activitypub/emoji_serializer.rb +++ b/app/serializers/activitypub/emoji_serializer.rb @@ -3,9 +3,9 @@ class ActivityPub::EmojiSerializer < ActivityPub::Serializer include RoutingHelper - context_extensions :emoji + context_extensions :emoji, :license, :keywords - attributes :id, :type, :domain, :name, :is_sensitive, :updated + attributes :id, :type, :domain, :name, :keywords, :is_sensitive, :updated attribute :license, if: -> { object.license.present? } @@ -23,6 +23,10 @@ class ActivityPub::EmojiSerializer < ActivityPub::Serializer object.domain.presence || Rails.configuration.x.local_domain end + def keywords + object.aliases + end + def icon object.image end diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index 1ea2e93e03..aa8fc41889 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -5,6 +5,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService include Redisable include Lockable + class AbortError < ::StandardError; end + def call(status, activity_json, object_json, request_id: nil) raise ArgumentError, 'Status has unsaved changes' if status.changed? @@ -30,6 +32,9 @@ class ActivityPub::ProcessStatusUpdateService < BaseService handle_implicit_update! end + @status + rescue AbortError + @status.reload @status end @@ -46,6 +51,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService update_poll! update_immediate_attributes! update_metadata! + validate_status_mentions! create_edits! end @@ -158,6 +164,15 @@ class ActivityPub::ProcessStatusUpdateService < BaseService !Admin::NgWord.reject?("#{@status_parser.spoiler_text}\n#{@status_parser.text}") && !Admin::NgWord.hashtag_reject?(@raw_tags.size) end + def validate_status_mentions! + raise AbortError if mention_to_stranger? && Admin::NgWord.stranger_mention_reject?("#{@status.spoiler_text}\n#{@status.text}") + end + + def mention_to_stranger? + @status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @status.account.id && !mentioned_account.following?(@status.account) } || + (@status.thread.present? && @status.thread.account.id != @status.account.id && !@status.thread.account.following?(@status.account)) + end + def update_immediate_attributes! @status.text = @status_parser.text || '' @status.spoiler_text = @status_parser.spoiler_text || '' @@ -247,6 +262,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService emoji.image_remote_url = custom_emoji_parser.image_remote_url emoji.license = custom_emoji_parser.license emoji.is_sensitive = custom_emoji_parser.is_sensitive + emoji.aliases = custom_emoji_parser.aliases emoji.save rescue Seahorse::Client::NetworkingError => e Rails.logger.warn "Error storing emoji: #{e}" diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb index 73c397852e..b0bab9a081 100644 --- a/app/services/concerns/payloadable.rb +++ b/app/services/concerns/payloadable.rb @@ -16,8 +16,9 @@ module Payloadable always_sign = options.delete(:always_sign) payload = ActiveModelSerializers::SerializableResource.new(record, options.merge(serializer: serializer, adapter: ActivityPub::Adapter)).as_json object = record.respond_to?(:virtual_object) ? record.virtual_object : record + bearcap = object.is_a?(String) && record.respond_to?(:type) && (record.type == 'Create' || record.type == 'Update') - if ((object.respond_to?(:sign?) && object.sign?) && signer && (always_sign || signing_enabled?)) || object.is_a?(String) + if ((object.respond_to?(:sign?) && object.sign?) && signer && (always_sign || signing_enabled?)) || bearcap ActivityPub::LinkedDataSignature.new(payload).sign!(signer, sign_with: sign_with) else payload diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 5410323dbb..0e3bd8805d 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -214,7 +214,7 @@ class PostStatusService < BaseService end def mention_to_stranger? - @status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @account && !mentioned_account.following?(@account) } || + @status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @account.id && !mentioned_account.following?(@account) } || (@in_reply_to && @in_reply_to.account.id != @account.id && !@in_reply_to.account.following?(@account)) end diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 0a5de6b907..a9733b0658 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -35,10 +35,13 @@ class UpdateStatusService < BaseService update_poll! if @options.key?(:poll) update_immediate_attributes! create_edit! unless @options[:no_history] + + reset_preview_card! + process_mentions_service.call(@status) + validate_status_mentions! end queue_poll_notifications! - reset_preview_card! update_metadata! update_references! broadcast_updates! @@ -81,6 +84,15 @@ class UpdateStatusService < BaseService raise Mastodon::ValidationError, I18n.t('statuses.too_many_hashtags') if Admin::NgWord.hashtag_reject_with_extractor?(@options[:text]) end + def validate_status_mentions! + raise Mastodon::ValidationError, I18n.t('statuses.contains_ng_words') if mention_to_stranger? && Setting.stranger_mention_from_local_ng && Admin::NgWord.stranger_mention_reject?("#{@options[:spoiler_text]}\n#{@options[:text]}") + end + + def mention_to_stranger? + @status.mentions.map(&:account).to_a.any? { |mentioned_account| mentioned_account.id != @status.account.id && !mentioned_account.following?(@status.account) } || + (@status.thread.present? && @status.thread.account.id != @status.account.id && !@status.thread.account.following?(@status.account)) + end + def validate_media! return [] if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable) @@ -167,7 +179,6 @@ class UpdateStatusService < BaseService def update_metadata! ProcessHashtagsService.new.call(@status) - process_mentions_service.call(@status) @status.update(limited_scope: :circle) if process_mentions_service.mentions? end diff --git a/app/views/admin/domain_blocks/_domain_block_list.html.haml b/app/views/admin/domain_blocks/_domain_block_list.html.haml new file mode 100644 index 0000000000..d16c639dd0 --- /dev/null +++ b/app/views/admin/domain_blocks/_domain_block_list.html.haml @@ -0,0 +1,44 @@ +%h4= I18n.t('admin.domain_blocks.headers.harassment') + +.fields-group + = f.input :reject_favourite, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_favourite'), hint: I18n.t('admin.domain_blocks.reject_favourite_hint') + +.fields-group + = f.input :reject_reply, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply'), hint: I18n.t('admin.domain_blocks.reject_reply_hint') + +.fields-group + = f.input :reject_reply_exclude_followers, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply_exclude_followers'), hint: I18n.t('admin.domain_blocks.reject_reply_exclude_followers_hint') + +.fields-group + = f.input :reject_hashtag, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_hashtag'), hint: I18n.t('admin.domain_blocks.reject_hashtag_hint') + +.fields-group + = f.input :reject_straight_follow, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_straight_follow'), hint: I18n.t('admin.domain_blocks.reject_straight_follow_hint') + +.fields-group + = f.input :reject_new_follow, as: :boolean, kmyblue: true, 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, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_friend'), hint: I18n.t('admin.domain_blocks.reject_friend_hint') + +%h4= I18n.t('admin.domain_blocks.headers.invalid_privacy') + +.fields-group + = f.input :reject_send_not_public_searchability, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_not_public_searchability'), hint: I18n.t('admin.domain_blocks.reject_send_not_public_searchability_hint') + +.fields-group + = f.input :reject_send_dissubscribable, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_dissubscribable'), hint: I18n.t('admin.domain_blocks.reject_send_dissubscribable_hint') + +.fields-group + = f.input :detect_invalid_subscription, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.detect_invalid_subscription'), hint: I18n.t('admin.domain_blocks.detect_invalid_subscription_hint') + +%h4= I18n.t('admin.domain_blocks.headers.disagreement') + +.fields-group + = f.input :reject_send_public_unlisted, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_public_unlisted'), hint: I18n.t('admin.domain_blocks.reject_send_public_unlisted_hint') + +.fields-group + = f.input :reject_send_media, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_media'), hint: I18n.t('admin.domain_blocks.reject_send_media_hint') + +.fields-group + = f.input :reject_send_sensitive, as: :boolean, kmyblue: true, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_sensitive'), hint: I18n.t('admin.domain_blocks.reject_send_sensitive_hint') diff --git a/app/views/admin/domain_blocks/edit.html.haml b/app/views/admin/domain_blocks/edit.html.haml index cf83d383e9..7454d81539 100644 --- a/app/views/admin/domain_blocks/edit.html.haml +++ b/app/views/admin/domain_blocks/edit.html.haml @@ -11,48 +11,13 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: ->(type) { t("admin.domain_blocks.new.severity.#{type}") }, hint: t('admin.domain_blocks.new.severity.desc_html') + = render 'domain_block_list', f: f + + %h4= I18n.t('admin.domain_blocks.headers.mastodon_default') + .fields-group = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint') - .fields-group - = f.input :reject_favourite, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_favourite'), hint: I18n.t('admin.domain_blocks.reject_favourite_hint') - - .fields-group - = f.input :reject_reply, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply'), hint: I18n.t('admin.domain_blocks.reject_reply_hint') - - .fields-group - = f.input :reject_reply_exclude_followers, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply_exclude_followers'), hint: I18n.t('admin.domain_blocks.reject_reply_exclude_followers_hint') - - .fields-group - = f.input :reject_send_not_public_searchability, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_not_public_searchability'), hint: I18n.t('admin.domain_blocks.reject_send_not_public_searchability_hint') - - .fields-group - = f.input :reject_send_dissubscribable, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_dissubscribable'), hint: I18n.t('admin.domain_blocks.reject_send_dissubscribable_hint') - - .fields-group - = f.input :reject_send_public_unlisted, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_public_unlisted'), hint: I18n.t('admin.domain_blocks.reject_send_public_unlisted_hint') - - .fields-group - = f.input :reject_send_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_media'), hint: I18n.t('admin.domain_blocks.reject_send_media_hint') - - .fields-group - = f.input :reject_send_sensitive, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_sensitive'), hint: I18n.t('admin.domain_blocks.reject_send_sensitive_hint') - - .fields-group - = f.input :reject_hashtag, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_hashtag'), hint: I18n.t('admin.domain_blocks.reject_hashtag_hint') - - .fields-group - = f.input :reject_straight_follow, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_straight_follow'), hint: I18n.t('admin.domain_blocks.reject_straight_follow_hint') - - .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') - .fields-group = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint') @@ -69,7 +34,7 @@ = f.input :hidden, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden'), hint: I18n.t('admin.domain_blocks.hidden_hint') .fields-group - = f.input :hidden_anonymous, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden_anonymous'), hint: I18n.t('admin.domain_blocks.hidden_anonymous_hint') + = f.input :hidden_anonymous, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden_anonymous'), hint: I18n.t('admin.domain_blocks.hidden_anonymous_hint') .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/app/views/admin/domain_blocks/new.html.haml b/app/views/admin/domain_blocks/new.html.haml index ed5142934f..ad25fcebbd 100644 --- a/app/views/admin/domain_blocks/new.html.haml +++ b/app/views/admin/domain_blocks/new.html.haml @@ -11,48 +11,13 @@ .fields-row__column.fields-row__column-6.fields-group = f.input :severity, collection: DomainBlock.severities.keys, wrapper: :with_label, include_blank: false, label_method: ->(type) { t(".severity.#{type}") }, hint: t('.severity.desc_html') + = render 'domain_block_list', f: f + + %h4= I18n.t('admin.domain_blocks.headers.mastodon_default') + .fields-group = f.input :reject_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_media'), hint: I18n.t('admin.domain_blocks.reject_media_hint') - .fields-group - = f.input :reject_favourite, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_favourite'), hint: I18n.t('admin.domain_blocks.reject_favourite_hint') - - .fields-group - = f.input :reject_reply, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply'), hint: I18n.t('admin.domain_blocks.reject_reply_hint') - - .fields-group - = f.input :reject_reply_exclude_followers, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reply_exclude_followers'), hint: I18n.t('admin.domain_blocks.reject_reply_exclude_followers_hint') - - .fields-group - = f.input :reject_send_not_public_searchability, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_not_public_searchability'), hint: I18n.t('admin.domain_blocks.reject_send_not_public_searchability_hint') - - .fields-group - = f.input :reject_send_dissubscribable, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_dissubscribable'), hint: I18n.t('admin.domain_blocks.reject_send_dissubscribable_hint') - - .fields-group - = f.input :reject_send_public_unlisted, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_public_unlisted'), hint: I18n.t('admin.domain_blocks.reject_send_public_unlisted_hint') - - .fields-group - = f.input :reject_send_media, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_media'), hint: I18n.t('admin.domain_blocks.reject_send_media_hint') - - .fields-group - = f.input :reject_send_sensitive, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_send_sensitive'), hint: I18n.t('admin.domain_blocks.reject_send_sensitive_hint') - - .fields-group - = f.input :reject_hashtag, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_hashtag'), hint: I18n.t('admin.domain_blocks.reject_hashtag_hint') - - .fields-group - = f.input :reject_straight_follow, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_straight_follow'), hint: I18n.t('admin.domain_blocks.reject_straight_follow_hint') - - .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') - .fields-group = f.input :reject_reports, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.reject_reports'), hint: I18n.t('admin.domain_blocks.reject_reports_hint') @@ -69,7 +34,7 @@ = f.input :hidden, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden'), hint: I18n.t('admin.domain_blocks.hidden_hint') .fields-group - = f.input :hidden_anonymous, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden_anonymous'), hint: I18n.t('admin.domain_blocks.hidden_anonymous_hint') + = f.input :hidden_anonymous, kmyblue: true, as: :boolean, wrapper: :with_label, label: I18n.t('admin.domain_blocks.hidden_anonymous'), hint: I18n.t('admin.domain_blocks.hidden_anonymous_hint') .actions = f.button :button, t('.create'), type: :submit diff --git a/app/workers/activitypub/status_update_distribution_worker.rb b/app/workers/activitypub/status_update_distribution_worker.rb index a79ede2bf6..a7081bed8e 100644 --- a/app/workers/activitypub/status_update_distribution_worker.rb +++ b/app/workers/activitypub/status_update_distribution_worker.rb @@ -15,15 +15,27 @@ class ActivityPub::StatusUpdateDistributionWorker < ActivityPub::DistributionWor protected - def activity + def build_activity(for_misskey: false, for_friend: false) ActivityPub::ActivityPresenter.new( id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @status.edited_at.to_i].join, type: 'Update', actor: ActivityPub::TagManager.instance.uri_for(@status.account), published: @status.edited_at, - to: ActivityPub::TagManager.instance.to(@status), - cc: ActivityPub::TagManager.instance.cc(@status), + to: for_friend ? ActivityPub::TagManager.instance.to_for_friend(@status) : ActivityPub::TagManager.instance.to(@status), + cc: for_misskey ? ActivityPub::TagManager.instance.cc_for_misskey : ActivityPub::TagManager.instance.cc(@status), virtual_object: @status ) end + + def activity + build_activity + end + + def activity_for_misskey + build_activity(for_misskey: true) + end + + def activity_for_friend + build_activity(for_friend: true) + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 7475451128..85db9c5a66 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -415,6 +415,11 @@ en: existing_domain_block: You have already imposed stricter limits on %{name}. existing_domain_block_html: You have already imposed stricter limits on %{name}, you need to <a href="%{unblock_url}">unblock it</a> first. export: Export + headers: + disagreement: Protect sensitive posts from political disagreement + harassment: Harassment or spam + invalid_privacy: Privacy is not protected + mastodon_default: Original Mastodon supports import: Import new: create: Create block diff --git a/config/locales/ja.yml b/config/locales/ja.yml index e0539d2150..a5fbf480a1 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -402,12 +402,17 @@ ja: created_msg: ドメインブロック処理を完了しました destroyed_msg: ドメインブロックを外しました detect_invalid_subscription: 不正な購読を行うサーバーとしてマークする - detect_invalid_subscription_hint: Misskey、Calckeyなどは購読機能で未フォローユーザーの未収載投稿を拾います。これをマークしたサーバーは、ユーザーが任意で配送を拒否できます。停止とは無関係です + detect_invalid_subscription_hint: 未フォローユーザーの未収載投稿が自由に購読・検索できるサーバーとしてマークします。Misskeyサーバーはこのチェックに関係なく、自動で制限が有効とみなされます。各ユーザーはそれぞれのプライバシー追加設定でMisskeyサーバーへの配送制限を有効にすることで、これらのサーバーへローカル公開・未収載投稿を鍵付きで配信できるようになります。停止とは無関係です domain: ドメイン edit: ドメインブロックを編集 existing_domain_block: あなたは既に%{name}さんに厳しい制限を課しています。 existing_domain_block_html: 既に%{name}に対して、より厳しい制限を課しています。先に<a href="%{unblock_url}">その制限を解除</a>する必要があります。 export: エクスポート + headers: + disagreement: 政治的な意見の相違からの敏感な投稿の保護 + harassment: 嫌がらせまたはスパム + invalid_privacy: プライバシーが守られていない + mastodon_default: 本家Mastodonの設定項目 hidden: 非公開にする hidden_hint: 公開することで当サーバーの安全が脅かされる場合、このドメインブロックを非公開にすることができます。 hidden_anonymous: 未ログインユーザーに非公開にする diff --git a/db/post_migrate/20231021005339_remove_remote_uri_from_local_custom_emojis.rb b/db/post_migrate/20231021005339_remove_remote_uri_from_local_custom_emojis.rb new file mode 100644 index 0000000000..d541de7429 --- /dev/null +++ b/db/post_migrate/20231021005339_remove_remote_uri_from_local_custom_emojis.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require Rails.root.join('lib', 'mastodon', 'migration_helpers') + +class RemoveRemoteUriFromLocalCustomEmojis < ActiveRecord::Migration[7.0] + include Mastodon::MigrationHelpers + + disable_ddl_transaction! + + class CustomEmoji < ApplicationRecord; end + + def up + safety_assured do + CustomEmoji.transaction do + CustomEmoji.where(domain: nil).update_all(image_remote_url: nil, uri: nil) # rubocop:disable Rails/SkipsModelValidations + end + end + end + + def down; end +end diff --git a/db/schema.rb b/db/schema.rb index 7a4ee11608..af31ed869b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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_10_09_235215) do +ActiveRecord::Schema[7.0].define(version: 2023_10_21_005339) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 96f2fb514c..68f6ec485f 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -246,6 +246,207 @@ RSpec.describe Account do end end + describe '#allow_emoji_reaction?' do + let(:policy) { :allow } + let(:reactioned) { Fabricate(:user, settings: { emoji_reaction_policy: policy }).account } + let(:followee) { Fabricate(:account) } + let(:follower) { Fabricate(:account) } + let(:mutual) { Fabricate(:account) } + let(:anyone) { Fabricate(:account) } + + before do + follower.follow!(reactioned) + reactioned.follow!(followee) + mutual.follow!(reactioned) + reactioned.follow!(mutual) + end + + context 'when policy is arrow' do + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be true + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be true + end + + it 'allows follower' do + expect(reactioned.allow_emoji_reaction?(follower)).to be true + end + + it 'allows mutual' do + expect(reactioned.allow_emoji_reaction?(mutual)).to be true + end + + it 'allows self' do + expect(reactioned.allow_emoji_reaction?(reactioned)).to be true + end + end + + context 'when policy is following_only' do + let(:policy) { :following_only } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be false + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be true + end + + it 'allows follower' do + expect(reactioned.allow_emoji_reaction?(follower)).to be false + end + + it 'allows mutual' do + expect(reactioned.allow_emoji_reaction?(mutual)).to be true + end + + it 'allows self' do + expect(reactioned.allow_emoji_reaction?(reactioned)).to be true + end + end + + context 'when policy is followers_only' do + let(:policy) { :followers_only } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be false + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be false + end + + it 'allows follower' do + expect(reactioned.allow_emoji_reaction?(follower)).to be true + end + + it 'allows mutual' do + expect(reactioned.allow_emoji_reaction?(mutual)).to be true + end + + it 'allows self' do + expect(reactioned.allow_emoji_reaction?(reactioned)).to be true + end + end + + context 'when policy is mutuals_only' do + let(:policy) { :mutuals_only } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be false + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be false + end + + it 'allows follower' do + expect(reactioned.allow_emoji_reaction?(follower)).to be false + end + + it 'allows mutual' do + expect(reactioned.allow_emoji_reaction?(mutual)).to be true + end + + it 'allows self' do + expect(reactioned.allow_emoji_reaction?(reactioned)).to be true + end + end + + context 'when policy is outside_only' do + let(:policy) { :outside_only } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be false + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be true + end + + it 'allows follower' do + expect(reactioned.allow_emoji_reaction?(follower)).to be true + end + + it 'allows mutual' do + expect(reactioned.allow_emoji_reaction?(mutual)).to be true + end + + it 'allows self' do + expect(reactioned.allow_emoji_reaction?(reactioned)).to be true + end + end + + context 'when policy is block' do + let(:policy) { :block } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be false + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be false + end + + it 'allows follower' do + expect(reactioned.allow_emoji_reaction?(follower)).to be false + end + + it 'allows mutual' do + expect(reactioned.allow_emoji_reaction?(mutual)).to be false + end + + it 'allows self' do + expect(reactioned.allow_emoji_reaction?(reactioned)).to be false + end + end + + context 'when reactioned is remote user' do + let(:reactioned) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/actor', settings: { emoji_reaction_policy: :following_only }) } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be false + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be true + end + end + + context 'when reactor is remote user' do + let(:anyone) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/actor/anyone') } + let(:policy) { :following_only } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be false + end + + it 'allows followee' do + expect(reactioned.allow_emoji_reaction?(followee)).to be true + end + end + + context 'when both are remote user' do + let(:reactioned) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/actor', settings: { emoji_reaction_policy: policy }) } + let(:anyone) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/actor/anyone') } + let(:followee) { Fabricate(:account, domain: 'foo.bar', uri: 'https://foo.bar/actor/followee') } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be true + end + + context 'with blocking' do + let(:policy) { :block } + + it 'allows anyone' do + expect(reactioned.allow_emoji_reaction?(anyone)).to be true + end + end + end + end + describe '#favourited?' do subject { Fabricate(:account) } diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 9d91f31cc5..0612c94d8c 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -9,19 +9,24 @@ end RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do subject { described_class.new } - let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) } + let(:thread) { nil } + let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com'), thread: thread) } + let(:json_tags) do + [ + { type: 'Hashtag', name: 'hoge' }, + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + ] + end + let(:content) { 'Hello universe' } let(:payload) do { '@context': 'https://www.w3.org/ns/activitystreams', id: 'foo', type: 'Note', summary: 'Show more', - content: 'Hello universe', + content: content, updated: '2021-09-08T22:39:25Z', - tag: [ - { type: 'Hashtag', name: 'hoge' }, - { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, - ], + tag: json_tags, } end let(:json) { Oj.load(Oj.dump(payload)) } @@ -462,5 +467,161 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService, type: :service do subject.call(status, json, json) expect(status.reload.edited_at.to_s).to eq '2021-09-08 22:39:25 UTC' end + + describe 'ng word is set' do + let(:json_tags) { [] } + + context 'when hit ng words' do + let(:content) { 'ng word test' } + + it 'update status' do + Form::AdminSettings.new(ng_words: 'test').save + + subject.call(status, json, json) + expect(status.reload.text).to_not eq content + end + end + + context 'when not hit ng words' do + let(:content) { 'ng word aiueo' } + + it 'update status' do + Form::AdminSettings.new(ng_words: 'test').save + + subject.call(status, json, json) + expect(status.reload.text).to eq content + end + end + + context 'when hit ng words for mention' do + let(:json_tags) do + [ + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + ] + end + let(:content) { 'ng word test' } + + it 'update status' do + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save + + subject.call(status, json, json) + expect(status.reload.text).to_not eq content + expect(status.mentioned_accounts.pluck(:id)).to_not include alice.id + end + end + + context 'when hit ng words for mention but local posts are not checked' do + let(:json_tags) do + [ + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + ] + end + let(:content) { 'ng word test' } + + it 'update status' do + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '0').save + + subject.call(status, json, json) + expect(status.reload.text).to_not eq content + expect(status.mentioned_accounts.pluck(:id)).to_not include alice.id + end + end + + context 'when hit ng words for mention to follower' do + let(:json_tags) do + [ + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + ] + end + let(:content) { 'ng word test' } + + before do + alice.follow!(status.account) + end + + it 'update status' do + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test').save + + subject.call(status, json, json) + expect(status.reload.text).to eq content + expect(status.mentioned_accounts.pluck(:id)).to include alice.id + end + end + + context 'when hit ng words for reply' do + let(:json_tags) do + [ + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + ] + end + let(:content) { 'ng word test' } + let(:thread) { Fabricate(:status, account: alice) } + + it 'update status' do + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test').save + + subject.call(status, json, json) + expect(status.reload.text).to_not eq content + expect(status.mentioned_accounts.pluck(:id)).to_not include alice.id + end + end + + context 'when hit ng words for reply to follower' do + let(:json_tags) do + [ + { type: 'Mention', href: ActivityPub::TagManager.instance.uri_for(alice) }, + ] + end + let(:content) { 'ng word test' } + let(:thread) { Fabricate(:status, account: alice) } + + before do + alice.follow!(status.account) + end + + it 'update status' do + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test').save + + subject.call(status, json, json) + expect(status.reload.text).to eq content + expect(status.mentioned_accounts.pluck(:id)).to include alice.id + end + end + + context 'when using hashtag under limit' do + let(:json_tags) do + [ + { type: 'Hashtag', name: 'a' }, + { type: 'Hashtag', name: 'b' }, + ] + end + let(:content) { 'ohagi is good' } + + it 'update status' do + Form::AdminSettings.new(post_hash_tags_max: 2).save + + subject.call(status, json, json) + expect(status.reload.text).to eq content + end + end + + context 'when using hashtag over limit' do + let(:json_tags) do + [ + { type: 'Hashtag', name: 'a' }, + { type: 'Hashtag', name: 'b' }, + { type: 'Hashtag', name: 'c' }, + ] + end + let(:content) { 'ohagi is good' } + + it 'update status' do + Form::AdminSettings.new(post_hash_tags_max: 2).save + + subject.call(status, json, json) + expect(status.reload.text).to_not eq content + end + end + end end end diff --git a/spec/services/backup_service_spec.rb b/spec/services/backup_service_spec.rb index 806ba18323..f5ad319136 100644 --- a/spec/services/backup_service_spec.rb +++ b/spec/services/backup_service_spec.rb @@ -9,6 +9,8 @@ RSpec.describe BackupService, type: :service do let!(:attachment) { Fabricate(:media_attachment, account: user.account) } let!(:status) { Fabricate(:status, account: user.account, text: 'Hello', visibility: :public, media_attachments: [attachment]) } let!(:private_status) { Fabricate(:status, account: user.account, text: 'secret', visibility: :private) } + let!(:limited_status) { Fabricate(:status, account: user.account, text: 'sec mutual', visibility: :limited, limited_scope: :mutual) } + let!(:reblog_status) { Fabricate(:status, account: user.account, reblog_of_id: Fabricate(:status).id) } let!(:favourite) { Fabricate(:favourite, account: user.account) } let!(:bookmark) { Fabricate(:bookmark, account: user.account) } let!(:backup) { Fabricate(:backup, user: user) } @@ -60,10 +62,12 @@ RSpec.describe BackupService, type: :service do aggregate_failures do expect(json['@context']).to_not be_nil expect(json['type']).to eq 'OrderedCollection' - expect(json['totalItems']).to eq 2 + expect(json['totalItems']).to eq 4 expect(json['orderedItems'][0]['@context']).to be_nil expect(json['orderedItems'][0]).to include_create_item(status) expect(json['orderedItems'][1]).to include_create_item(private_status) + expect(json['orderedItems'][2]).to include_create_item(limited_status) + expect(json['orderedItems'][3]).to include_announce_item(reblog_status) end end @@ -98,4 +102,11 @@ RSpec.describe BackupService, type: :service do }), }) end + + def include_announce_item(status) + include({ + 'type' => 'Announce', + 'object' => ActivityPub::TagManager.instance.uri_for(status.reblog), + }) + end end diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index f122b5d236..e27baf13a9 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -510,6 +510,26 @@ RSpec.describe PostStatusService, type: :service do expect(status).to be_persisted expect(status.text).to eq text end + + it 'using hashtag under limit' do + account = Fabricate(:account) + text = '#a #b' + Form::AdminSettings.new(post_hash_tags_max: 2).save + + status = subject.call(account, text: text) + + expect(status).to be_persisted + expect(status.tags.count).to eq 2 + expect(status.text).to eq text + end + + it 'using hashtag over limit' do + account = Fabricate(:account) + text = '#a #b #c' + Form::AdminSettings.new(post_hash_tags_max: 2).save + + expect { subject.call(account, text: text) }.to raise_error Mastodon::ValidationError + end end def create_status_with_options(**options) diff --git a/spec/services/update_status_service_spec.rb b/spec/services/update_status_service_spec.rb index 288466bdeb..d2f3d42a0f 100644 --- a/spec/services/update_status_service_spec.rb +++ b/spec/services/update_status_service_spec.rb @@ -218,4 +218,103 @@ RSpec.describe UpdateStatusService, type: :service do subject.call(status, status.account_id, text: 'Bar') expect(ActivityPub::DistributionWorker).to have_received(:perform_async) end + + describe 'ng word is set' do + let(:account) { Fabricate(:account) } + let(:status) { PostStatusService.new.call(account, text: 'ohagi') } + + it 'hit ng words' do + text = 'ng word test' + Form::AdminSettings.new(ng_words: 'test').save + + expect { subject.call(status, status.account_id, text: text) }.to raise_error(Mastodon::ValidationError) + end + + it 'not hit ng words' do + text = 'ng word aiueo' + Form::AdminSettings.new(ng_words: 'test').save + + status2 = subject.call(status, status.account_id, text: text) + + expect(status2).to be_persisted + expect(status2.text).to eq text + end + + it 'hit ng words for mention' do + Fabricate(:account, username: 'ohagi', domain: nil) + text = 'ng word test @ohagi' + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save + + expect { subject.call(status, status.account_id, text: text) }.to raise_error(Mastodon::ValidationError) + expect(status.reload.text).to_not eq text + expect(status.mentioned_accounts.pluck(:username)).to_not include 'ohagi' + end + + it 'hit ng words for mention but local posts are not checked' do + Fabricate(:account, username: 'ohagi', domain: nil) + text = 'ng word test @ohagi' + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '0').save + + status2 = subject.call(status, status.account_id, text: text) + + expect(status2).to be_persisted + expect(status2.text).to eq text + end + + it 'hit ng words for mention to follower' do + mentioned = Fabricate(:account, username: 'ohagi', domain: nil) + mentioned.follow!(account) + text = 'ng word test @ohagi' + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save + + status2 = subject.call(status, status.account_id, text: text) + + expect(status2).to be_persisted + expect(status2.text).to eq text + end + + it 'hit ng words for reply' do + text = 'ng word test' + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save + + status = PostStatusService.new.call(account, text: 'hello', thread: Fabricate(:status)) + + expect { subject.call(status, status.account_id, text: text) }.to raise_error(Mastodon::ValidationError) + expect(status.reload.text).to_not eq text + end + + it 'hit ng words for reply to follower' do + mentioned = Fabricate(:account, username: 'ohagi', domain: nil) + mentioned.follow!(account) + text = 'ng word test' + Form::AdminSettings.new(ng_words_for_stranger_mention: 'test', stranger_mention_from_local_ng: '1').save + + status = PostStatusService.new.call(account, text: 'hello', thread: Fabricate(:status, account: mentioned)) + + status = subject.call(status, status.account_id, text: text) + + expect(status).to be_persisted + expect(status.text).to eq text + end + + it 'using hashtag under limit' do + text = '#a #b' + Form::AdminSettings.new(post_hash_tags_max: 2).save + + subject.call(status, status.account_id, text: text) + + expect(status.reload.tags.count).to eq 2 + expect(status.text).to eq text + end + + it 'using hashtag over limit' do + text = '#a #b #c' + Form::AdminSettings.new(post_hash_tags_max: 2).save + + expect { subject.call(status, status.account_id, text: text) }.to raise_error Mastodon::ValidationError + + expect(status.reload.tags.count).to eq 0 + expect(status.text).to_not eq text + end + end end