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)"