diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index be5b4f3029..5bdfdd14c9 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController private def account_params - params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, :hide_collections, fields_attributes: [:name, :value]) + params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :my_actor_type, :group_message_following_only, :group_allow_private_message, :discoverable, :hide_collections, fields_attributes: [:name, :value]) end def set_account diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 2d2dd83ae2..659fee9a48 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1287,6 +1287,12 @@ body > [data-popper-placement] { .emoji { display: block; height: 16px; + transition: transform .2s ease; + + &:hover { + transform: scale(1.2); + } + img { margin-top: 0; margin-bottom: 0; diff --git a/app/javascript/styles/mastodon/rich_text.scss b/app/javascript/styles/mastodon/rich_text.scss index 729825fc07..3e979f7b44 100644 --- a/app/javascript/styles/mastodon/rich_text.scss +++ b/app/javascript/styles/mastodon/rich_text.scss @@ -86,6 +86,13 @@ vertical-align: super; } + .emojione { + transition: transform .2s ease; + + &:hover { + transform: scale(1.2); + } + } } .status__content__text { diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index e2355bfbcc..95f558f15e 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -89,6 +89,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity fetch_replies(@status) distribute forward_for_reply + join_group! end def distribute @@ -424,4 +425,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity poll.reload retry end + + def join_group! + GroupReblogService.new.call(@status) + end end diff --git a/app/lib/text_formatter.rb b/app/lib/text_formatter.rb index 980e783ff9..f59a5e25aa 100644 --- a/app/lib/text_formatter.rb +++ b/app/lib/text_formatter.rb @@ -48,7 +48,6 @@ class TextFormatter # html = simple_format(html, {}, sanitize: false).delete("\n") if multiline? html = html.delete("\n") - p html html.html_safe # rubocop:disable Rails/OutputSafety end diff --git a/app/models/account.rb b/app/models/account.rb index 1ff083e54a..c14931e02f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -50,6 +50,8 @@ # trendable :boolean # reviewed_at :datetime # requested_review_at :datetime +# group_message_following_only :boolean +# group_allow_private_message :boolean # class Account < ApplicationRecord @@ -171,8 +173,20 @@ class Account < ApplicationRecord actor_type == 'Group' end + def group=(val) + self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Group' : 'Person' + end + alias group group? + def my_actor_type + actor_type == 'Service' ? 'bot' : actor_type == 'Group' ? 'group' : 'person' + end + + def my_actor_type=(val) + self.actor_type = val == 'bot' ? 'Service' : val == 'group' ? 'Group' : 'Person' + end + def acct local? ? username : "#{username}@#{domain}" end diff --git a/app/serializers/rest/custom_emoji_serializer.rb b/app/serializers/rest/custom_emoji_serializer.rb index a4d2556849..2d47068642 100644 --- a/app/serializers/rest/custom_emoji_serializer.rb +++ b/app/serializers/rest/custom_emoji_serializer.rb @@ -3,9 +3,11 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer include RoutingHelper - attributes :shortcode, :url, :static_url, :visible_in_picker, :width, :height + attributes :shortcode, :url, :static_url, :visible_in_picker attribute :category, if: :category_loaded? + attribute :width, if: :width? + attribute :height, if: :height? def url full_asset_url(object.image.url) @@ -23,11 +25,19 @@ class REST::CustomEmojiSerializer < ActiveModel::Serializer object.association(:category).loaded? && object.category.present? end + def width? + object.respond_to?(:image_width) || object.respond_to?(:width) + end + + def height? + object.respond_to?(:image_height) || object.respond_to?(:height) + end + def width - object.image_width + object.respond_to?(:image_width) ? object.image_width : object.width end def height - object.image_height + object.respond_to?(:image_height) ? object.image_height : object.height end end diff --git a/app/services/group_reblog_service.rb b/app/services/group_reblog_service.rb new file mode 100644 index 0000000000..c770c1ae4f --- /dev/null +++ b/app/services/group_reblog_service.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class GroupReblogService < BaseService + include RoutingHelper + + def call(status) + visibility = status.visibility.to_sym + return nil if !%i(public public_unlisted unlisted private direct).include?(visibility) + + accounts = status.mentions.map(&:account) | status.active_mentions.map(&:account) + transcription = %i(private direct).include?(visibility) + + accounts.each do |account| + next unless account.local? + next if account.group_message_following_only && !account.following?(status.account) + next unless account.group? + next if account.id == status.account_id + next if transcription && !account.group_allow_private_message + + ReblogService.new.call(account, status, { visibility: status.visibility }) if !transcription + + if transcription + username = status.account.local? ? status.account.username : "#{status.account.username}@#{status.account.domain}" + + media_attachments = status.media_attachments.map do |media| + url = media.needs_redownload? ? media_proxy_url(media.id, :original) : full_asset_url(media.file.url(:original)) + MediaAttachment.create( + account: account, + remote_url: media_url(media), + thumbnail_remote_url: media_preview_url(media), + ).tap do |attachment| + attachment.download_file! + attachment.save + end + end + + text = status.account.local? ? status.text : strip_tags(status.text.gsub(/
/, "\n").gsub(/
/, "\n").gsub(/<\/p>/, "\n\n").strip) + + PostStatusService.new.call( + account, + text: "Private message by @#{username}\n\\-\\-\\-\\-\n#{text}", + thread: status.thread, + media_ids: media_attachments.map(&:id), + sensitive: status.sensitive, + spoiler_text: status.spoiler_text, + visibility: :private, + language: status.language, + poll: status.poll, + with_rate_limit: true + ) + end + end + end + + def media_url(media) + if media.not_processed? + nil + elsif media.needs_redownload? + media_proxy_url(media.id, :original) + else + full_asset_url(media.file.url(:original)) + end + end + + def media_preview_url(media) + if media.needs_redownload? + media_proxy_url(media.id, :small) + elsif media.thumbnail.present? + full_asset_url(media.thumbnail.url(:original)) + elsif media.file.styles.key?(:small) + full_asset_url(media.file.url(:small)) + end + end +end diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index d1e750c9c4..f9349670d9 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -122,6 +122,7 @@ class PostStatusService < BaseService DistributionWorker.perform_async(@status.id) ActivityPub::DistributionWorker.perform_async(@status.id) PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll + GroupReblogService.new.call(@status) end def validate_media! diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 3067b37370..5aeb479b89 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -27,7 +27,13 @@ = f.input :locked, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.locked') .fields-group - = f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot') + = f.input :my_actor_type, collection: ['person', 'bot', 'group'],label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.#{item}"), content_tag(:span, I18n.t("simple_form.hints.defaults.#{item}"), class: 'hint')]) }, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', wrapper: :with_floating_label + + .fields-group + = f.input :group_message_following_only, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.group_message_following_only') + + .fields-group + = f.input :group_allow_private_message, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.group_allow_private_message') .fields-group = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 96b0131efe..d19a3f61ce 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -40,12 +40,16 @@ en: discoverable: Allow your account to be discovered by strangers through recommendations, trends and other features email: You will be sent a confirmation e-mail fields: You can have up to 4 items displayed as a table on your profile + group: Reps sent to this account will be automatically BT'd and distributed to all accounts you follow! + group_allow_private_message: Posts are duplicated and cannot be edited or deleted by the post + group_message_following_only: Effective as an anti-troll/spam measure, but requires the effort to follow up with subscribers header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px inbox_url: Copy the URL from the frontpage of the relay you want to use irreversible: Filtered posts will disappear irreversibly, even if filter is later removed locale: The language of the user interface, e-mails and push notifications locked: Manually control who can follow you by approving follow requests password: Use at least 8 characters + person: This is normal account phrase: Will be matched regardless of casing in text or content warning of a post scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) @@ -179,6 +183,9 @@ en: email: E-mail address expires_in: Expire after fields: Profile metadata + group: This is a group account + group_allow_private_message: For group accounts, duplicate private or direct message + group_message_following_only: For group accounts, BT only mentions from people you are following header: Header honeypot: "%{label} (do not fill in)" inbox_url: URL of the relay inbox @@ -186,10 +193,12 @@ en: locale: Interface language locked: Require follow requests max_uses: Max number of uses + my_actor_type: Account type new_password: New password note: Bio otp_attempt: Two-factor code password: Password + person: This is a normal account phrase: Keyword or phrase setting_advanced_layout: Enable advanced web interface setting_aggregate_reblogs: Group boosts in timelines diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index f7e2cb9545..05943fcaa9 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -40,12 +40,16 @@ ja: discoverable: レコメンド、トレンド、その他の機能により、あなたのアカウントを他の人から見つけられるようにします email: 確認のメールが送信されます fields: プロフィールに表として4つまでの項目を表示することができます + group: このアカウントに送られたメンションは自動でBTされ、フォローしている全てのアカウントに配信されます + group_allow_private_message: 投稿は複製されるため、投稿者が編集・削除することはできません + group_message_following_only: 荒らし・スパム対策として有効ですが、加入者をフォローする手間が発生します header: "%{size}までのPNG、GIF、JPGが利用可能です。 %{dimensions}pxまで縮小されます" inbox_url: 使用したいリレーサーバーのトップページからURLをコピーします irreversible: フィルターが後で削除されても、除外された投稿は元に戻せなくなります locale: ユーザーインターフェース、メールやプッシュ通知の言語 locked: フォロワーを手動で承認する必要があります password: 少なくとも8文字は入力してください + person: これは人が使用している通常のアカウントです phrase: 投稿内容の大文字小文字や閲覧注意に関係なく一致 scopes: アプリの API に許可するアクセス権を選択してください。最上位のスコープを選択する場合、個々のスコープを選択する必要はありません。 setting_aggregate_reblogs: 最近ブーストされた投稿が新たにブーストされても表示しません (設定後受信したものにのみ影響) @@ -179,6 +183,9 @@ ja: email: メールアドレス expires_in: 有効期限 fields: プロフィール補足情報 + group: これはグループアカウントです + group_allow_private_message: グループアカウントの場合、フォロワーのみ・ダイレクトのメンションを複製する + group_message_following_only: グループアカウントの場合、自分がフォローしている相手からのメンションのみをBTする header: ヘッダー honeypot: "%{label} (入力しない)" inbox_url: リレーサーバーの inbox URL @@ -186,10 +193,12 @@ ja: locale: 言語 locked: 承認制アカウントにする max_uses: 使用できる回数 + my_actor_type: アカウントの種類 new_password: 新しいパスワード note: プロフィール otp_attempt: 二要素認証コード password: パスワード + person: これは通常のアカウントです phrase: キーワードまたはフレーズ setting_advanced_layout: 上級者向けUIを有効にする setting_aggregate_reblogs: ブーストをまとめる diff --git a/db/migrate/20230314021909_add_group_message_following_only_to_accounts.rb b/db/migrate/20230314021909_add_group_message_following_only_to_accounts.rb new file mode 100644 index 0000000000..40f843afd8 --- /dev/null +++ b/db/migrate/20230314021909_add_group_message_following_only_to_accounts.rb @@ -0,0 +1,5 @@ +class AddGroupMessageFollowingOnlyToAccounts < ActiveRecord::Migration[6.1] + def change + add_column :accounts, :group_message_following_only, :boolean + end +end diff --git a/db/migrate/20230314081013_add_group_allow_private_message_to_accounts.rb b/db/migrate/20230314081013_add_group_allow_private_message_to_accounts.rb new file mode 100644 index 0000000000..c315c14088 --- /dev/null +++ b/db/migrate/20230314081013_add_group_allow_private_message_to_accounts.rb @@ -0,0 +1,5 @@ +class AddGroupAllowPrivateMessageToAccounts < ActiveRecord::Migration[6.1] + def change + add_column :accounts, :group_allow_private_message, :boolean + end +end diff --git a/db/schema.rb b/db/schema.rb index 93a923bbe2..284dc41863 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.define(version: 2023_03_08_061833) do +ActiveRecord::Schema.define(version: 2023_03_14_081013) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -186,6 +186,8 @@ ActiveRecord::Schema.define(version: 2023_03_08_061833) do t.boolean "trendable" t.datetime "reviewed_at" t.datetime "requested_review_at" + t.boolean "group_message_following_only" + t.boolean "group_allow_private_message" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id", where: "(moved_to_account_id IS NOT NULL)"