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:
Claire
2025-07-25 21:45:00 +02:00
58 changed files with 800 additions and 139 deletions

View File

@@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp` # `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 # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new

View File

@@ -765,7 +765,7 @@ GEM
rspec-mocks (~> 3.0) rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9) sidekiq (>= 5, < 9)
rspec-support (3.13.4) rspec-support (3.13.4)
rubocop (1.78.0) rubocop (1.79.0)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@@ -773,8 +773,9 @@ GEM
parser (>= 3.3.0.2) parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.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) ruby-progressbar (~> 1.7)
tsort (>= 0.2.0)
unicode-display_width (>= 2.4.0, < 4.0) unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.46.0) rubocop-ast (1.46.0)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
@@ -880,6 +881,7 @@ GEM
bindata (~> 2.4) bindata (~> 2.4)
openssl (> 2.0) openssl (> 2.0)
openssl-signature_algorithm (~> 1.0) openssl-signature_algorithm (~> 1.0)
tsort (0.2.0)
tty-color (0.6.0) tty-color (0.6.0)
tty-cursor (0.7.1) tty-cursor (0.7.1)
tty-prompt (0.23.1) tty-prompt (0.23.1)

View File

@@ -2,6 +2,7 @@
class Api::V1::Admin::TagsController < Api::BaseController class Api::V1::Admin::TagsController < Api::BaseController
include Authorization include Authorization
before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show] before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write' }, only: :update before_action -> { authorize_if_got_token! :'admin:write' }, only: :update

View File

@@ -10,6 +10,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action :set_statuses, only: [:index] before_action :set_statuses, only: [:index]
before_action :set_status, only: [:show, :context] before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create] before_action :set_thread, only: [:create]
before_action :set_quoted_status, only: [:create]
before_action :check_statuses_limit, only: [:index] before_action :check_statuses_limit, only: [:index]
override_rate_limit_headers :create, family: :statuses override_rate_limit_headers :create, family: :statuses
@@ -76,6 +77,7 @@ class Api::V1::StatusesController < Api::BaseController
current_user.account, current_user.account,
text: status_params[:status], text: status_params[:status],
thread: @thread, thread: @thread,
quoted_status: @quoted_status,
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text], 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 render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end 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 def check_statuses_limit
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
end end
@@ -165,6 +177,7 @@ class Api::V1::StatusesController < Api::BaseController
params.permit( params.permit(
:status, :status,
:in_reply_to_id, :in_reply_to_id,
:quoted_status_id,
:sensitive, :sensitive,
:spoiler_text, :spoiler_text,
:visibility, :visibility,

View File

@@ -49,8 +49,8 @@ module HomeHelper
end end
end end
def custom_field_classes(field) def field_verified_class(verified)
if field.verified? if verified
'verified' 'verified'
else else
'emojify' 'emojify'

View File

@@ -558,6 +558,8 @@
"status.bookmark": "Ouzhpennañ d'ar sinedoù", "status.bookmark": "Ouzhpennañ d'ar sinedoù",
"status.cancel_reblog_private": "Nac'hañ ar skignadenn", "status.cancel_reblog_private": "Nac'hañ ar skignadenn",
"status.cannot_reblog": "Ar c'hannad-se na c'hall ket bezañ skignet", "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.copy": "Eilañ liamm ar c'hannad",
"status.delete": "Dilemel", "status.delete": "Dilemel",
"status.detailed_status": "Gwel kaozeadenn munudek", "status.detailed_status": "Gwel kaozeadenn munudek",

View File

@@ -43,7 +43,7 @@
"account.followers": "Follower", "account.followers": "Follower",
"account.followers.empty": "Diesem Profil folgt noch niemand.", "account.followers.empty": "Diesem Profil folgt noch niemand.",
"account.followers_counter": "{count, plural, one {{counter} Follower} other {{counter} Follower}}", "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": "Folge ich",
"account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}", "account.following_counter": "{count, plural, one {{counter} Folge ich} other {{counter} Folge ich}}",
"account.follows.empty": "Dieses Profil folgt noch niemandem.", "account.follows.empty": "Dieses Profil folgt noch niemandem.",
@@ -63,7 +63,7 @@
"account.mute_short": "Stummschalten", "account.mute_short": "Stummschalten",
"account.muted": "Stummgeschaltet", "account.muted": "Stummgeschaltet",
"account.muting": "Stummgeschaltet", "account.muting": "Stummgeschaltet",
"account.mutual": "Ihr folgt einander", "account.mutual": "Ihr folgt euch",
"account.no_bio": "Keine Beschreibung verfügbar.", "account.no_bio": "Keine Beschreibung verfügbar.",
"account.open_original_page": "Ursprüngliche Seite öffnen", "account.open_original_page": "Ursprüngliche Seite öffnen",
"account.posts": "Beiträge", "account.posts": "Beiträge",
@@ -225,7 +225,7 @@
"confirmations.discard_draft.edit.title": "Änderungen an diesem Beitrag verwerfen?", "confirmations.discard_draft.edit.title": "Änderungen an diesem Beitrag verwerfen?",
"confirmations.discard_draft.post.cancel": "Entwurf fortsetzen", "confirmations.discard_draft.post.cancel": "Entwurf fortsetzen",
"confirmations.discard_draft.post.message": "Beim Fortfahren wird der gerade verfasste Beitrag verworfen.", "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.confirm": "Verwerfen",
"confirmations.discard_edit_media.message": "Du hast Änderungen an der Medienbeschreibung oder -vorschau vorgenommen, die noch nicht gespeichert sind. Trotzdem 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", "confirmations.follow_to_list.confirm": "Folgen und zur Liste hinzufügen",

View File

@@ -612,7 +612,7 @@
"notification.moderation_warning.action_suspend": "Ο λογαριασμός σου έχει ανασταλεί.", "notification.moderation_warning.action_suspend": "Ο λογαριασμός σου έχει ανασταλεί.",
"notification.own_poll": "Η δημοσκόπησή σου έληξε", "notification.own_poll": "Η δημοσκόπησή σου έληξε",
"notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει", "notification.poll": "Μία ψηφοφορία στην οποία συμμετείχες έχει τελειώσει",
"notification.reblog": "Ο/Η {name} ενίσχυσε τη δημοσίευσή σου", "notification.reblog": "Ο/Η {name} ενίσχυσε την ανάρτηση σου",
"notification.reblog.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> ενίσχυσαν την ανάρτησή σου", "notification.reblog.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> ενίσχυσαν την ανάρτησή σου",
"notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}", "notification.relationships_severance_event": "Χάθηκε η σύνδεση με το {name}",
"notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.", "notification.relationships_severance_event.account_suspension": "Ένας διαχειριστής από το {from} ανέστειλε το {target}, πράγμα που σημαίνει ότι δεν μπορείς πλέον να λαμβάνεις ενημερώσεις από αυτούς ή να αλληλεπιδράς μαζί τους.",
@@ -845,6 +845,8 @@
"status.bookmark": "Σελιδοδείκτης", "status.bookmark": "Σελιδοδείκτης",
"status.cancel_reblog_private": "Ακύρωση ενίσχυσης", "status.cancel_reblog_private": "Ακύρωση ενίσχυσης",
"status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί", "status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί",
"status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες",
"status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις",
"status.continued_thread": "Συνεχιζόμενο νήματος", "status.continued_thread": "Συνεχιζόμενο νήματος",
"status.copy": "Αντιγραφή συνδέσμου ανάρτησης", "status.copy": "Αντιγραφή συνδέσμου ανάρτησης",
"status.delete": "Διαγραφή", "status.delete": "Διαγραφή",

View File

@@ -612,7 +612,7 @@
"notification.moderation_warning.action_suspend": "Your account has been suspended.", "notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended", "notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you voted in 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.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": "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.", "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.",

View File

@@ -845,6 +845,8 @@
"status.bookmark": "Leabharmharcanna", "status.bookmark": "Leabharmharcanna",
"status.cancel_reblog_private": "Dímhol", "status.cancel_reblog_private": "Dímhol",
"status.cannot_reblog": "Ní féidir an phostáil seo a mholadh", "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.continued_thread": "Snáithe ar lean",
"status.copy": "Cóipeáil an nasc chuig an bpostáil", "status.copy": "Cóipeáil an nasc chuig an bpostáil",
"status.delete": "Scrios", "status.delete": "Scrios",

View File

@@ -1,6 +1,7 @@
{ {
"about.blocks": "Модерацияланған серверлер", "about.blocks": "Модерацияланған серверлер",
"about.contact": "Байланыс:", "about.contact": "Байланыс:",
"about.default_locale": "Әдепкі",
"about.disclaimer": "Mastodon деген тегін, бастапқы коды ашық бағдарламалық жасақтама және Mastodon gGmbH-тің сауда маркасы.", "about.disclaimer": "Mastodon деген тегін, бастапқы коды ашық бағдарламалық жасақтама және Mastodon gGmbH-тің сауда маркасы.",
"about.domain_blocks.no_reason_available": "Себеп қолжетімсіз", "about.domain_blocks.no_reason_available": "Себеп қолжетімсіз",
"about.domain_blocks.preamble": "Mastodon әдетте сізге Fediverse'тің кез келген серверінің қолданушыларының контентін көріп, олармен байланысуға мүмкіндік береді. Осы белгілі серверде жасалған ережеден тыс жағдайлар міне.", "about.domain_blocks.preamble": "Mastodon әдетте сізге Fediverse'тің кез келген серверінің қолданушыларының контентін көріп, олармен байланысуға мүмкіндік береді. Осы белгілі серверде жасалған ережеден тыс жағдайлар міне.",

View File

@@ -845,6 +845,8 @@
"status.bookmark": "Guardar nos marcadores", "status.bookmark": "Guardar nos marcadores",
"status.cancel_reblog_private": "Retirar impulso", "status.cancel_reblog_private": "Retirar impulso",
"status.cannot_reblog": "Esta publicação não pode ser impulsionada", "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.continued_thread": "Continuação da conversa",
"status.copy": "Copiar hiperligação da publicação", "status.copy": "Copiar hiperligação da publicação",
"status.delete": "Eliminar", "status.delete": "Eliminar",

View File

@@ -1,6 +1,7 @@
{ {
"about.blocks": "Serbidores moderados", "about.blocks": "Serbidores moderados",
"about.contact": "Cuntatu:", "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.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.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.", "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.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.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.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.not_available": "Custa informatzione no est istada posta a disponimentu in custu serbidore.",
"about.powered_by": "Rete sotziale detzentralizada impulsada dae {mastodon}", "about.powered_by": "Rete sotziale detzentralizada impulsada dae {mastodon}",
"about.rules": "Règulas de su serbidore", "about.rules": "Règulas de su serbidore",
@@ -19,13 +21,21 @@
"account.block_domain": "Bloca su domìniu {domain}", "account.block_domain": "Bloca su domìniu {domain}",
"account.block_short": "Bloca", "account.block_short": "Bloca",
"account.blocked": "Blocadu", "account.blocked": "Blocadu",
"account.blocking": "Blocadu",
"account.cancel_follow_request": "Annulla sa sighidura", "account.cancel_follow_request": "Annulla sa sighidura",
"account.copy": "Còpia su ligòngiu a su profilu", "account.copy": "Còpia su ligòngiu a su profilu",
"account.direct": "Mèntova a @{name} in privadu", "account.direct": "Mèntova a @{name} in privadu",
"account.disable_notifications": "Non mi notìfiches prus cando @{name} pùblichet messàgios", "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.edit_profile": "Modìfica profilu",
"account.enable_notifications": "Notìfica·mi cando @{name} pùblicat messàgios", "account.enable_notifications": "Notìfica·mi cando @{name} pùblicat messàgios",
"account.endorse": "Cussìgia in su profilu tuo", "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_at": "Ùrtima publicatzione in su {date}",
"account.featured_tags.last_status_never": "Peruna publicatzione", "account.featured_tags.last_status_never": "Peruna publicatzione",
"account.follow": "Sighi", "account.follow": "Sighi",
@@ -33,9 +43,11 @@
"account.followers": "Sighiduras", "account.followers": "Sighiduras",
"account.followers.empty": "Nemos sighit ancora custa persone.", "account.followers.empty": "Nemos sighit ancora custa persone.",
"account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}", "account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}",
"account.followers_you_know_counter": "{counter} chi connosches",
"account.following": "Sighende", "account.following": "Sighende",
"account.following_counter": "{count, plural, one {sighende a {counter}} other {sighende a {counter}}}", "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.empty": "Custa persone non sighit ancora a nemos.",
"account.follows_you": "Ti sighit",
"account.go_to_profile": "Bae a su profilu", "account.go_to_profile": "Bae a su profilu",
"account.hide_reblogs": "Cua is cumpartziduras de @{name}", "account.hide_reblogs": "Cua is cumpartziduras de @{name}",
"account.in_memoriam": "In memoriam.", "account.in_memoriam": "In memoriam.",
@@ -50,18 +62,22 @@
"account.mute_notifications_short": "Pone is notìficas a sa muda", "account.mute_notifications_short": "Pone is notìficas a sa muda",
"account.mute_short": "A sa muda", "account.mute_short": "A sa muda",
"account.muted": "A sa muda", "account.muted": "A sa muda",
"account.muting": "A sa muda",
"account.no_bio": "Peruna descritzione frunida.", "account.no_bio": "Peruna descritzione frunida.",
"account.open_original_page": "Aberi sa pàgina originale", "account.open_original_page": "Aberi sa pàgina originale",
"account.posts": "Publicatziones", "account.posts": "Publicatziones",
"account.posts_with_replies": "Publicatziones e rispostas", "account.posts_with_replies": "Publicatziones e rispostas",
"account.remove_from_followers": "Cantzella a {name} dae is sighiduras",
"account.report": "Signala @{name}", "account.report": "Signala @{name}",
"account.requested": "Abetende s'aprovatzione. Incarca pro annullare sa rechesta de sighidura", "account.requested": "Abetende s'aprovatzione. Incarca pro annullare sa rechesta de sighidura",
"account.requested_follow": "{name} at dimandadu de ti sighire", "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.share": "Cumpartzi su profilu de @{name}",
"account.show_reblogs": "Ammustra is cumpartziduras de @{name}", "account.show_reblogs": "Ammustra is cumpartziduras de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}", "account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
"account.unblock": "Isbloca a @{name}", "account.unblock": "Isbloca a @{name}",
"account.unblock_domain": "Isbloca su domìniu {domain}", "account.unblock_domain": "Isbloca su domìniu {domain}",
"account.unblock_domain_short": "Isbloca",
"account.unblock_short": "Isbloca", "account.unblock_short": "Isbloca",
"account.unendorse": "Non cussiges in su profilu", "account.unendorse": "Non cussiges in su profilu",
"account.unfollow": "Non sigas prus", "account.unfollow": "Non sigas prus",
@@ -83,7 +99,22 @@
"alert.unexpected.message": "Ddoe est istada una faddina.", "alert.unexpected.message": "Ddoe est istada una faddina.",
"alert.unexpected.title": "Oh!", "alert.unexpected.title": "Oh!",
"alt_text_badge.title": "Testu alternativu", "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", "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)", "attachments_list.unprocessed": "(non protzessadu)",
"audio.hide": "Cua s'àudio", "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.", "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.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_column_error.routing.title": "404",
"bundle_modal_error.close": "Serra", "bundle_modal_error.close": "Serra",
"bundle_modal_error.message": "Faddina in su carrigamentu de custu ischermu.",
"bundle_modal_error.retry": "Torra·bi a proare", "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.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.", "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.blocks": "Persones blocadas",
"column.bookmarks": "Sinnalibros", "column.bookmarks": "Sinnalibros",
"column.community": "Lìnia de tempus locale", "column.community": "Lìnia de tempus locale",
"column.create_list": "Crea una lista",
"column.direct": "Mentziones privadas", "column.direct": "Mentziones privadas",
"column.directory": "Nàviga in is profilos", "column.directory": "Nàviga in is profilos",
"column.domain_blocks": "Domìnios blocados", "column.domain_blocks": "Domìnios blocados",
"column.edit_list": "Modifica sa lista",
"column.favourites": "Preferidos", "column.favourites": "Preferidos",
"column.firehose": "Publicatziones in direta", "column.firehose": "Publicatziones in direta",
"column.follow_requests": "Rechestas de sighidura", "column.follow_requests": "Rechestas de sighidura",
"column.home": "Printzipale", "column.home": "Printzipale",
"column.list_members": "Gesti is persones de sa lista",
"column.lists": "Listas", "column.lists": "Listas",
"column.mutes": "Persones a sa muda", "column.mutes": "Persones a sa muda",
"column.notifications": "Notìficas", "column.notifications": "Notìficas",
@@ -135,6 +170,7 @@
"column_header.pin": "Apica", "column_header.pin": "Apica",
"column_header.show_settings": "Ammustra is cunfiguratziones", "column_header.show_settings": "Ammustra is cunfiguratziones",
"column_header.unpin": "Boga dae pitzu", "column_header.unpin": "Boga dae pitzu",
"column_search.cancel": "Annulla",
"community.column_settings.local_only": "Isceti locale", "community.column_settings.local_only": "Isceti locale",
"community.column_settings.media_only": "Isceti multimediale", "community.column_settings.media_only": "Isceti multimediale",
"community.column_settings.remote_only": "Isceti remotu", "community.column_settings.remote_only": "Isceti remotu",
@@ -152,6 +188,7 @@
"compose_form.poll.duration": "Longària de su sondàgiu", "compose_form.poll.duration": "Longària de su sondàgiu",
"compose_form.poll.multiple": "Sèberu mùltiplu", "compose_form.poll.multiple": "Sèberu mùltiplu",
"compose_form.poll.option_placeholder": "Optzione {number}", "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_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.switch_to_single": "Muda su sondàgiu pro permìtere un'optzione isceti",
"compose_form.poll.type": "Istile", "compose_form.poll.type": "Istile",
@@ -169,6 +206,8 @@
"confirmations.delete_list.confirm": "Cantzella", "confirmations.delete_list.confirm": "Cantzella",
"confirmations.delete_list.message": "Seguru chi boles cantzellare custa lista in manera permanente?", "confirmations.delete_list.message": "Seguru chi boles cantzellare custa lista in manera permanente?",
"confirmations.delete_list.title": "Cantzellare sa lista?", "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.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.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", "confirmations.logout.confirm": "Essi·nche",
@@ -256,6 +295,7 @@
"explore.trending_links": "Noas", "explore.trending_links": "Noas",
"explore.trending_statuses": "Publicatziones", "explore.trending_statuses": "Publicatziones",
"explore.trending_tags": "Etichetas", "explore.trending_tags": "Etichetas",
"featured_carousel.slide": "{index} de {total}",
"filter_modal.added.context_mismatch_title": "Su cuntestu non currispondet.", "filter_modal.added.context_mismatch_title": "Su cuntestu non currispondet.",
"filter_modal.added.expired_title": "Filtru iscadidu.", "filter_modal.added.expired_title": "Filtru iscadidu.",
"filter_modal.added.review_and_configure_title": "Cunfiguratziones de filtru", "filter_modal.added.review_and_configure_title": "Cunfiguratziones de filtru",
@@ -294,8 +334,12 @@
"footer.privacy_policy": "Polìtica de riservadesa", "footer.privacy_policy": "Polìtica de riservadesa",
"footer.source_code": "Ammustra su còdighe de orìgine", "footer.source_code": "Ammustra su còdighe de orìgine",
"footer.status": "Istadu", "footer.status": "Istadu",
"footer.terms_of_service": "Cunditziones de su servìtziu",
"generic.saved": "Sarvadu", "generic.saved": "Sarvadu",
"getting_started.heading": "Comente cumintzare", "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.all": "e {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}", "hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sena {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_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": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}} oe", "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.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", "hashtag.unfollow": "Non sigas prus s'eticheta",
"hashtags.and_other": "… e {count, plural, one {un'àteru} other {àteros #}}", "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.", "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_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_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", "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.on_this_server": "In custu serbidore",
"interaction_modal.title.follow": "Sighi a {name}", "interaction_modal.title.follow": "Sighi a {name}",
"interaction_modal.title.reply": "Risponde a sa publicatzione de {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.days": "{number, plural, one {# die} other {# dies}}",
"intervals.full.hours": "{number, plural, one {# ora} other {# oras}}", "intervals.full.hours": "{number, plural, one {# ora} other {# oras}}",
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}", "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.direct": "pro abèrrere sa colunna de mèntovos privados",
"keyboard_shortcuts.down": "Move in bàsciu in sa lista", "keyboard_shortcuts.down": "Move in bàsciu in sa lista",
"keyboard_shortcuts.enter": "Aberi una publicatzione", "keyboard_shortcuts.enter": "Aberi una publicatzione",
"keyboard_shortcuts.favourite": "Publicatzione preferida",
"keyboard_shortcuts.favourites": "Aberi sa lista de preferidos", "keyboard_shortcuts.favourites": "Aberi sa lista de preferidos",
"keyboard_shortcuts.federated": "Aberi sa lìnia de tempus federada", "keyboard_shortcuts.federated": "Aberi sa lìnia de tempus federada",
"keyboard_shortcuts.heading": "Incurtzaduras de tecladu", "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_hidden": "Ammustra o cua su testu de is AC",
"keyboard_shortcuts.toggle_sensitivity": "Ammustra/cua elementos multimediales", "keyboard_shortcuts.toggle_sensitivity": "Ammustra/cua elementos multimediales",
"keyboard_shortcuts.toot": "Cumintza a iscrìere una publicatzione noa", "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.unfocus": "Essi de s'àrea de cumpositzione de testu o de chirca",
"keyboard_shortcuts.up": "Move in susu in sa lista", "keyboard_shortcuts.up": "Move in susu in sa lista",
"lightbox.close": "Serra", "lightbox.close": "Serra",
"lightbox.next": "Imbeniente", "lightbox.next": "Imbeniente",
"lightbox.previous": "Pretzedente", "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}.", "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}}", "link_preview.shares": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
"lists.delete": "Cantzella sa lista", "lists.delete": "Cantzella sa lista",
"lists.edit": "Modìfica 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.followed": "Cale si siat persone chi sighis",
"lists.replies_policy.list": "Persones de sa lista", "lists.replies_policy.list": "Persones de sa lista",
"lists.replies_policy.none": "Nemos", "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}}", "load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}",
"loading_indicator.label": "Carrighende…", "loading_indicator.label": "Carrighende…",
"media_gallery.hide": "Cua", "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_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.", "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.about": "Informatziones",
"navigation_bar.account_settings": "Crae e seguresa",
"navigation_bar.administration": "Amministratzione", "navigation_bar.administration": "Amministratzione",
"navigation_bar.advanced_interface": "Aberi s'interfache web avantzada", "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.blocks": "Persones blocadas",
"navigation_bar.bookmarks": "Sinnalibros", "navigation_bar.bookmarks": "Sinnalibros",
"navigation_bar.direct": "Mentziones privadas", "navigation_bar.direct": "Mentziones privadas",
@@ -398,13 +460,18 @@
"navigation_bar.follow_requests": "Rechestas de sighidura", "navigation_bar.follow_requests": "Rechestas de sighidura",
"navigation_bar.followed_tags": "Etichetas sighidas", "navigation_bar.followed_tags": "Etichetas sighidas",
"navigation_bar.follows_and_followers": "Gente chi sighis e sighiduras", "navigation_bar.follows_and_followers": "Gente chi sighis e sighiduras",
"navigation_bar.import_export": "Importatzione e esportatzione",
"navigation_bar.lists": "Listas", "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.logout": "Essi",
"navigation_bar.moderation": "Moderatzione", "navigation_bar.moderation": "Moderatzione",
"navigation_bar.more": "Àteru",
"navigation_bar.mutes": "Persones a sa muda", "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.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.preferences": "Preferèntzias",
"navigation_bar.search": "Chirca", "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.", "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": "{name} at sinnaladu a {target}",
"notification.admin.report_account": "{name} at sinnaladu {count, plural, one {una publicatzione} other {# publicatziones}} dae {target} pro {category}", "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.minimize_banner": "Mìnima su bànner de notìficas filtradas",
"notification_requests.notifications_from": "Notìficas dae {name}", "notification_requests.notifications_from": "Notìficas dae {name}",
"notification_requests.title": "Notìficas filtradas", "notification_requests.title": "Notìficas filtradas",
"notification_requests.view": "Mustra notìficas",
"notifications.clear": "Lìmpia 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_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.report": "Informes noos:",
"notifications.column_settings.admin.sign_up": "Registros noos:",
"notifications.column_settings.alert": "Notìficas de iscrivania", "notifications.column_settings.alert": "Notìficas de iscrivania",
"notifications.column_settings.favourite": "Preferidos:", "notifications.column_settings.favourite": "Preferidos:",
"notifications.column_settings.filter_bar.advanced": "Ammustra totu is categorias", "notifications.column_settings.filter_bar.advanced": "Ammustra totu is categorias",
"notifications.column_settings.filter_bar.category": "Barra de filtru lestru", "notifications.column_settings.filter_bar.category": "Barra de filtru lestru",
"notifications.column_settings.follow": "Sighiduras noas:", "notifications.column_settings.follow": "Sighiduras noas:",
"notifications.column_settings.follow_request": "Rechestas noas de sighidura:", "notifications.column_settings.follow_request": "Rechestas noas de sighidura:",
"notifications.column_settings.group": "Grupu",
"notifications.column_settings.mention": "Mèntovos:", "notifications.column_settings.mention": "Mèntovos:",
"notifications.column_settings.poll": "Resurtados de su sondàgiu:", "notifications.column_settings.poll": "Resurtados de su sondàgiu:",
"notifications.column_settings.push": "Notìficas push", "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": "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_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.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.hint": "Creadu {days, plural, one {erisero} other {in is ùrtimas # dies}}",
"notifications.policy.filter_new_accounts_title": "Contos noos", "notifications.policy.filter_new_accounts_title": "Contos noos",
"notifications.policy.filter_not_followers_title": "Gente chi non ti sighit", "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.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.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", "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.display_name": "Nòmine visìbile",
"onboarding.profile.note": "Biografia", "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", "picture_in_picture.restore": "Torra·ddu a ue fiat",
"poll.closed": "Serradu", "poll.closed": "Serradu",
"poll.refresh": "Atualiza", "poll.refresh": "Atualiza",
@@ -597,6 +677,7 @@
"search_results.hashtags": "Etichetas", "search_results.hashtags": "Etichetas",
"search_results.see_all": "Bide totu", "search_results.see_all": "Bide totu",
"search_results.statuses": "Publicatziones", "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.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.active_users": "utentes ativos",
"server_banner.administered_by": "Amministradu dae:", "server_banner.administered_by": "Amministradu dae:",

View File

@@ -815,6 +815,8 @@
"status.bookmark": "Додати до закладок", "status.bookmark": "Додати до закладок",
"status.cancel_reblog_private": "Скасувати поширення", "status.cancel_reblog_private": "Скасувати поширення",
"status.cannot_reblog": "Цей допис не може бути поширений", "status.cannot_reblog": "Цей допис не може бути поширений",
"status.context.load_new_replies": "Доступні нові відповіді",
"status.context.loading": "Перевірка додаткових відповідей",
"status.continued_thread": "Продовження у потоці", "status.continued_thread": "Продовження у потоці",
"status.copy": "Копіювати посилання на допис", "status.copy": "Копіювати посилання на допис",
"status.delete": "Видалити", "status.delete": "Видалити",

View File

@@ -116,6 +116,20 @@ class ActivityPub::Activity
fetch_remote_original_status fetch_remote_original_status
end 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! def dereference_object!
return unless @object.is_a?(String) 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? @follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end 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 def follow_from_object
@follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil? @follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end end

View File

@@ -4,10 +4,13 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
def perform def perform
return accept_follow_for_relay if relay_follow? return accept_follow_for_relay if relay_follow?
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil? 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'] case @object['type']
when 'Follow' when 'Follow'
accept_embedded_follow accept_embedded_follow
when 'QuoteRequest'
accept_embedded_quote_request
end end
end end
@@ -31,6 +34,29 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow
end 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 def accept_follow_for_relay
relay.update!(state: :accepted) relay.update!(state: :accepted)
end end

View File

@@ -5,10 +5,13 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
return reject_follow_for_relay if relay_follow? return reject_follow_for_relay if relay_follow?
return follow_request_from_object.reject! unless follow_request_from_object.nil? 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 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'] case @object['type']
when 'Follow' when 'Follow'
reject_embedded_follow reject_embedded_follow
when 'QuoteRequest'
reject_embedded_quote_request
end end
end end
@@ -29,6 +32,20 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
relay.update!(state: :rejected) relay.update!(state: :rejected)
end 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 def relay
@relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil? @relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil?
end end

View File

@@ -12,9 +12,7 @@ module ActivityPub::CaseTransform
when Hash then value.deep_transform_keys! { |key| camel_lower(key) } when Hash then value.deep_transform_keys! { |key| camel_lower(key) }
when Symbol then camel_lower(value.to_s).to_sym when Symbol then camel_lower(value.to_s).to_sym
when String when String
camel_lower_cache[value] ||= if value.start_with?('_:') camel_lower_cache[value] ||= if value.start_with?('_misskey') || LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
"_:#{value.delete_prefix('_:').underscore.camelize(:lower)}"
elsif LanguagesHelper::ISO_639_1_REGIONAL.key?(value.to_sym)
value value
else else
value.underscore.camelize(:lower) value.underscore.camelize(:lower)

View File

@@ -45,7 +45,9 @@ class EmojiFormatter
i += 1 i += 1
if inside_shortname && text[i] == ':' 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)] shortcode = text[(shortname_start_index + 1)..(i - 1)]
char_after = text[i + 1] char_after = text[i + 1]

View File

@@ -71,6 +71,8 @@ class StatusCacheHydrator
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id) 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[: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) 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] payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote]
end end

View 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

View File

@@ -45,16 +45,10 @@ class Status < ApplicationRecord
include Status::SnapshotConcern include Status::SnapshotConcern
include Status::ThreadingConcern include Status::ThreadingConcern
include Status::Visibility include Status::Visibility
include Status::InteractionPolicyConcern
MEDIA_ATTACHMENTS_LIMIT = 4 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 rate_limit by: :account, family: :statuses
self.discard_column = :deleted_at self.discard_column = :deleted_at

View File

@@ -20,6 +20,11 @@ class StatusPolicy < ApplicationPolicy
end end
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? def reblog?
!requires_mention? && (!private? || owned?) && show? && !blocking_author? !requires_mention? && (!private? || owned?) && show? && !blocking_author?
end end

View File

@@ -3,7 +3,7 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer class ActivityPub::NoteSerializer < ActivityPub::Serializer
include FormattingHelper 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, attributes :id, :type, :summary,
:in_reply_to, :published, :url, :in_reply_to, :published, :url,
@@ -32,6 +32,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
attribute :voters_count, if: :poll_and_voters_count? 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 def id
raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only] 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 object.preloadable_poll&.voters_count
end 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 class MediaAttachmentSerializer < ActivityPub::Serializer
context_extensions :blurhash, :focal_point context_extensions :blurhash, :focal_point

View File

@@ -34,6 +34,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
has_one :quote, key: :quote, serializer: REST::QuoteSerializer has_one :quote, key: :quote, serializer: REST::QuoteSerializer
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
delegate :local?, to: :object delegate :local?, to: :object
@@ -163,6 +164,14 @@ class REST::StatusSerializer < ActiveModel::Serializer
object.active_mentions.to_a.sort_by(&:id) object.active_mentions.to_a.sort_by(&:id)
end 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 private
def relationships def relationships

View File

@@ -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 # 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 # we only support incoming quotes so far
status.quote = Quote.new(quoted_status: @quoted_status) status.quote = Quote.create(quoted_status: @quoted_status, status: status)
status.quote.accept! if @status.account == @quoted_status.account || @quoted_status.active_mentions.exists?(mentions: { account_id: status.account_id }) if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote?
# TODO: produce a QuoteAuthorization
# TODO: the following has yet to be implemented: status.quote.accept!
# - handle approval of local users (requires the interactionPolicy PR) end
# - produce a QuoteAuthorization for quotes of local users
# - send a QuoteRequest for quotes of remote users
end end
def safeguard_mentions!(status) def safeguard_mentions!(status)
@@ -162,6 +160,7 @@ class PostStatusService < BaseService
DistributionWorker.perform_async(@status.id) DistributionWorker.perform_async(@status.id)
ActivityPub::DistributionWorker.perform_async(@status.id) unless @status.local_only? ActivityPub::DistributionWorker.perform_async(@status.id) unless @status.local_only?
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll 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 end
def validate_media! def validate_media!

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

View File

@@ -7,25 +7,17 @@
= render 'application/card', account: @account = render 'application/card', account: @account
- account = @account - if @account.fields? || @account.note?
- fields = account.fields
- unless fields.empty? && account.note.blank?
.admin-account-bio .admin-account-bio
- unless fields.empty? - if @account.fields?
%div %div
.account__header__fields .account__header__fields
- fields.each do |field| = render partial: 'field', collection: @account.fields, locals: { account: @account }
%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)
- if account.note.present? - if @account.note?
%div %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 = render 'admin/accounts/counters', account: @account

View 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

View File

@@ -17,10 +17,10 @@ class ActivityPub::StatusUpdateDistributionWorker < ActivityPub::DistributionWor
def activity def activity
ActivityPub::ActivityPresenter.new( 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', type: 'Update',
actor: ActivityPub::TagManager.instance.uri_for(@status.account), 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), to: ActivityPub::TagManager.instance.to(@status),
cc: ActivityPub::TagManager.instance.cc(@status), cc: ActivityPub::TagManager.instance.cc(@status),
virtual_object: @status virtual_object: @status

View File

@@ -22,11 +22,7 @@ class MentionResolveWorker
rescue Mastodon::UnexpectedResponseError => e rescue Mastodon::UnexpectedResponseError => e
response = e.response response = e.response
if response_error_unsalvageable?(response) raise(e) unless response_error_unsalvageable?(response)
# Give up
else
raise e
end
end end
private private

View File

@@ -20,10 +20,6 @@ class RedownloadAvatarWorker
rescue Mastodon::UnexpectedResponseError => e rescue Mastodon::UnexpectedResponseError => e
response = e.response response = e.response
if response_error_unsalvageable?(response) raise(e) unless response_error_unsalvageable?(response)
# Give up
else
raise e
end
end end
end end

View File

@@ -20,10 +20,6 @@ class RedownloadHeaderWorker
rescue Mastodon::UnexpectedResponseError => e rescue Mastodon::UnexpectedResponseError => e
response = e.response response = e.response
if response_error_unsalvageable?(response) raise(e) unless response_error_unsalvageable?(response)
# Give up
else
raise e
end
end end
end end

View File

@@ -20,10 +20,6 @@ class RedownloadMediaWorker
rescue Mastodon::UnexpectedResponseError => e rescue Mastodon::UnexpectedResponseError => e
response = e.response response = e.response
if response_error_unsalvageable?(response) raise(e) unless response_error_unsalvageable?(response)
# Give up
else
raise e
end
end end
end end

View File

@@ -15,10 +15,6 @@ class RemoteAccountRefreshWorker
rescue Mastodon::UnexpectedResponseError => e rescue Mastodon::UnexpectedResponseError => e
response = e.response response = e.response
if response_error_unsalvageable?(response) raise(e) unless response_error_unsalvageable?(response)
# Give up
else
raise e
end
end end
end end

View File

@@ -1873,6 +1873,7 @@ en:
edited_at_html: Edited %{date} edited_at_html: Edited %{date}
errors: errors:
in_reply_not_found: The post you are trying to reply to does not appear to exist. 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 over_character_limit: character limit of %{max} exceeded
pin_errors: pin_errors:
direct: Posts that are only visible to mentioned users cannot be pinned direct: Posts that are only visible to mentioned users cannot be pinned

View File

@@ -319,6 +319,7 @@ fr-CA:
create: Créer une annonce create: Créer une annonce
title: Nouvelle annonce title: Nouvelle annonce
preview: 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 :' 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 title: Aperçu de la notification d'annonce
publish: Publier publish: Publier

View File

@@ -319,6 +319,7 @@ fr:
create: Créer une annonce create: Créer une annonce
title: Nouvelle annonce title: Nouvelle annonce
preview: 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 :' 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 title: Aperçu de la notification d'annonce
publish: Publier publish: Publier

View File

@@ -1393,7 +1393,7 @@ ru:
featured_tags: featured_tags:
add_new: Добавить add_new: Добавить
errors: errors:
limit: Вы уже добавили максимальное число хештегов limit: Вы достигли максимального количества хештегов, которые можно рекомендовать в профиле
hint_html: "<strong>Рекомендуйте самые важные для вас хештеги в своём профиле.</strong> Это отличный инструмент для того, чтобы держать подписчиков в курсе ваших долгосрочных проектов и творческих работ. Рекомендации хештегов заметны в вашем профиле и предоставляют быстрый доступ к вашим постам." hint_html: "<strong>Рекомендуйте самые важные для вас хештеги в своём профиле.</strong> Это отличный инструмент для того, чтобы держать подписчиков в курсе ваших долгосрочных проектов и творческих работ. Рекомендации хештегов заметны в вашем профиле и предоставляют быстрый доступ к вашим постам."
filters: filters:
contexts: contexts:
@@ -1552,16 +1552,16 @@ ru:
many: Вы собираетесь <strong>игнорировать</strong> <strong>%{count} пользователей</strong> из файла <strong>%{filename}</strong>. many: Вы собираетесь <strong>игнорировать</strong> <strong>%{count} пользователей</strong> из файла <strong>%{filename}</strong>.
one: Вы собираетесь <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>. other: Вы собираетесь <strong>игнорировать</strong> <strong>%{count} пользователей</strong> из файла <strong>%{filename}</strong>.
preface: Вы можете загрузить некоторые данные, например, списки людей, на которых Вы подписаны или которых блокируете, в Вашу учётную запись на этом узле из файлов, экспортированных с другого узла. preface: Вы можете перенести прежде экспортированные с другого сервера данные, такие как блокируемые вами пользователи и ваши подписки.
recent_imports: Недавно импортированное recent_imports: История импорта
states: states:
finished: Готово finished: Завершён
in_progress: В процессе in_progress: Выполняется
scheduled: Запланировано scheduled: Запланирован
unconfirmed: Неподтвержденный unconfirmed: Не подтверждён
status: Статус status: Состояние
success: Ваши данные были успешно загружены и будут обработаны с должной скоростью success: Ваши данные были загружены и в скором времени будут обработаны
time_started: Началось в time_started: Начат
titles: titles:
blocking: Импорт списка заблокированных пользователей blocking: Импорт списка заблокированных пользователей
bookmarks: Импорт закладок bookmarks: Импорт закладок
@@ -1572,7 +1572,7 @@ ru:
type: Тип импорта type: Тип импорта
type_groups: type_groups:
constructive: Подписки и закладки constructive: Подписки и закладки
destructive: Блокировки и игнорируемые destructive: Чёрный список
types: types:
blocking: Заблокированные пользователи blocking: Заблокированные пользователи
bookmarks: Закладки bookmarks: Закладки
@@ -1583,7 +1583,7 @@ ru:
upload: Загрузить upload: Загрузить
invites: invites:
delete: Удалить delete: Удалить
expired: Истекло expired: Срок действия истёк
expires_in: expires_in:
'1800': 30 минут '1800': 30 минут
'21600': 6 часов '21600': 6 часов
@@ -1592,33 +1592,32 @@ ru:
'604800': 1 неделя '604800': 1 неделя
'86400': 1 день '86400': 1 день
expires_in_prompt: Бессрочно expires_in_prompt: Бессрочно
generate: Сгенерировать generate: Создать ссылку
invalid: Это приглашение недействительно invalid: Это приглашение недействительно
invited_by: 'Вас пригласил(а):' invited_by: 'Вы были приглашены этим пользователем:'
max_uses: max_uses:
few: "%{count} раза" few: "%{count} раза"
many: "%{count} раз" many: "%{count} раз"
one: "%{count} раз" one: "%{count} раз"
other: "%{count} раза" other: "%{count} раз"
max_uses_prompt: Без ограничения max_uses_prompt: Без ограничения
prompt: Создавайте и делитесь ссылками с другими, чтобы предоставить им доступом к этому узлу. prompt: Создавайте приглашения и делитесь ими с другими людьми, чтобы они могли зарегистрироваться на этом сервере
table: table:
expires_at: Истекает expires_at: Истекает
uses: Исп. uses: Регистрации
title: Пригласить людей title: Приглашения
lists: lists:
errors: errors:
limit: Вы достигли максимального количества пользователей limit: Вы достигли максимального количества списков
login_activities: login_activities:
authentication_methods: authentication_methods:
otp: приложение двухфакторной аутентификации otp: приложения двухфакторной аутентификации
password: пароль password: пароля
sign_in_token: код безопасности электронной почты webauthn: электронного ключа
webauthn: ключи безопасности description_html: Если вы заметили действия, которых не совершали, вам следует сменить пароль и включить двухфакторную аутентификацию.
description_html: Если вы видите неопознанное действие, смените пароль и/или включите двухфакторную авторизацию. empty: История входов отсутствует
empty: Нет доступной истории входов failed_sign_in_html: Неудачная попытка входа при помощи %{method} с IP-адреса %{ip} (%{browser})
failed_sign_in_html: Неудачная попытка входа используя %{method} через %{browser} (%{ip}) successful_sign_in_html: Вход при помощи %{method} с IP-адреса %{ip} (%{browser})
successful_sign_in_html: Успешный вход используя %{method} через %{browser} (%{ip})
title: История входов title: История входов
mail_subscriptions: mail_subscriptions:
unsubscribe: unsubscribe:

View File

@@ -231,8 +231,8 @@ el:
setting_always_send_emails: Πάντα να αποστέλλονται ειδοποίησεις μέσω email setting_always_send_emails: Πάντα να αποστέλλονται ειδοποίησεις μέσω email
setting_auto_play_gif: Αυτόματη αναπαραγωγή των GIF setting_auto_play_gif: Αυτόματη αναπαραγωγή των GIF
setting_boost_modal: Επιβεβαίωση πριν την προώθηση setting_boost_modal: Επιβεβαίωση πριν την προώθηση
setting_default_language: Γλώσσα δημοσιεύσεων setting_default_language: Γλώσσα κατά την ανάρτηση
setting_default_privacy: Ιδιωτικότητα δημοσιεύσεων setting_default_privacy: Ιδιωτικότητα αναρτήσεων
setting_default_quote_policy: Ποιος μπορεί να παραθέσει setting_default_quote_policy: Ποιος μπορεί να παραθέσει
setting_default_sensitive: Σημείωση όλων των πολυμέσων ως ευαίσθητου περιεχομένου setting_default_sensitive: Σημείωση όλων των πολυμέσων ως ευαίσθητου περιεχομένου
setting_delete_modal: Επιβεβαίωση πριν τη διαγραφή ενός τουτ setting_delete_modal: Επιβεβαίωση πριν τη διαγραφή ενός τουτ

View File

@@ -6,6 +6,7 @@ RSpec.describe AccountableConcern do
let(:hoge_class) do let(:hoge_class) do
Class.new do Class.new do
include AccountableConcern include AccountableConcern
attr_reader :current_account attr_reader :current_account
def initialize(current_account) def initialize(current_account)

View File

@@ -75,23 +75,19 @@ RSpec.describe HomeHelper do
end end
end end
describe 'custom_field_classes' do describe 'field_verified_class' do
context 'with a verified field' do subject { helper.field_verified_class(verified) }
let(:field) { instance_double(Account::Field, verified?: true) }
it 'returns verified string' do context 'with a verified field' do
result = helper.custom_field_classes(field) let(:verified) { true }
expect(result).to eq 'verified'
end it { is_expected.to eq('verified') }
end end
context 'with a non-verified field' do context 'with a non-verified field' do
let(:field) { instance_double(Account::Field, verified?: false) } let(:verified) { false }
it 'returns verified string' do it { is_expected.to eq('emojify') }
result = helper.custom_field_classes(field)
expect(result).to eq 'emojify'
end
end end
end end

View File

@@ -3,7 +3,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe ActivityPub::Activity::Accept do RSpec.describe ActivityPub::Activity::Accept do
let(:sender) { Fabricate(:account) } let(:sender) { Fabricate(:account, domain: 'example.com') }
let(:recipient) { Fabricate(:account) } let(:recipient) { Fabricate(:account) }
describe '#perform' do describe '#perform' do
@@ -48,5 +48,128 @@ RSpec.describe ActivityPub::Activity::Accept do
end end
end 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
end end

View File

@@ -125,5 +125,27 @@ RSpec.describe ActivityPub::Activity::Reject do
expect(relay.reload.rejected?).to be true expect(relay.reload.rejected?).to be true
end 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, 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
end end

View File

@@ -28,6 +28,18 @@ RSpec.describe StatusCacheHydrator do
end end
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 context 'when handling a filtered status' do
let(:status) { Fabricate(:status, text: 'this toot is about that banned word') } let(:status) { Fabricate(:status, text: 'this toot is about that banned word') }

View File

@@ -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

View File

@@ -98,6 +98,92 @@ RSpec.describe StatusPolicy, type: :model do
end end
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 context 'with the permission of reblog?' do
permissions :reblog? do permissions :reblog? do
it 'denies access when private' do it 'denies access when private' do

View File

@@ -158,6 +158,27 @@ RSpec.describe '/api/v1/statuses' do
end end
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 context 'with a safeguard' do
let!(:alice) { Fabricate(:account, username: 'alice') } let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob') } let!(:bob) { Fabricate(:account, username: 'bob') }

View File

@@ -49,6 +49,7 @@ RSpec.describe 'Routes under accounts/' do
context 'with local username encoded at' do context 'with local username encoded at' do
include RSpec::Rails::RequestExampleGroup include RSpec::Rails::RequestExampleGroup
let(:username) { 'alice' } let(:username) { 'alice' }
it 'routes /%40:username' do it 'routes /%40:username' do
@@ -140,6 +141,7 @@ RSpec.describe 'Routes under accounts/' do
context 'with remote username encoded at' do context 'with remote username encoded at' do
include RSpec::Rails::RequestExampleGroup include RSpec::Rails::RequestExampleGroup
let(:username) { 'alice%40example.com' } let(:username) { 'alice%40example.com' }
let(:username_decoded) { 'alice@example.com' } let(:username_decoded) { 'alice@example.com' }

View File

@@ -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_other_first.uri)) # Replies from others
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility .and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
end 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 end

View File

@@ -291,6 +291,14 @@ RSpec.describe PostStatusService do
) )
end 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 it 'returns existing status when used twice with idempotency key' do
account = Fabricate(:account) account = Fabricate(:account)
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep') status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')

View File

@@ -6,6 +6,7 @@ RSpec.describe ExistingUsernameValidator do
let(:record_class) do let(:record_class) do
Class.new do Class.new do
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :contact, :friends attr_accessor :contact, :friends
def self.name def self.name

View File

@@ -6,6 +6,7 @@ RSpec.describe LanguageValidator do
let(:record_class) do let(:record_class) do
Class.new do Class.new do
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :locale attr_accessor :locale
validates :locale, language: true validates :locale, language: true

View File

@@ -6,6 +6,7 @@ RSpec.describe UnreservedUsernameValidator do
let(:record_class) do let(:record_class) do
Class.new do Class.new do
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :username attr_accessor :username
validates_with UnreservedUsernameValidator validates_with UnreservedUsernameValidator

View File

@@ -6,6 +6,7 @@ RSpec.describe URLValidator do
let(:record_class) do let(:record_class) do
Class.new do Class.new do
include ActiveModel::Validations include ActiveModel::Validations
attr_accessor :profile attr_accessor :profile
validates :profile, url: true validates :profile, url: true

View 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

View File

@@ -9,6 +9,7 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com', domain: 'example.com') } let(:follower) { Fabricate(:account, protocol: :activitypub, inbox_url: 'http://example.com', domain: 'example.com') }
describe '#perform' do describe '#perform' do
context 'with an explicitly edited status' do
before do before do
follower.follow!(status.account) follower.follow!(status.account)
@@ -25,9 +26,8 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
end end
it 'delivers to followers' do 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 expect { subject.perform(status.id) }
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
end end
@@ -37,8 +37,36 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
end end
it 'delivers to followers' do 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 expect { subject.perform(status.id) }
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 end
end end

View File

@@ -189,29 +189,30 @@ async function findEntrypoints() {
const entrypoints: Record<string, string> = {}; const entrypoints: Record<string, string> = {};
// First, JS entrypoints // First, JS entrypoints
const jsEntrypoints = await readdir(path.resolve(jsRoot, 'entrypoints'), { const jsEntrypointsDir = path.resolve(jsRoot, 'entrypoints');
const jsEntrypoints = await readdir(jsEntrypointsDir, {
withFileTypes: true, withFileTypes: true,
}); });
const jsExtTest = /\.[jt]sx?$/; const jsExtTest = /\.[jt]sx?$/;
for (const file of jsEntrypoints) { for (const file of jsEntrypoints) {
if (file.isFile() && jsExtTest.test(file.name)) { if (file.isFile() && jsExtTest.test(file.name)) {
entrypoints[file.name.replace(jsExtTest, '')] = path.resolve( entrypoints[file.name.replace(jsExtTest, '')] = path.resolve(
file.parentPath, jsEntrypointsDir,
file.name, file.name,
); );
} }
} }
// Next, SCSS entrypoints // Next, SCSS entrypoints
const scssEntrypoints = await readdir( const scssEntrypointsDir = path.resolve(jsRoot, 'styles/entrypoints');
path.resolve(jsRoot, 'styles/entrypoints'), const scssEntrypoints = await readdir(scssEntrypointsDir, {
{ withFileTypes: true }, withFileTypes: true,
); });
const scssExtTest = /\.s?css$/; const scssExtTest = /\.s?css$/;
for (const file of scssEntrypoints) { for (const file of scssEntrypoints) {
if (file.isFile() && scssExtTest.test(file.name)) { if (file.isFile() && scssExtTest.test(file.name)) {
entrypoints[file.name.replace(scssExtTest, '')] = path.resolve( entrypoints[file.name.replace(scssExtTest, '')] = path.resolve(
file.parentPath, scssEntrypointsDir,
file.name, file.name,
); );
} }