mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-14 16:28:59 +00:00
Merge commit 'e93efe0e131481635e88d7ad114ef66148626f90' into glitch-soc/merge-upstream
Conflicts: - `app/serializers/activitypub/note_serializer.rb`: Conflict because upstream added context extensions where glitch-soc had its own. Added upstream's new context extension while keeping ours.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.77.0.
|
||||
# using RuboCop version 1.79.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
|
||||
@@ -765,7 +765,7 @@ GEM
|
||||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 9)
|
||||
rspec-support (3.13.4)
|
||||
rubocop (1.78.0)
|
||||
rubocop (1.79.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -773,8 +773,9 @@ GEM
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.45.1, < 2.0)
|
||||
rubocop-ast (>= 1.46.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
tsort (>= 0.2.0)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.46.0)
|
||||
parser (>= 3.3.7.2)
|
||||
@@ -880,6 +881,7 @@ GEM
|
||||
bindata (~> 2.4)
|
||||
openssl (> 2.0)
|
||||
openssl-signature_algorithm (~> 1.0)
|
||||
tsort (0.2.0)
|
||||
tty-color (0.6.0)
|
||||
tty-cursor (0.7.1)
|
||||
tty-prompt (0.23.1)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
class Api::V1::Admin::TagsController < Api::BaseController
|
||||
include Authorization
|
||||
|
||||
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
|
||||
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
before_action :set_statuses, only: [:index]
|
||||
before_action :set_status, only: [:show, :context]
|
||||
before_action :set_thread, only: [:create]
|
||||
before_action :set_quoted_status, only: [:create]
|
||||
before_action :check_statuses_limit, only: [:index]
|
||||
|
||||
override_rate_limit_headers :create, family: :statuses
|
||||
@@ -76,6 +77,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
current_user.account,
|
||||
text: status_params[:status],
|
||||
thread: @thread,
|
||||
quoted_status: @quoted_status,
|
||||
media_ids: status_params[:media_ids],
|
||||
sensitive: status_params[:sensitive],
|
||||
spoiler_text: status_params[:spoiler_text],
|
||||
@@ -149,6 +151,16 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
|
||||
end
|
||||
|
||||
def set_quoted_status
|
||||
return unless Mastodon::Feature.outgoing_quotes_enabled?
|
||||
|
||||
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
|
||||
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
# TODO: distinguish between non-existing and non-quotable posts
|
||||
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
|
||||
end
|
||||
|
||||
def check_statuses_limit
|
||||
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
|
||||
end
|
||||
@@ -165,6 +177,7 @@ class Api::V1::StatusesController < Api::BaseController
|
||||
params.permit(
|
||||
:status,
|
||||
:in_reply_to_id,
|
||||
:quoted_status_id,
|
||||
:sensitive,
|
||||
:spoiler_text,
|
||||
:visibility,
|
||||
|
||||
@@ -49,8 +49,8 @@ module HomeHelper
|
||||
end
|
||||
end
|
||||
|
||||
def custom_field_classes(field)
|
||||
if field.verified?
|
||||
def field_verified_class(verified)
|
||||
if verified
|
||||
'verified'
|
||||
else
|
||||
'emojify'
|
||||
|
||||
@@ -558,6 +558,8 @@
|
||||
"status.bookmark": "Ouzhpennañ d'ar sinedoù",
|
||||
"status.cancel_reblog_private": "Nac'hañ ar skignadenn",
|
||||
"status.cannot_reblog": "Ar c'hannad-se na c'hall ket bezañ skignet",
|
||||
"status.context.load_new_replies": "Respontoù nevez zo",
|
||||
"status.context.loading": "O kerc'hat muioc'h a respontoù",
|
||||
"status.copy": "Eilañ liamm ar c'hannad",
|
||||
"status.delete": "Dilemel",
|
||||
"status.detailed_status": "Gwel kaozeadenn munudek",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"account.followers": "Follower",
|
||||
"account.followers.empty": "Diesem Profil folgt noch niemand.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}",
|
||||
"account.followers_you_know_counter": "{counter} bekannt",
|
||||
"account.followers_you_know_counter": "{counter} Follower kennst Du",
|
||||
"account.following": "Folge ich",
|
||||
"account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
|
||||
"account.follows.empty": "Dieses Profil folgt noch niemandem.",
|
||||
@@ -63,7 +63,7 @@
|
||||
"account.mute_short": "Stummschalten",
|
||||
"account.muted": "Stummgeschaltet",
|
||||
"account.muting": "Stummgeschaltet",
|
||||
"account.mutual": "Ihr folgt einander",
|
||||
"account.mutual": "Ihr folgt euch",
|
||||
"account.no_bio": "Keine Beschreibung verfügbar.",
|
||||
"account.open_original_page": "Ursprüngliche Seite öffnen",
|
||||
"account.posts": "Beiträge",
|
||||
@@ -225,7 +225,7 @@
|
||||
"confirmations.discard_draft.edit.title": "Änderungen an diesem Beitrag verwerfen?",
|
||||
"confirmations.discard_draft.post.cancel": "Entwurf fortsetzen",
|
||||
"confirmations.discard_draft.post.message": "Beim Fortfahren wird der gerade verfasste Beitrag verworfen.",
|
||||
"confirmations.discard_draft.post.title": "Beitragsentwurf verwerfen?",
|
||||
"confirmations.discard_draft.post.title": "Entwurf verwerfen?",
|
||||
"confirmations.discard_edit_media.confirm": "Verwerfen",
|
||||
"confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem verwerfen?",
|
||||
"confirmations.follow_to_list.confirm": "Folgen und zur Liste hinzufügen",
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
"notification.moderation_warning.action_suspend": "Ο λογαριασμός σου έχει ανασταλεί.",
|
||||
"notification.own_poll": "Η δημοσκόπησή σου έληξε",
|
||||
"notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει",
|
||||
"notification.reblog": "Ο/Η {name} ενίσχυσε τη δημοσίευσή σου",
|
||||
"notification.reblog": "Ο/Η {name} ενίσχυσε την ανάρτηση σου",
|
||||
"notification.reblog.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> ενίσχυσαν την ανάρτησή σου",
|
||||
"notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}",
|
||||
"notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.",
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "Σελιδοδείκτης",
|
||||
"status.cancel_reblog_private": "Ακύρωση ενίσχυσης",
|
||||
"status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί",
|
||||
"status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες",
|
||||
"status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις",
|
||||
"status.continued_thread": "Συνεχιζόμενο νήματος",
|
||||
"status.copy": "Αντιγραφή συνδέσμου ανάρτησης",
|
||||
"status.delete": "Διαγραφή",
|
||||
|
||||
@@ -612,7 +612,7 @@
|
||||
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
|
||||
"notification.own_poll": "Your poll has ended",
|
||||
"notification.poll": "A poll you voted in has ended",
|
||||
"notification.reblog": "{name} boosted your status",
|
||||
"notification.reblog": "{name} boosted your post",
|
||||
"notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post",
|
||||
"notification.relationships_severance_event": "Lost connections with {name}",
|
||||
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",
|
||||
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "Leabharmharcanna",
|
||||
"status.cancel_reblog_private": "Dímhol",
|
||||
"status.cannot_reblog": "Ní féidir an phostáil seo a mholadh",
|
||||
"status.context.load_new_replies": "Freagraí nua ar fáil",
|
||||
"status.context.loading": "Ag seiceáil le haghaidh tuilleadh freagraí",
|
||||
"status.continued_thread": "Snáithe ar lean",
|
||||
"status.copy": "Cóipeáil an nasc chuig an bpostáil",
|
||||
"status.delete": "Scrios",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"about.blocks": "Модерацияланған серверлер",
|
||||
"about.contact": "Байланыс:",
|
||||
"about.default_locale": "Әдепкі",
|
||||
"about.disclaimer": "Mastodon деген тегін, бастапқы коды ашық бағдарламалық жасақтама және Mastodon gGmbH-тің сауда маркасы.",
|
||||
"about.domain_blocks.no_reason_available": "Себеп қолжетімсіз",
|
||||
"about.domain_blocks.preamble": "Mastodon әдетте сізге Fediverse'тің кез келген серверінің қолданушыларының контентін көріп, олармен байланысуға мүмкіндік береді. Осы белгілі серверде жасалған ережеден тыс жағдайлар міне.",
|
||||
|
||||
@@ -845,6 +845,8 @@
|
||||
"status.bookmark": "Guardar nos marcadores",
|
||||
"status.cancel_reblog_private": "Retirar impulso",
|
||||
"status.cannot_reblog": "Esta publicação não pode ser impulsionada",
|
||||
"status.context.load_new_replies": "Novas respostas disponíveis",
|
||||
"status.context.loading": "A verificar por mais respostas",
|
||||
"status.continued_thread": "Continuação da conversa",
|
||||
"status.copy": "Copiar hiperligação da publicação",
|
||||
"status.delete": "Eliminar",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"about.blocks": "Serbidores moderados",
|
||||
"about.contact": "Cuntatu:",
|
||||
"about.default_locale": "Predefinidu",
|
||||
"about.disclaimer": "Mastodon est software de còdighe lìberu e unu màrchiu de Mastodon gGmbH.",
|
||||
"about.domain_blocks.no_reason_available": "Peruna resone a disponimentu",
|
||||
"about.domain_blocks.preamble": "Mastodon ti permitit de bìdere su cuntenutu de utentes de cale si siat àteru serbidore de su fediversu. Custas sunt etzetziones fatas in custu serbidore ispetzìficu.",
|
||||
@@ -8,6 +9,7 @@
|
||||
"about.domain_blocks.silenced.title": "Limitadu",
|
||||
"about.domain_blocks.suspended.explanation": "Perunu datu de custu serbidore at a èssere protzessadu, immagasinadu o cuncambiadu; est impossìbile duncas cale si siat interatzione o comunicatzione cun is utentes de custu serbidore.",
|
||||
"about.domain_blocks.suspended.title": "Suspèndidu",
|
||||
"about.language_label": "Idioma",
|
||||
"about.not_available": "Custa informatzione no est istada posta a disponimentu in custu serbidore.",
|
||||
"about.powered_by": "Rete sotziale detzentralizada impulsada dae {mastodon}",
|
||||
"about.rules": "Règulas de su serbidore",
|
||||
@@ -19,13 +21,21 @@
|
||||
"account.block_domain": "Bloca su domìniu {domain}",
|
||||
"account.block_short": "Bloca",
|
||||
"account.blocked": "Blocadu",
|
||||
"account.blocking": "Blocadu",
|
||||
"account.cancel_follow_request": "Annulla sa sighidura",
|
||||
"account.copy": "Còpia su ligòngiu a su profilu",
|
||||
"account.direct": "Mèntova a @{name} in privadu",
|
||||
"account.disable_notifications": "Non mi notìfiches prus cando @{name} pùblichet messàgios",
|
||||
"account.domain_blocking": "Blocamus su domìniu",
|
||||
"account.edit_profile": "Modìfica profilu",
|
||||
"account.enable_notifications": "Notìfica·mi cando @{name} pùblicat messàgios",
|
||||
"account.endorse": "Cussìgia in su profilu tuo",
|
||||
"account.familiar_followers_many": "Sighidu dae {name1}, {name2} e {othersCount, plural,one {un'àtera persone chi connosches} other {àteras # persones chi connosches}}",
|
||||
"account.familiar_followers_one": "Sighidu dae {name1}",
|
||||
"account.familiar_followers_two": "Sighidu dae {name1} e {name2}",
|
||||
"account.featured": "In evidèntzia",
|
||||
"account.featured.accounts": "Profilos",
|
||||
"account.featured.hashtags": "Etichetas",
|
||||
"account.featured_tags.last_status_at": "Ùrtima publicatzione in su {date}",
|
||||
"account.featured_tags.last_status_never": "Peruna publicatzione",
|
||||
"account.follow": "Sighi",
|
||||
@@ -33,9 +43,11 @@
|
||||
"account.followers": "Sighiduras",
|
||||
"account.followers.empty": "Nemos sighit ancora custa persone.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}",
|
||||
"account.followers_you_know_counter": "{counter} chi connosches",
|
||||
"account.following": "Sighende",
|
||||
"account.following_counter": "{count, plural, one {sighende a {counter}} other {sighende a {counter}}}",
|
||||
"account.follows.empty": "Custa persone non sighit ancora a nemos.",
|
||||
"account.follows_you": "Ti sighit",
|
||||
"account.go_to_profile": "Bae a su profilu",
|
||||
"account.hide_reblogs": "Cua is cumpartziduras de @{name}",
|
||||
"account.in_memoriam": "In memoriam.",
|
||||
@@ -50,18 +62,22 @@
|
||||
"account.mute_notifications_short": "Pone is notìficas a sa muda",
|
||||
"account.mute_short": "A sa muda",
|
||||
"account.muted": "A sa muda",
|
||||
"account.muting": "A sa muda",
|
||||
"account.no_bio": "Peruna descritzione frunida.",
|
||||
"account.open_original_page": "Aberi sa pàgina originale",
|
||||
"account.posts": "Publicatziones",
|
||||
"account.posts_with_replies": "Publicatziones e rispostas",
|
||||
"account.remove_from_followers": "Cantzella a {name} dae is sighiduras",
|
||||
"account.report": "Signala @{name}",
|
||||
"account.requested": "Abetende s'aprovatzione. Incarca pro annullare sa rechesta de sighidura",
|
||||
"account.requested_follow": "{name} at dimandadu de ti sighire",
|
||||
"account.requests_to_follow_you": "Rechestas de sighidura",
|
||||
"account.share": "Cumpartzi su profilu de @{name}",
|
||||
"account.show_reblogs": "Ammustra is cumpartziduras de @{name}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
|
||||
"account.unblock": "Isbloca a @{name}",
|
||||
"account.unblock_domain": "Isbloca su domìniu {domain}",
|
||||
"account.unblock_domain_short": "Isbloca",
|
||||
"account.unblock_short": "Isbloca",
|
||||
"account.unendorse": "Non cussiges in su profilu",
|
||||
"account.unfollow": "Non sigas prus",
|
||||
@@ -83,7 +99,22 @@
|
||||
"alert.unexpected.message": "Ddoe est istada una faddina.",
|
||||
"alert.unexpected.title": "Oh!",
|
||||
"alt_text_badge.title": "Testu alternativu",
|
||||
"alt_text_modal.add_alt_text": "Agiunghe testu alternativu",
|
||||
"alt_text_modal.add_text_from_image": "Agiunghe testu dae un'immàgine",
|
||||
"alt_text_modal.cancel": "Annulla",
|
||||
"alt_text_modal.change_thumbnail": "Càmbia sa miniadura",
|
||||
"alt_text_modal.done": "Fatu",
|
||||
"announcement.announcement": "Annùntziu",
|
||||
"annual_report.summary.archetype.booster": "Semper a s'ùrtima",
|
||||
"annual_report.summary.followers.followers": "sighiduras",
|
||||
"annual_report.summary.followers.total": "{count} totale",
|
||||
"annual_report.summary.highlighted_post.possessive": "de {name}",
|
||||
"annual_report.summary.most_used_app.most_used_app": "aplicatzione prus impreada",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "eticheta prus impreada",
|
||||
"annual_report.summary.most_used_hashtag.none": "Peruna",
|
||||
"annual_report.summary.new_posts.new_posts": "publicatziones noas",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "No dd'amus a nàrrere a Bernie.",
|
||||
"annual_report.summary.thanks": "Gràtzias de èssere parte de Mastodon!",
|
||||
"attachments_list.unprocessed": "(non protzessadu)",
|
||||
"audio.hide": "Cua s'àudio",
|
||||
"block_modal.remote_users_caveat": "Amus a pedire a su serbidore {domain} de rispetare sa detzisione tua. Nointames custu, su rispetu no est garantidu ca unos cantos serbidores diant pòdere gestire is blocos de manera diferente. Is publicatzione pùblicas diant pòdere ancora èssere visìbiles a is utentes chi no ant fatu s'atzessu.",
|
||||
@@ -107,6 +138,7 @@
|
||||
"bundle_column_error.routing.body": "Impossìbile agatare sa pàgina rechesta. Seguru chi s'URL in sa barra de indiritzos est curretu?",
|
||||
"bundle_column_error.routing.title": "404",
|
||||
"bundle_modal_error.close": "Serra",
|
||||
"bundle_modal_error.message": "Faddina in su carrigamentu de custu ischermu.",
|
||||
"bundle_modal_error.retry": "Torra·bi a proare",
|
||||
"closed_registrations.other_server_instructions": "Dae chi Mastodon est detzentralizadu, podes creare unu contu in un'àteru serbidore e interagire cun custu.",
|
||||
"closed_registrations_modal.description": "Sa creatzione de contos in {domain} no est possìbile in custu momentu, però tene in cunsideru chi non tenes bisòngiu de unu contu ispetzìficu in {domain} pro impreare Mastodon.",
|
||||
@@ -116,13 +148,16 @@
|
||||
"column.blocks": "Persones blocadas",
|
||||
"column.bookmarks": "Sinnalibros",
|
||||
"column.community": "Lìnia de tempus locale",
|
||||
"column.create_list": "Crea una lista",
|
||||
"column.direct": "Mentziones privadas",
|
||||
"column.directory": "Nàviga in is profilos",
|
||||
"column.domain_blocks": "Domìnios blocados",
|
||||
"column.edit_list": "Modifica sa lista",
|
||||
"column.favourites": "Preferidos",
|
||||
"column.firehose": "Publicatziones in direta",
|
||||
"column.follow_requests": "Rechestas de sighidura",
|
||||
"column.home": "Printzipale",
|
||||
"column.list_members": "Gesti is persones de sa lista",
|
||||
"column.lists": "Listas",
|
||||
"column.mutes": "Persones a sa muda",
|
||||
"column.notifications": "Notìficas",
|
||||
@@ -135,6 +170,7 @@
|
||||
"column_header.pin": "Apica",
|
||||
"column_header.show_settings": "Ammustra is cunfiguratziones",
|
||||
"column_header.unpin": "Boga dae pitzu",
|
||||
"column_search.cancel": "Annulla",
|
||||
"community.column_settings.local_only": "Isceti locale",
|
||||
"community.column_settings.media_only": "Isceti multimediale",
|
||||
"community.column_settings.remote_only": "Isceti remotu",
|
||||
@@ -152,6 +188,7 @@
|
||||
"compose_form.poll.duration": "Longària de su sondàgiu",
|
||||
"compose_form.poll.multiple": "Sèberu mùltiplu",
|
||||
"compose_form.poll.option_placeholder": "Optzione {number}",
|
||||
"compose_form.poll.single": "Sèberu ùnicu",
|
||||
"compose_form.poll.switch_to_multiple": "Muda su sondàgiu pro permìtere multi-optziones",
|
||||
"compose_form.poll.switch_to_single": "Muda su sondàgiu pro permìtere un'optzione isceti",
|
||||
"compose_form.poll.type": "Istile",
|
||||
@@ -169,6 +206,8 @@
|
||||
"confirmations.delete_list.confirm": "Cantzella",
|
||||
"confirmations.delete_list.message": "Seguru chi boles cantzellare custa lista in manera permanente?",
|
||||
"confirmations.delete_list.title": "Cantzellare sa lista?",
|
||||
"confirmations.discard_draft.confirm": "Iscarta e sighi",
|
||||
"confirmations.discard_draft.edit.cancel": "Sighi cun s'editzione",
|
||||
"confirmations.discard_edit_media.confirm": "Iscarta",
|
||||
"confirmations.discard_edit_media.message": "Tenes modìficas non sarvadas a is descritziones o a is anteprimas de is cuntenutos, ddas boles iscartare su matessi?",
|
||||
"confirmations.logout.confirm": "Essi·nche",
|
||||
@@ -256,6 +295,7 @@
|
||||
"explore.trending_links": "Noas",
|
||||
"explore.trending_statuses": "Publicatziones",
|
||||
"explore.trending_tags": "Etichetas",
|
||||
"featured_carousel.slide": "{index} de {total}",
|
||||
"filter_modal.added.context_mismatch_title": "Su cuntestu non currispondet.",
|
||||
"filter_modal.added.expired_title": "Filtru iscadidu.",
|
||||
"filter_modal.added.review_and_configure_title": "Cunfiguratziones de filtru",
|
||||
@@ -294,8 +334,12 @@
|
||||
"footer.privacy_policy": "Polìtica de riservadesa",
|
||||
"footer.source_code": "Ammustra su còdighe de orìgine",
|
||||
"footer.status": "Istadu",
|
||||
"footer.terms_of_service": "Cunditziones de su servìtziu",
|
||||
"generic.saved": "Sarvadu",
|
||||
"getting_started.heading": "Comente cumintzare",
|
||||
"hashtag.admin_moderation": "Aberi s'interfache de moderatzione pro #{name}",
|
||||
"hashtag.browse": "Nàviga in is publicatziones de #{hashtag}",
|
||||
"hashtag.browse_from_account": "Nàviga in is publicatziones de @{name} in #{hashtag}",
|
||||
"hashtag.column_header.tag_mode.all": "e {additional}",
|
||||
"hashtag.column_header.tag_mode.any": "o {additional}",
|
||||
"hashtag.column_header.tag_mode.none": "sena {additional}",
|
||||
@@ -308,7 +352,10 @@
|
||||
"hashtag.counter_by_accounts": "{count, plural, one {{counter} partetzipante} other {{counter} partetzipantes}}",
|
||||
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
|
||||
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}} oe",
|
||||
"hashtag.feature": "In evidèntzia in su profilu",
|
||||
"hashtag.follow": "Sighi su hashtag",
|
||||
"hashtag.mute": "Pone #{hashtag} a sa muda",
|
||||
"hashtag.unfeature": "No ddu pòngias in evidèntzia in su profilu",
|
||||
"hashtag.unfollow": "Non sigas prus s'eticheta",
|
||||
"hashtags.and_other": "… e {count, plural, one {un'àteru} other {àteros #}}",
|
||||
"hints.profiles.posts_may_be_missing": "Podet èssere chi ammanchent tzertas publicatziones de custu profilu.",
|
||||
@@ -324,9 +371,14 @@
|
||||
"ignore_notifications_modal.filter_instead": "Opuru filtra",
|
||||
"ignore_notifications_modal.filter_to_act_users": "As a pòdere ancora atzetare, refudare o sinnalare a utentes",
|
||||
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtrare agiudat a evitare possìbiles confusiones",
|
||||
"info_button.label": "Agiudu",
|
||||
"interaction_modal.go": "Sighi",
|
||||
"interaction_modal.no_account_yet": "Non tenes galu perunu contu?",
|
||||
"interaction_modal.on_another_server": "In un'àteru serbidore",
|
||||
"interaction_modal.on_this_server": "In custu serbidore",
|
||||
"interaction_modal.title.follow": "Sighi a {name}",
|
||||
"interaction_modal.title.reply": "Risponde a sa publicatzione de {name}",
|
||||
"interaction_modal.username_prompt": "Pro es., {example}",
|
||||
"intervals.full.days": "{number, plural, one {# die} other {# dies}}",
|
||||
"intervals.full.hours": "{number, plural, one {# ora} other {# oras}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
|
||||
@@ -339,6 +391,7 @@
|
||||
"keyboard_shortcuts.direct": "pro abèrrere sa colunna de mèntovos privados",
|
||||
"keyboard_shortcuts.down": "Move in bàsciu in sa lista",
|
||||
"keyboard_shortcuts.enter": "Aberi una publicatzione",
|
||||
"keyboard_shortcuts.favourite": "Publicatzione preferida",
|
||||
"keyboard_shortcuts.favourites": "Aberi sa lista de preferidos",
|
||||
"keyboard_shortcuts.federated": "Aberi sa lìnia de tempus federada",
|
||||
"keyboard_shortcuts.heading": "Incurtzaduras de tecladu",
|
||||
@@ -361,18 +414,25 @@
|
||||
"keyboard_shortcuts.toggle_hidden": "Ammustra o cua su testu de is AC",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Ammustra/cua elementos multimediales",
|
||||
"keyboard_shortcuts.toot": "Cumintza a iscrìere una publicatzione noa",
|
||||
"keyboard_shortcuts.translate": "pro tradùere una publicatzione",
|
||||
"keyboard_shortcuts.unfocus": "Essi de s'àrea de cumpositzione de testu o de chirca",
|
||||
"keyboard_shortcuts.up": "Move in susu in sa lista",
|
||||
"lightbox.close": "Serra",
|
||||
"lightbox.next": "Imbeniente",
|
||||
"lightbox.previous": "Pretzedente",
|
||||
"lightbox.zoom_in": "Ismànnia finas a sa mannària atuale",
|
||||
"limited_account_hint.title": "Custu profilu est istadu cuadu dae sa moderatzione de {domain}.",
|
||||
"link_preview.author": "Dae {name}",
|
||||
"link_preview.shares": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
|
||||
"lists.delete": "Cantzella sa lista",
|
||||
"lists.edit": "Modìfica sa lista",
|
||||
"lists.remove_member": "Cantzella",
|
||||
"lists.replies_policy.followed": "Cale si siat persone chi sighis",
|
||||
"lists.replies_policy.list": "Persones de sa lista",
|
||||
"lists.replies_policy.none": "Nemos",
|
||||
"lists.save": "Sarva",
|
||||
"lists.search": "Chirca",
|
||||
"lists.show_replies_to": "Include rispostas dae gente de sa lista a",
|
||||
"load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}",
|
||||
"loading_indicator.label": "Carrighende…",
|
||||
"media_gallery.hide": "Cua",
|
||||
@@ -387,8 +447,10 @@
|
||||
"mute_modal.you_wont_see_mentions": "No as a bìdere is publicatziones chi mèntovent a custa persone.",
|
||||
"mute_modal.you_wont_see_posts": "At a pòdere bìdere is publicatziones tuas, però tue no as a bìdere cussas suas.",
|
||||
"navigation_bar.about": "Informatziones",
|
||||
"navigation_bar.account_settings": "Crae e seguresa",
|
||||
"navigation_bar.administration": "Amministratzione",
|
||||
"navigation_bar.advanced_interface": "Aberi s'interfache web avantzada",
|
||||
"navigation_bar.automated_deletion": "Cantzelladura automàtica de publicatziones",
|
||||
"navigation_bar.blocks": "Persones blocadas",
|
||||
"navigation_bar.bookmarks": "Sinnalibros",
|
||||
"navigation_bar.direct": "Mentziones privadas",
|
||||
@@ -398,13 +460,18 @@
|
||||
"navigation_bar.follow_requests": "Rechestas de sighidura",
|
||||
"navigation_bar.followed_tags": "Etichetas sighidas",
|
||||
"navigation_bar.follows_and_followers": "Gente chi sighis e sighiduras",
|
||||
"navigation_bar.import_export": "Importatzione e esportatzione",
|
||||
"navigation_bar.lists": "Listas",
|
||||
"navigation_bar.live_feed_local": "Canale in direta (locale)",
|
||||
"navigation_bar.live_feed_public": "Canale in direta (pùblicu)",
|
||||
"navigation_bar.logout": "Essi",
|
||||
"navigation_bar.moderation": "Moderatzione",
|
||||
"navigation_bar.more": "Àteru",
|
||||
"navigation_bar.mutes": "Persones a sa muda",
|
||||
"navigation_bar.opened_in_classic_interface": "Publicatziones, contos e àteras pàginas ispetzìficas sunt abertas in manera predefinida in s'interfache web clàssica.",
|
||||
"navigation_bar.preferences": "Preferèntzias",
|
||||
"navigation_bar.search": "Chirca",
|
||||
"navigation_bar.search_trends": "Chirca / in tendèntzia",
|
||||
"not_signed_in_indicator.not_signed_in": "Ti depes identificare pro atzèdere a custa resursa.",
|
||||
"notification.admin.report": "{name} at sinnaladu a {target}",
|
||||
"notification.admin.report_account": "{name} at sinnaladu {count, plural, one {una publicatzione} other {# publicatziones}} dae {target} pro {category}",
|
||||
@@ -461,15 +528,19 @@
|
||||
"notification_requests.minimize_banner": "Mìnima su bànner de notìficas filtradas",
|
||||
"notification_requests.notifications_from": "Notìficas dae {name}",
|
||||
"notification_requests.title": "Notìficas filtradas",
|
||||
"notification_requests.view": "Mustra notìficas",
|
||||
"notifications.clear": "Lìmpia notìficas",
|
||||
"notifications.clear_confirmation": "Seguru chi boles isboidare in manera permanente totu is notìficas tuas?",
|
||||
"notifications.clear_title": "Boles cantzellare is notìficas?",
|
||||
"notifications.column_settings.admin.report": "Informes noos:",
|
||||
"notifications.column_settings.admin.sign_up": "Registros noos:",
|
||||
"notifications.column_settings.alert": "Notìficas de iscrivania",
|
||||
"notifications.column_settings.favourite": "Preferidos:",
|
||||
"notifications.column_settings.filter_bar.advanced": "Ammustra totu is categorias",
|
||||
"notifications.column_settings.filter_bar.category": "Barra de filtru lestru",
|
||||
"notifications.column_settings.follow": "Sighiduras noas:",
|
||||
"notifications.column_settings.follow_request": "Rechestas noas de sighidura:",
|
||||
"notifications.column_settings.group": "Grupu",
|
||||
"notifications.column_settings.mention": "Mèntovos:",
|
||||
"notifications.column_settings.poll": "Resurtados de su sondàgiu:",
|
||||
"notifications.column_settings.push": "Notìficas push",
|
||||
@@ -493,6 +564,8 @@
|
||||
"notifications.permission_denied": "Is notìficas de iscrivania non sunt a disponimentu pro neghe de rechestas de permissu chi sunt istadas dennegadas in antis",
|
||||
"notifications.permission_denied_alert": "Is notìficas de iscrivania non podent èssere abilitadas, ca su permissu de su navigadore est istadu dennegadu in antis",
|
||||
"notifications.permission_required": "Is notìficas de iscrivania no sunt a disponimentu ca ammancat su permissu rechèdidu.",
|
||||
"notifications.policy.accept": "Atzeta",
|
||||
"notifications.policy.accept_hint": "Mustra in is notìficas",
|
||||
"notifications.policy.filter_new_accounts.hint": "Creadu {days, plural, one {erisero} other {in is ùrtimas # dies}}",
|
||||
"notifications.policy.filter_new_accounts_title": "Contos noos",
|
||||
"notifications.policy.filter_not_followers_title": "Gente chi non ti sighit",
|
||||
@@ -501,8 +574,15 @@
|
||||
"notifications_permission_banner.enable": "Abilita is notìficas de iscrivania",
|
||||
"notifications_permission_banner.how_to_control": "Pro retzire notìficas cando Mastodon no est abertu, abilita is notìficas de iscrivania. Podes controllare cun pretzisione is castas de interatziones chi ingendrant notìficas de iscrivania pro mèdiu de su butone {icon} in subra, cando sunt abilitadas.",
|
||||
"notifications_permission_banner.title": "Non ti perdas mai nudda",
|
||||
"onboarding.follows.back": "A coa",
|
||||
"onboarding.follows.done": "Fatu",
|
||||
"onboarding.follows.search": "Chirca",
|
||||
"onboarding.follows.title": "Sighi a gente pro cumintzare",
|
||||
"onboarding.profile.display_name": "Nòmine visìbile",
|
||||
"onboarding.profile.note": "Biografia",
|
||||
"onboarding.profile.save_and_continue": "Sarva e sighi",
|
||||
"onboarding.profile.title": "Cunfiguratzione de profilu",
|
||||
"onboarding.profile.upload_avatar": "Càrriga una fotografia de profilu",
|
||||
"picture_in_picture.restore": "Torra·ddu a ue fiat",
|
||||
"poll.closed": "Serradu",
|
||||
"poll.refresh": "Atualiza",
|
||||
@@ -597,6 +677,7 @@
|
||||
"search_results.hashtags": "Etichetas",
|
||||
"search_results.see_all": "Bide totu",
|
||||
"search_results.statuses": "Publicatziones",
|
||||
"search_results.title": "Chirca \"{q}\"",
|
||||
"server_banner.about_active_users": "Gente chi at impreadu custu serbidore is ùrtimas 30 dies (Utentes cun Atividade a su Mese)",
|
||||
"server_banner.active_users": "utentes ativos",
|
||||
"server_banner.administered_by": "Amministradu dae:",
|
||||
|
||||
@@ -815,6 +815,8 @@
|
||||
"status.bookmark": "Додати до закладок",
|
||||
"status.cancel_reblog_private": "Скасувати поширення",
|
||||
"status.cannot_reblog": "Цей допис не може бути поширений",
|
||||
"status.context.load_new_replies": "Доступні нові відповіді",
|
||||
"status.context.loading": "Перевірка додаткових відповідей",
|
||||
"status.continued_thread": "Продовження у потоці",
|
||||
"status.copy": "Копіювати посилання на допис",
|
||||
"status.delete": "Видалити",
|
||||
|
||||
@@ -116,6 +116,20 @@ class ActivityPub::Activity
|
||||
fetch_remote_original_status
|
||||
end
|
||||
|
||||
def quote_from_request_json(json)
|
||||
quoted_status_uri = value_or_id(json['object'])
|
||||
quoting_status_uri = value_or_id(json['instrument'])
|
||||
return if quoting_status_uri.nil? || quoted_status_uri.nil?
|
||||
|
||||
quoting_status = status_from_uri(quoting_status_uri)
|
||||
return unless quoting_status.present? && quoting_status.quote.present?
|
||||
|
||||
quoted_status = status_from_uri(quoted_status_uri)
|
||||
return unless quoted_status.present? && quoted_status.account == @account && quoting_status.quote.quoted_status == quoted_status
|
||||
|
||||
quoting_status.quote
|
||||
end
|
||||
|
||||
def dereference_object!
|
||||
return unless @object.is_a?(String)
|
||||
|
||||
@@ -143,6 +157,10 @@ class ActivityPub::Activity
|
||||
@follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def quote_request_from_object
|
||||
@quote_request_from_object ||= Quote.find_by(quoted_account: @account, activity_uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
def follow_from_object
|
||||
@follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
@@ -4,10 +4,13 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||
def perform
|
||||
return accept_follow_for_relay if relay_follow?
|
||||
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
|
||||
return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
accept_embedded_follow
|
||||
when 'QuoteRequest'
|
||||
accept_embedded_quote_request
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +34,29 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
|
||||
RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow
|
||||
end
|
||||
|
||||
def accept_embedded_quote_request
|
||||
approval_uri = value_or_id(first_of_value(@json['result']))
|
||||
return if approval_uri.nil?
|
||||
|
||||
quote = quote_from_request_json(@object)
|
||||
return unless quote.present? && quote.status.local?
|
||||
|
||||
accept_quote!(quote)
|
||||
end
|
||||
|
||||
def accept_quote!(quote)
|
||||
approval_uri = value_or_id(first_of_value(@json['result']))
|
||||
return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local?
|
||||
|
||||
# NOTE: we are not going through `ActivityPub::VerifyQuoteService` as the `Accept` is as authoritative
|
||||
# as the stamp, but this means we are not checking the stamp, which may lead to inconsistencies
|
||||
# in case of an implementation bug
|
||||
quote.update!(state: :accepted, approval_uri: approval_uri)
|
||||
|
||||
DistributionWorker.perform_async(quote.status_id, { 'update' => true })
|
||||
ActivityPub::StatusUpdateDistributionWorker.perform_async(quote.status_id, { 'updated_at' => Time.now.utc.iso8601 })
|
||||
end
|
||||
|
||||
def accept_follow_for_relay
|
||||
relay.update!(state: :accepted)
|
||||
end
|
||||
|
||||
@@ -5,10 +5,13 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||
return reject_follow_for_relay if relay_follow?
|
||||
return follow_request_from_object.reject! unless follow_request_from_object.nil?
|
||||
return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil?
|
||||
return reject_quote!(quote_request_from_object) unless quote_request_from_object.nil?
|
||||
|
||||
case @object['type']
|
||||
when 'Follow'
|
||||
reject_embedded_follow
|
||||
when 'QuoteRequest'
|
||||
reject_embedded_quote_request
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,6 +32,20 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||
relay.update!(state: :rejected)
|
||||
end
|
||||
|
||||
def reject_embedded_quote_request
|
||||
quote = quote_from_request_json(@object)
|
||||
return unless quote.present? && quote.status.local?
|
||||
|
||||
reject_quote!(quoting_status.quote)
|
||||
end
|
||||
|
||||
def reject_quote!(quote)
|
||||
return unless quote.quoted_account == @account && quote.status.local?
|
||||
|
||||
# TODO: broadcast an update?
|
||||
quote.reject!
|
||||
end
|
||||
|
||||
def relay
|
||||
@relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil?
|
||||
end
|
||||
|
||||
@@ -12,9 +12,7 @@ module ActivityPub::CaseTransform
|
||||
when Hash then value.deep_transform_keys! { |key| camel_lower(key) }
|
||||
when Symbol then camel_lower(value.to_s).to_sym
|
||||
when String
|
||||
camel_lower_cache[value] ||= if value.start_with?('_:')
|
||||
"_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
|
||||
elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
|
||||
camel_lower_cache[value] ||= if value.start_with?('_misskey') || LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
|
||||
value
|
||||
else
|
||||
value.underscore.camelize(:lower)
|
||||
|
||||
@@ -45,7 +45,9 @@ class EmojiFormatter
|
||||
i += 1
|
||||
|
||||
if inside_shortname && text[i] == ':'
|
||||
inside_shortname = false
|
||||
# https://github.com/rubocop/rubocop/issues/14383
|
||||
# False positive in line below, remove disable when resolved
|
||||
inside_shortname = false # rubocop:disable Lint/UselessAssignment
|
||||
shortcode = text[(shortname_start_index + 1)..(i - 1)]
|
||||
char_after = text[i + 1]
|
||||
|
||||
|
||||
@@ -71,6 +71,8 @@ class StatusCacheHydrator
|
||||
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
|
||||
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
|
||||
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
|
||||
# TODO: performance optimization by not loading `Account` twice
|
||||
payload[:quote_approval][:current_user] = status.quote_policy_for_account(Account.find_by(id: account_id)) if payload[:quote_approval]
|
||||
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote]
|
||||
end
|
||||
|
||||
|
||||
63
app/models/concerns/status/interaction_policy_concern.rb
Normal file
63
app/models/concerns/status/interaction_policy_concern.rb
Normal file
@@ -0,0 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Status::InteractionPolicyConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
QUOTE_APPROVAL_POLICY_FLAGS = {
|
||||
unknown: (1 << 0),
|
||||
public: (1 << 1),
|
||||
followers: (1 << 2),
|
||||
followed: (1 << 3),
|
||||
}.freeze
|
||||
|
||||
def quote_policy_as_keys(kind)
|
||||
case kind
|
||||
when :automatic
|
||||
policy = quote_approval_policy >> 16
|
||||
when :manual
|
||||
policy = quote_approval_policy & 0xFFFF
|
||||
end
|
||||
|
||||
QUOTE_APPROVAL_POLICY_FLAGS.keys.select { |key| policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[key]) }.map(&:to_s)
|
||||
end
|
||||
|
||||
# Returns `:automatic`, `:manual`, `:unknown` or `:denied`
|
||||
def quote_policy_for_account(other_account, preloaded_relations: {})
|
||||
return :denied if other_account.nil?
|
||||
|
||||
following_author = nil
|
||||
|
||||
# Post author is always allowed to quote themselves
|
||||
return :automatic if account_id == other_account.id
|
||||
|
||||
automatic_policy = quote_approval_policy >> 16
|
||||
manual_policy = quote_approval_policy & 0xFFFF
|
||||
|
||||
# Checking for public policy first because it's less expensive than looking at mentions
|
||||
return :automatic if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
||||
|
||||
# Mentioned users are always allowed to quote
|
||||
if active_mentions.loaded?
|
||||
return :automatic if active_mentions.any? { |mention| mention.account_id == other_account.id }
|
||||
elsif active_mentions.exists?(account: other_account)
|
||||
return :automatic
|
||||
end
|
||||
|
||||
if automatic_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
||||
following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil?
|
||||
return :automatic if following_author
|
||||
end
|
||||
|
||||
# We don't know we are allowed by the automatic policy, considering the manual one
|
||||
return :manual if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:public])
|
||||
|
||||
if manual_policy.anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:followers])
|
||||
following_author = preloaded_relations[:following] ? preloaded_relations[:following][account_id] : other_account.following?(account) if following_author.nil?
|
||||
return :manual if following_author
|
||||
end
|
||||
|
||||
return :unknown if (automatic_policy | manual_policy).anybits?(QUOTE_APPROVAL_POLICY_FLAGS[:unknown])
|
||||
|
||||
:denied
|
||||
end
|
||||
end
|
||||
@@ -45,16 +45,10 @@ class Status < ApplicationRecord
|
||||
include Status::SnapshotConcern
|
||||
include Status::ThreadingConcern
|
||||
include Status::Visibility
|
||||
include Status::InteractionPolicyConcern
|
||||
|
||||
MEDIA_ATTACHMENTS_LIMIT = 4
|
||||
|
||||
QUOTE_APPROVAL_POLICY_FLAGS = {
|
||||
unknown: (1 << 0),
|
||||
public: (1 << 1),
|
||||
followers: (1 << 2),
|
||||
followed: (1 << 3),
|
||||
}.freeze
|
||||
|
||||
rate_limit by: :account, family: :statuses
|
||||
|
||||
self.discard_column = :deleted_at
|
||||
|
||||
@@ -20,6 +20,11 @@ class StatusPolicy < ApplicationPolicy
|
||||
end
|
||||
end
|
||||
|
||||
# This is about requesting a quote post, not validating it
|
||||
def quote?
|
||||
record.quote_policy_for_account(current_account, preloaded_relations: @preloaded_relations) != :denied
|
||||
end
|
||||
|
||||
def reblog?
|
||||
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
include FormattingHelper
|
||||
|
||||
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message
|
||||
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes, :direct_message
|
||||
|
||||
attributes :id, :type, :summary,
|
||||
:in_reply_to, :published, :url,
|
||||
@@ -32,6 +32,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
|
||||
attribute :voters_count, if: :poll_and_voters_count?
|
||||
|
||||
attribute :quote, if: :quote?
|
||||
attribute :quote, key: :_misskey_quote, if: :quote?
|
||||
attribute :quote, key: :quote_uri, if: :quote?
|
||||
attribute :quote_authorization, if: :quote_authorization?
|
||||
|
||||
def id
|
||||
raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only]
|
||||
|
||||
@@ -206,6 +211,24 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
object.preloadable_poll&.voters_count
|
||||
end
|
||||
|
||||
def quote?
|
||||
object.quote&.present?
|
||||
end
|
||||
|
||||
def quote_authorization?
|
||||
object.quote&.approval_uri.present?
|
||||
end
|
||||
|
||||
def quote
|
||||
# TODO: handle inlining self-quotes
|
||||
ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status)
|
||||
end
|
||||
|
||||
def quote_authorization
|
||||
# TODO: approval of local quotes may work differently, perhaps?
|
||||
object.quote.approval_uri
|
||||
end
|
||||
|
||||
class MediaAttachmentSerializer < ActivityPub::Serializer
|
||||
context_extensions :blurhash, :focal_point
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
has_one :quote, key: :quote, serializer: REST::QuoteSerializer
|
||||
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||
has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
|
||||
|
||||
delegate :local?, to: :object
|
||||
|
||||
@@ -163,6 +164,14 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
object.active_mentions.to_a.sort_by(&:id)
|
||||
end
|
||||
|
||||
def quote_approval
|
||||
{
|
||||
automatic: object.quote_policy_as_keys(:automatic),
|
||||
manual: object.quote_policy_as_keys(:manual),
|
||||
current_user: object.quote_policy_for_account(current_user&.account),
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def relationships
|
||||
|
||||
@@ -112,13 +112,11 @@ class PostStatusService < BaseService
|
||||
# NOTE: for now this is only for convenience in testing, as we don't support the request flow nor serialize quotes in ActivityPub
|
||||
# we only support incoming quotes so far
|
||||
|
||||
status.quote = Quote.new(quoted_status: @quoted_status)
|
||||
status.quote.accept! if @status.account == @quoted_status.account || @quoted_status.active_mentions.exists?(mentions: { account_id: status.account_id })
|
||||
|
||||
# TODO: the following has yet to be implemented:
|
||||
# - handle approval of local users (requires the interactionPolicy PR)
|
||||
# - produce a QuoteAuthorization for quotes of local users
|
||||
# - send a QuoteRequest for quotes of remote users
|
||||
status.quote = Quote.create(quoted_status: @quoted_status, status: status)
|
||||
if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote?
|
||||
# TODO: produce a QuoteAuthorization
|
||||
status.quote.accept!
|
||||
end
|
||||
end
|
||||
|
||||
def safeguard_mentions!(status)
|
||||
@@ -162,6 +160,7 @@ class PostStatusService < BaseService
|
||||
DistributionWorker.perform_async(@status.id)
|
||||
ActivityPub::DistributionWorker.perform_async(@status.id) unless @status.local_only?
|
||||
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
|
||||
ActivityPub::QuoteRequestWorker.perform_async(@status.quote.id) if @status.quote&.quoted_status.present? && !@status.quote&.quoted_status&.local?
|
||||
end
|
||||
|
||||
def validate_media!
|
||||
|
||||
9
app/views/admin/accounts/_field.html.haml
Normal file
9
app/views/admin/accounts/_field.html.haml
Normal file
@@ -0,0 +1,9 @@
|
||||
-# locals: (field:, account:)
|
||||
%dl
|
||||
%dt.emojify{ title: field.name }
|
||||
= prerender_custom_emojis(h(field.name), account.emojis)
|
||||
%dd{ title: field.value, class: field_verified_class(field.verified?) }
|
||||
- if field.verified?
|
||||
%span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
|
||||
= material_symbol 'check'
|
||||
= prerender_custom_emojis(account_field_value_format(field, with_rel_me: false), account.emojis)
|
||||
@@ -7,25 +7,17 @@
|
||||
|
||||
= render 'application/card', account: @account
|
||||
|
||||
- account = @account
|
||||
- fields = account.fields
|
||||
- unless fields.empty? && account.note.blank?
|
||||
- if @account.fields? || @account.note?
|
||||
.admin-account-bio
|
||||
- unless fields.empty?
|
||||
- if @account.fields?
|
||||
%div
|
||||
.account__header__fields
|
||||
- fields.each do |field|
|
||||
%dl
|
||||
%dt.emojify{ title: field.name }= prerender_custom_emojis(h(field.name), account.emojis)
|
||||
%dd{ title: field.value, class: custom_field_classes(field) }
|
||||
- if field.verified?
|
||||
%span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
|
||||
= material_symbol 'check'
|
||||
= prerender_custom_emojis(account_field_value_format(field, with_rel_me: false), account.emojis)
|
||||
= render partial: 'field', collection: @account.fields, locals: { account: @account }
|
||||
|
||||
- if account.note.present?
|
||||
- if @account.note?
|
||||
%div
|
||||
.account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis)
|
||||
.account__header__content.emojify
|
||||
= prerender_custom_emojis(account_bio_format(@account), @account.emojis)
|
||||
|
||||
= render 'admin/accounts/counters', account: @account
|
||||
|
||||
|
||||
22
app/workers/activitypub/quote_request_worker.rb
Normal file
22
app/workers/activitypub/quote_request_worker.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::QuoteRequestWorker < ActivityPub::RawDistributionWorker
|
||||
def perform(quote_id)
|
||||
@quote = Quote.find(quote_id)
|
||||
@account = @quote.account
|
||||
|
||||
distribute!
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def inboxes
|
||||
@inboxes ||= [@quote.quoted_account&.inbox_url].compact
|
||||
end
|
||||
|
||||
def payload
|
||||
@payload ||= Oj.dump(serialize_payload(@quote, ActivityPub::QuoteRequestSerializer, signer: @account))
|
||||
end
|
||||
end
|
||||
@@ -17,10 +17,10 @@ class ActivityPub::StatusUpdateDistributionWorker < ActivityPub::DistributionWor
|
||||
|
||||
def activity
|
||||
ActivityPub::ActivityPresenter.new(
|
||||
id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @status.edited_at.to_i].join,
|
||||
id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @options[:updated_at]&.to_datetime&.to_i || @status.edited_at.to_i].join,
|
||||
type: 'Update',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(@status.account),
|
||||
published: @status.edited_at,
|
||||
published: @options[:updated_at]&.to_datetime || @status.edited_at,
|
||||
to: ActivityPub::TagManager.instance.to(@status),
|
||||
cc: ActivityPub::TagManager.instance.cc(@status),
|
||||
virtual_object: @status
|
||||
|
||||
@@ -22,11 +22,7 @@ class MentionResolveWorker
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
response = e.response
|
||||
|
||||
if response_error_unsalvageable?(response)
|
||||
# Give up
|
||||
else
|
||||
raise e
|
||||
end
|
||||
raise(e) unless response_error_unsalvageable?(response)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -20,10 +20,6 @@ class RedownloadAvatarWorker
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
response = e.response
|
||||
|
||||
if response_error_unsalvageable?(response)
|
||||
# Give up
|
||||
else
|
||||
raise e
|
||||
end
|
||||
raise(e) unless response_error_unsalvageable?(response)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,10 +20,6 @@ class RedownloadHeaderWorker
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
response = e.response
|
||||
|
||||
if response_error_unsalvageable?(response)
|
||||
# Give up
|
||||
else
|
||||
raise e
|
||||
end
|
||||
raise(e) unless response_error_unsalvageable?(response)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,10 +20,6 @@ class RedownloadMediaWorker
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
response = e.response
|
||||
|
||||
if response_error_unsalvageable?(response)
|
||||
# Give up
|
||||
else
|
||||
raise e
|
||||
end
|
||||
raise(e) unless response_error_unsalvageable?(response)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,10 +15,6 @@ class RemoteAccountRefreshWorker
|
||||
rescue Mastodon::UnexpectedResponseError => e
|
||||
response = e.response
|
||||
|
||||
if response_error_unsalvageable?(response)
|
||||
# Give up
|
||||
else
|
||||
raise e
|
||||
end
|
||||
raise(e) unless response_error_unsalvageable?(response)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1873,6 +1873,7 @@ en:
|
||||
edited_at_html: Edited %{date}
|
||||
errors:
|
||||
in_reply_not_found: The post you are trying to reply to does not appear to exist.
|
||||
quoted_status_not_found: The post you are trying to quote does not appear to exist.
|
||||
over_character_limit: character limit of %{max} exceeded
|
||||
pin_errors:
|
||||
direct: Posts that are only visible to mentioned users cannot be pinned
|
||||
|
||||
@@ -319,6 +319,7 @@ fr-CA:
|
||||
create: Créer une annonce
|
||||
title: Nouvelle annonce
|
||||
preview:
|
||||
disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur.
|
||||
explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :'
|
||||
title: Aperçu de la notification d'annonce
|
||||
publish: Publier
|
||||
|
||||
@@ -319,6 +319,7 @@ fr:
|
||||
create: Créer une annonce
|
||||
title: Nouvelle annonce
|
||||
preview:
|
||||
disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur.
|
||||
explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :'
|
||||
title: Aperçu de la notification d'annonce
|
||||
publish: Publier
|
||||
|
||||
@@ -1393,7 +1393,7 @@ ru:
|
||||
featured_tags:
|
||||
add_new: Добавить
|
||||
errors:
|
||||
limit: Вы уже добавили максимальное число хештегов
|
||||
limit: Вы достигли максимального количества хештегов, которые можно рекомендовать в профиле
|
||||
hint_html: "<strong>Рекомендуйте самые важные для вас хештеги в своём профиле.</strong> Это отличный инструмент для того, чтобы держать подписчиков в курсе ваших долгосрочных проектов и творческих работ. Рекомендации хештегов заметны в вашем профиле и предоставляют быстрый доступ к вашим постам."
|
||||
filters:
|
||||
contexts:
|
||||
@@ -1552,16 +1552,16 @@ ru:
|
||||
many: Вы собираетесь <strong>игнорировать</strong> <strong>%{count} пользователей</strong> из файла <strong>%{filename}</strong>.
|
||||
one: Вы собираетесь <strong>игнорировать</strong> <strong>%{count} пользователя</strong> из файла <strong>%{filename}</strong>.
|
||||
other: Вы собираетесь <strong>игнорировать</strong> <strong>%{count} пользователей</strong> из файла <strong>%{filename}</strong>.
|
||||
preface: Вы можете загрузить некоторые данные, например, списки людей, на которых Вы подписаны или которых блокируете, в Вашу учётную запись на этом узле из файлов, экспортированных с другого узла.
|
||||
recent_imports: Недавно импортированное
|
||||
preface: Вы можете перенести прежде экспортированные с другого сервера данные, такие как блокируемые вами пользователи и ваши подписки.
|
||||
recent_imports: История импорта
|
||||
states:
|
||||
finished: Готово
|
||||
in_progress: В процессе
|
||||
scheduled: Запланировано
|
||||
unconfirmed: Неподтвержденный
|
||||
status: Статус
|
||||
success: Ваши данные были успешно загружены и будут обработаны с должной скоростью
|
||||
time_started: Началось в
|
||||
finished: Завершён
|
||||
in_progress: Выполняется
|
||||
scheduled: Запланирован
|
||||
unconfirmed: Не подтверждён
|
||||
status: Состояние
|
||||
success: Ваши данные были загружены и в скором времени будут обработаны
|
||||
time_started: Начат
|
||||
titles:
|
||||
blocking: Импорт списка заблокированных пользователей
|
||||
bookmarks: Импорт закладок
|
||||
@@ -1572,7 +1572,7 @@ ru:
|
||||
type: Тип импорта
|
||||
type_groups:
|
||||
constructive: Подписки и закладки
|
||||
destructive: Блокировки и игнорируемые
|
||||
destructive: Чёрный список
|
||||
types:
|
||||
blocking: Заблокированные пользователи
|
||||
bookmarks: Закладки
|
||||
@@ -1583,7 +1583,7 @@ ru:
|
||||
upload: Загрузить
|
||||
invites:
|
||||
delete: Удалить
|
||||
expired: Истекло
|
||||
expired: Срок действия истёк
|
||||
expires_in:
|
||||
'1800': 30 минут
|
||||
'21600': 6 часов
|
||||
@@ -1592,33 +1592,32 @@ ru:
|
||||
'604800': 1 неделя
|
||||
'86400': 1 день
|
||||
expires_in_prompt: Бессрочно
|
||||
generate: Сгенерировать
|
||||
generate: Создать ссылку
|
||||
invalid: Это приглашение недействительно
|
||||
invited_by: 'Вас пригласил(а):'
|
||||
invited_by: 'Вы были приглашены этим пользователем:'
|
||||
max_uses:
|
||||
few: "%{count} раза"
|
||||
many: "%{count} раз"
|
||||
one: "%{count} раз"
|
||||
other: "%{count} раза"
|
||||
other: "%{count} раз"
|
||||
max_uses_prompt: Без ограничения
|
||||
prompt: Создавайте и делитесь ссылками с другими, чтобы предоставить им доступом к этому узлу.
|
||||
prompt: Создавайте приглашения и делитесь ими с другими людьми, чтобы они могли зарегистрироваться на этом сервере
|
||||
table:
|
||||
expires_at: Истекает
|
||||
uses: Исп.
|
||||
title: Пригласить людей
|
||||
uses: Регистрации
|
||||
title: Приглашения
|
||||
lists:
|
||||
errors:
|
||||
limit: Вы достигли максимального количества пользователей
|
||||
limit: Вы достигли максимального количества списков
|
||||
login_activities:
|
||||
authentication_methods:
|
||||
otp: приложение двухфакторной аутентификации
|
||||
password: пароль
|
||||
sign_in_token: код безопасности электронной почты
|
||||
webauthn: ключи безопасности
|
||||
description_html: Если вы видите неопознанное действие, смените пароль и/или включите двухфакторную авторизацию.
|
||||
empty: Нет доступной истории входов
|
||||
failed_sign_in_html: Неудачная попытка входа используя %{method} через %{browser} (%{ip})
|
||||
successful_sign_in_html: Успешный вход используя %{method} через %{browser} (%{ip})
|
||||
otp: приложения двухфакторной аутентификации
|
||||
password: пароля
|
||||
webauthn: электронного ключа
|
||||
description_html: Если вы заметили действия, которых не совершали, вам следует сменить пароль и включить двухфакторную аутентификацию.
|
||||
empty: История входов отсутствует
|
||||
failed_sign_in_html: Неудачная попытка входа при помощи %{method} с IP-адреса %{ip} (%{browser})
|
||||
successful_sign_in_html: Вход при помощи %{method} с IP-адреса %{ip} (%{browser})
|
||||
title: История входов
|
||||
mail_subscriptions:
|
||||
unsubscribe:
|
||||
|
||||
@@ -231,8 +231,8 @@ el:
|
||||
setting_always_send_emails: Πάντα να αποστέλλονται ειδοποίησεις μέσω email
|
||||
setting_auto_play_gif: Αυτόματη αναπαραγωγή των GIF
|
||||
setting_boost_modal: Επιβεβαίωση πριν την προώθηση
|
||||
setting_default_language: Γλώσσα δημοσιεύσεων
|
||||
setting_default_privacy: Ιδιωτικότητα δημοσιεύσεων
|
||||
setting_default_language: Γλώσσα κατά την ανάρτηση
|
||||
setting_default_privacy: Ιδιωτικότητα αναρτήσεων
|
||||
setting_default_quote_policy: Ποιος μπορεί να παραθέσει
|
||||
setting_default_sensitive: Σημείωση όλων των πολυμέσων ως ευαίσθητου περιεχομένου
|
||||
setting_delete_modal: Επιβεβαίωση πριν τη διαγραφή ενός τουτ
|
||||
|
||||
@@ -6,6 +6,7 @@ RSpec.describe AccountableConcern do
|
||||
let(:hoge_class) do
|
||||
Class.new do
|
||||
include AccountableConcern
|
||||
|
||||
attr_reader :current_account
|
||||
|
||||
def initialize(current_account)
|
||||
|
||||
@@ -75,23 +75,19 @@ RSpec.describe HomeHelper do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'custom_field_classes' do
|
||||
context 'with a verified field' do
|
||||
let(:field) { instance_double(Account::Field, verified?: true) }
|
||||
describe 'field_verified_class' do
|
||||
subject { helper.field_verified_class(verified) }
|
||||
|
||||
it 'returns verified string' do
|
||||
result = helper.custom_field_classes(field)
|
||||
expect(result).to eq 'verified'
|
||||
end
|
||||
context 'with a verified field' do
|
||||
let(:verified) { true }
|
||||
|
||||
it { is_expected.to eq('verified') }
|
||||
end
|
||||
|
||||
context 'with a non-verified field' do
|
||||
let(:field) { instance_double(Account::Field, verified?: false) }
|
||||
let(:verified) { false }
|
||||
|
||||
it 'returns verified string' do
|
||||
result = helper.custom_field_classes(field)
|
||||
expect(result).to eq 'emojify'
|
||||
end
|
||||
it { is_expected.to eq('emojify') }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::Activity::Accept do
|
||||
let(:sender) { Fabricate(:account) }
|
||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||
let(:recipient) { Fabricate(:account) }
|
||||
|
||||
describe '#perform' do
|
||||
@@ -48,5 +48,128 @@ RSpec.describe ActivityPub::Activity::Accept do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a QuoteRequest' do
|
||||
let(:status) { Fabricate(:status, account: recipient) }
|
||||
let(:quoted_status) { Fabricate(:status, account: sender) }
|
||||
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status) }
|
||||
let(:approval_uri) { "https://#{sender.domain}/approvals/1" }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
|
||||
},
|
||||
],
|
||||
id: 'foo',
|
||||
type: 'Accept',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: {
|
||||
id: quote.activity_uri,
|
||||
type: 'QuoteRequest',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
instrument: ActivityPub::TagManager.instance.uri_for(status),
|
||||
},
|
||||
result: approval_uri,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'marks the quote as approved and distribute an update' do
|
||||
expect { subject.perform }
|
||||
.to change { quote.reload.accepted? }.from(false).to(true)
|
||||
.and change { quote.reload.approval_uri }.to(approval_uri)
|
||||
expect(DistributionWorker)
|
||||
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
|
||||
expect(ActivityPub::StatusUpdateDistributionWorker)
|
||||
.to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) })
|
||||
end
|
||||
|
||||
context 'when the quoted status is not from the sender of the Accept' do
|
||||
let(:quoted_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) }
|
||||
|
||||
it 'does not mark the quote as approved and does not distribute an update' do
|
||||
expect { subject.perform }
|
||||
.to not_change { quote.reload.accepted? }.from(false)
|
||||
.and not_change { quote.reload.approval_uri }.from(nil)
|
||||
expect(DistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
|
||||
expect(ActivityPub::StatusUpdateDistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, anything)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the quoting status is from an unrelated user' do
|
||||
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foobar.com')) }
|
||||
|
||||
it 'does not mark the quote as approved and does not distribute an update' do
|
||||
expect { subject.perform }
|
||||
.to not_change { quote.reload.accepted? }.from(false)
|
||||
.and not_change { quote.reload.approval_uri }.from(nil)
|
||||
expect(DistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
|
||||
expect(ActivityPub::StatusUpdateDistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, anything)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when approval_uri is missing' do
|
||||
let(:approval_uri) { nil }
|
||||
|
||||
it 'does not mark the quote as approved and does not distribute an update' do
|
||||
expect { subject.perform }
|
||||
.to not_change { quote.reload.accepted? }.from(false)
|
||||
.and not_change { quote.reload.approval_uri }.from(nil)
|
||||
expect(DistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
|
||||
expect(ActivityPub::StatusUpdateDistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, anything)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the QuoteRequest is referenced by its identifier' do
|
||||
let(:json) do
|
||||
{
|
||||
'@context': [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
{
|
||||
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
|
||||
},
|
||||
],
|
||||
id: 'foo',
|
||||
type: 'Accept',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: quote.activity_uri,
|
||||
result: approval_uri,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'marks the quote as approved and distribute an update' do
|
||||
expect { subject.perform }
|
||||
.to change { quote.reload.accepted? }.from(false).to(true)
|
||||
.and change { quote.reload.approval_uri }.to(approval_uri)
|
||||
expect(DistributionWorker)
|
||||
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
|
||||
expect(ActivityPub::StatusUpdateDistributionWorker)
|
||||
.to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) })
|
||||
end
|
||||
|
||||
context 'when approval_uri is missing' do
|
||||
let(:approval_uri) { nil }
|
||||
|
||||
it 'does not mark the quote as approved and does not distribute an update' do
|
||||
expect { subject.perform }
|
||||
.to not_change { quote.reload.accepted? }.from(false)
|
||||
.and not_change { quote.reload.approval_uri }.from(nil)
|
||||
expect(DistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
|
||||
expect(ActivityPub::StatusUpdateDistributionWorker)
|
||||
.to_not have_enqueued_sidekiq_job(status.id, anything)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -125,5 +125,27 @@ RSpec.describe ActivityPub::Activity::Reject do
|
||||
expect(relay.reload.rejected?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a QuoteRequest' do
|
||||
let(:status) { Fabricate(:status, account: recipient) }
|
||||
let(:quoted_status) { Fabricate(:status, account: sender) }
|
||||
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'https://abc-123/456') }
|
||||
let(:approval_uri) { "https://#{sender.domain}/approvals/1" }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
id: 'https://abc-123/456',
|
||||
type: 'QuoteRequest',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(recipient),
|
||||
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
instrument: ActivityPub::TagManager.instance.uri_for(status),
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'marks the quote as rejected' do
|
||||
expect { subject.perform }
|
||||
.to change { quote.reload.rejected? }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,6 +28,18 @@ RSpec.describe StatusCacheHydrator do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling a status with a quote policy', feature: :outgoing_quotes do
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
||||
|
||||
before do
|
||||
account.follow!(status.account)
|
||||
end
|
||||
|
||||
it 'renders the same attributes as a full render' do
|
||||
expect(subject).to eql(compare_to_hash)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when handling a filtered status' do
|
||||
let(:status) { Fabricate(:status, text: 'this toot is about that banned word') }
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Status::InteractionPolicyConcern do
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: (0b0101 << 16) | 0b0010) }
|
||||
|
||||
describe '#quote_policy_as_keys' do
|
||||
it 'returns the expected values' do
|
||||
expect(status.quote_policy_as_keys(:automatic)).to eq ['unknown', 'followers']
|
||||
expect(status.quote_policy_as_keys(:manual)).to eq ['public']
|
||||
end
|
||||
end
|
||||
|
||||
describe '#quote_policy_for_account' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
context 'when the account is not following the user' do
|
||||
it 'returns :manual because of the public entry in the manual policy' do
|
||||
expect(status.quote_policy_for_account(account)).to eq :manual
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account is following the user' do
|
||||
before do
|
||||
account.follow!(status.account)
|
||||
end
|
||||
|
||||
it 'returns :automatic because of the followers entry in the automatic policy' do
|
||||
expect(status.quote_policy_for_account(account)).to eq :automatic
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the account falls into the unknown bucket' do
|
||||
let(:status) { Fabricate(:status, quote_approval_policy: (0b0001 << 16) | 0b0100) }
|
||||
|
||||
it 'returns :automatic because of the followers entry in the automatic policy' do
|
||||
expect(status.quote_policy_for_account(account)).to eq :unknown
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -98,6 +98,92 @@ RSpec.describe StatusPolicy, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the permission of quote?' do
|
||||
permissions :quote? do
|
||||
it 'grants access when direct and account is viewer' do
|
||||
status.visibility = :direct
|
||||
|
||||
expect(subject).to permit(status.account, status)
|
||||
end
|
||||
|
||||
it 'grants access when direct and viewer is mentioned' do
|
||||
status.visibility = :direct
|
||||
status.mentions = [Fabricate(:mention, account: alice)]
|
||||
|
||||
expect(subject).to permit(alice, status)
|
||||
end
|
||||
|
||||
it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do
|
||||
status.visibility = :direct
|
||||
status.mentions = [Fabricate(:mention, account: bob)]
|
||||
status.active_mentions.load
|
||||
|
||||
expect(subject).to permit(bob, status)
|
||||
end
|
||||
|
||||
it 'denies access when direct and viewer is not mentioned' do
|
||||
viewer = Fabricate(:account)
|
||||
status.visibility = :direct
|
||||
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'denies access when private and viewer is not mentioned' do
|
||||
viewer = Fabricate(:account)
|
||||
status.visibility = :private
|
||||
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'grants access when private and viewer is mentioned' do
|
||||
status.visibility = :private
|
||||
status.mentions = [Fabricate(:mention, account: bob)]
|
||||
|
||||
expect(subject).to permit(bob, status)
|
||||
end
|
||||
|
||||
it 'denies access when private and non-viewer is mentioned' do
|
||||
viewer = Fabricate(:account)
|
||||
status.visibility = :private
|
||||
status.mentions = [Fabricate(:mention, account: bob)]
|
||||
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'denies access when private and account is following viewer' do
|
||||
follow = Fabricate(:follow)
|
||||
status.visibility = :private
|
||||
status.account = follow.target_account
|
||||
|
||||
expect(subject).to_not permit(follow.account, status)
|
||||
end
|
||||
|
||||
it 'denies access when public but policy does not allow anyone' do
|
||||
viewer = Fabricate(:account)
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'grants access when public and policy allows everyone' do
|
||||
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]
|
||||
viewer = Fabricate(:account)
|
||||
expect(subject).to permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'denies access when public and policy allows followers but viewer is not one' do
|
||||
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
|
||||
viewer = Fabricate(:account)
|
||||
expect(subject).to_not permit(viewer, status)
|
||||
end
|
||||
|
||||
it 'grants access when public and policy allows followers and viewer is one' do
|
||||
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
|
||||
viewer = Fabricate(:account)
|
||||
viewer.follow!(status.account)
|
||||
expect(subject).to permit(viewer, status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with the permission of reblog?' do
|
||||
permissions :reblog? do
|
||||
it 'denies access when private' do
|
||||
|
||||
@@ -158,6 +158,27 @@ RSpec.describe '/api/v1/statuses' do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a self-quote post', feature: :outgoing_quotes do
|
||||
let(:quoted_status) { Fabricate(:status, account: user.account) }
|
||||
let(:params) do
|
||||
{
|
||||
status: 'Hello world, this is a self-quote',
|
||||
quoted_status_id: quoted_status.id,
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns a quote post, as well as rate limit headers', :aggregate_failures do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
expect(response.parsed_body[:quote]).to be_present
|
||||
expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
|
||||
expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a safeguard' do
|
||||
let!(:alice) { Fabricate(:account, username: 'alice') }
|
||||
let!(:bob) { Fabricate(:account, username: 'bob') }
|
||||
|
||||
@@ -49,6 +49,7 @@ RSpec.describe 'Routes under accounts/' do
|
||||
|
||||
context 'with local username encoded at' do
|
||||
include RSpec::Rails::RequestExampleGroup
|
||||
|
||||
let(:username) { 'alice' }
|
||||
|
||||
it 'routes /%40:username' do
|
||||
@@ -140,6 +141,7 @@ RSpec.describe 'Routes under accounts/' do
|
||||
|
||||
context 'with remote username encoded at' do
|
||||
include RSpec::Rails::RequestExampleGroup
|
||||
|
||||
let(:username) { 'alice%40example.com' }
|
||||
let(:username_decoded) { 'alice@example.com' }
|
||||
|
||||
|
||||
@@ -41,4 +41,20 @@ RSpec.describe ActivityPub::NoteSerializer do
|
||||
.and(not_include(reply_by_other_first.uri)) # Replies from others
|
||||
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
|
||||
end
|
||||
|
||||
context 'with a quote' do
|
||||
let(:quoted_status) { Fabricate(:status) }
|
||||
let(:approval_uri) { 'https://example.com/foo/bar' }
|
||||
let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, approval_uri: approval_uri) }
|
||||
|
||||
it 'has the expected shape' do
|
||||
expect(subject).to include({
|
||||
'type' => 'Note',
|
||||
'quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
|
||||
'quoteUri' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
|
||||
'_misskey_quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
|
||||
'quoteAuthorization' => approval_uri,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -291,6 +291,14 @@ RSpec.describe PostStatusService do
|
||||
)
|
||||
end
|
||||
|
||||
it 'correctly requests a quote for remote posts' do
|
||||
account = Fabricate(:account)
|
||||
quoted_status = Fabricate(:status, account: Fabricate(:account, domain: 'example.com'))
|
||||
|
||||
expect { subject.call(account, text: 'test', quoted_status: quoted_status) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::QuoteRequestWorker)
|
||||
end
|
||||
|
||||
it 'returns existing status when used twice with idempotency key' do
|
||||
account = Fabricate(:account)
|
||||
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')
|
||||
|
||||
@@ -6,6 +6,7 @@ RSpec.describe ExistingUsernameValidator do
|
||||
let(:record_class) do
|
||||
Class.new do
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :contact, :friends
|
||||
|
||||
def self.name
|
||||
|
||||
@@ -6,6 +6,7 @@ RSpec.describe LanguageValidator do
|
||||
let(:record_class) do
|
||||
Class.new do
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :locale
|
||||
|
||||
validates :locale, language: true
|
||||
|
||||
@@ -6,6 +6,7 @@ RSpec.describe UnreservedUsernameValidator do
|
||||
let(:record_class) do
|
||||
Class.new do
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :username
|
||||
|
||||
validates_with UnreservedUsernameValidator
|
||||
|
||||
@@ -6,6 +6,7 @@ RSpec.describe URLValidator do
|
||||
let(:record_class) do
|
||||
Class.new do
|
||||
include ActiveModel::Validations
|
||||
|
||||
attr_accessor :profile
|
||||
|
||||
validates :profile, url: true
|
||||
|
||||
30
spec/workers/activitypub/quote_request_worker_spec.rb
Normal file
30
spec/workers/activitypub/quote_request_worker_spec.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ActivityPub::QuoteRequestWorker do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:quoted_account) { Fabricate(:account, inbox_url: 'http://example.com', domain: 'example.com') }
|
||||
let(:quoted_status) { Fabricate(:status, account: quoted_account) }
|
||||
let(:status) { Fabricate(:status, text: 'foo') }
|
||||
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'TODO') } # TODO: activity URI
|
||||
|
||||
describe '#perform' do
|
||||
it 'sends the expected QuoteRequest activity' do
|
||||
subject.perform(quote.id)
|
||||
|
||||
expect(ActivityPub::DeliveryWorker)
|
||||
.to have_enqueued_sidekiq_job(match_object_shape, quote.account_id, 'http://example.com', {})
|
||||
end
|
||||
|
||||
def match_object_shape
|
||||
match_json_values(
|
||||
type: 'QuoteRequest',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(quote.account),
|
||||
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
instrument: anything # TODO: inline post in request?
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,6 +9,7 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
|
||||
let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com', domain: 'example.com') }
|
||||
|
||||
describe '#perform' do
|
||||
context 'with an explicitly edited status' do
|
||||
before do
|
||||
follower.follow!(status.account)
|
||||
|
||||
@@ -25,9 +26,8 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
|
||||
subject.perform(status.id)
|
||||
end
|
||||
expect { subject.perform(status.id) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -37,8 +37,36 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
|
||||
subject.perform(status.id)
|
||||
expect { subject.perform(status.id) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an implicitly edited status' do
|
||||
before do
|
||||
follower.follow!(status.account)
|
||||
end
|
||||
|
||||
context 'with public status' do
|
||||
before do
|
||||
status.update(visibility: :public)
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with private status' do
|
||||
before do
|
||||
status.update(visibility: :private)
|
||||
end
|
||||
|
||||
it 'delivers to followers' do
|
||||
expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) }
|
||||
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -189,29 +189,30 @@ async function findEntrypoints() {
|
||||
const entrypoints: Record<string, string> = {};
|
||||
|
||||
// First, JS entrypoints
|
||||
const jsEntrypoints = await readdir(path.resolve(jsRoot, 'entrypoints'), {
|
||||
const jsEntrypointsDir = path.resolve(jsRoot, 'entrypoints');
|
||||
const jsEntrypoints = await readdir(jsEntrypointsDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const jsExtTest = /\.[jt]sx?$/;
|
||||
for (const file of jsEntrypoints) {
|
||||
if (file.isFile() && jsExtTest.test(file.name)) {
|
||||
entrypoints[file.name.replace(jsExtTest, '')] = path.resolve(
|
||||
file.parentPath,
|
||||
jsEntrypointsDir,
|
||||
file.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Next, SCSS entrypoints
|
||||
const scssEntrypoints = await readdir(
|
||||
path.resolve(jsRoot, 'styles/entrypoints'),
|
||||
{ withFileTypes: true },
|
||||
);
|
||||
const scssEntrypointsDir = path.resolve(jsRoot, 'styles/entrypoints');
|
||||
const scssEntrypoints = await readdir(scssEntrypointsDir, {
|
||||
withFileTypes: true,
|
||||
});
|
||||
const scssExtTest = /\.s?css$/;
|
||||
for (const file of scssEntrypoints) {
|
||||
if (file.isFile() && scssExtTest.test(file.name)) {
|
||||
entrypoints[file.name.replace(scssExtTest, '')] = path.resolve(
|
||||
file.parentPath,
|
||||
scssEntrypointsDir,
|
||||
file.name,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user