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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ class Api::V1::StatusesController < Api::BaseController
before_action :set_statuses, only: [:index]
before_action :set_status, only: [:show, :context]
before_action :set_thread, only: [:create]
before_action :set_quoted_status, only: [:create]
before_action :check_statuses_limit, only: [:index]
override_rate_limit_headers :create, family: :statuses
@@ -76,6 +77,7 @@ class Api::V1::StatusesController < Api::BaseController
current_user.account,
text: status_params[:status],
thread: @thread,
quoted_status: @quoted_status,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
@@ -149,6 +151,16 @@ class Api::V1::StatusesController < Api::BaseController
render json: { error: I18n.t('statuses.errors.in_reply_not_found') }, status: 404
end
def set_quoted_status
return unless Mastodon::Feature.outgoing_quotes_enabled?
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
# TODO: distinguish between non-existing and non-quotable posts
render json: { error: I18n.t('statuses.errors.quoted_status_not_found') }, status: 404
end
def check_statuses_limit
raise(Mastodon::ValidationError) if status_ids.size > DEFAULT_STATUSES_LIMIT
end
@@ -165,6 +177,7 @@ class Api::V1::StatusesController < Api::BaseController
params.permit(
:status,
:in_reply_to_id,
:quoted_status_id,
:sensitive,
:spoiler_text,
:visibility,

View File

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

View File

@@ -558,6 +558,8 @@
"status.bookmark": "Ouzhpennañ d'ar sinedoù",
"status.cancel_reblog_private": "Nac'hañ ar skignadenn",
"status.cannot_reblog": "Ar c'hannad-se na c'hall ket bezañ skignet",
"status.context.load_new_replies": "Respontoù nevez zo",
"status.context.loading": "O kerc'hat muioc'h a respontoù",
"status.copy": "Eilañ liamm ar c'hannad",
"status.delete": "Dilemel",
"status.detailed_status": "Gwel kaozeadenn munudek",

View File

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

View File

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

View File

@@ -612,7 +612,7 @@
"notification.moderation_warning.action_suspend": "Your account has been suspended.",
"notification.own_poll": "Your poll has ended",
"notification.poll": "A poll you voted in has ended",
"notification.reblog": "{name} boosted your status",
"notification.reblog": "{name} boosted your post",
"notification.reblog.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> boosted your post",
"notification.relationships_severance_event": "Lost connections with {name}",
"notification.relationships_severance_event.account_suspension": "An admin from {from} has suspended {target}, which means you can no longer receive updates from them or interact with them.",

View File

@@ -845,6 +845,8 @@
"status.bookmark": "Leabharmharcanna",
"status.cancel_reblog_private": "Dímhol",
"status.cannot_reblog": "Ní féidir an phostáil seo a mholadh",
"status.context.load_new_replies": "Freagraí nua ar fáil",
"status.context.loading": "Ag seiceáil le haghaidh tuilleadh freagraí",
"status.continued_thread": "Snáithe ar lean",
"status.copy": "Cóipeáil an nasc chuig an bpostáil",
"status.delete": "Scrios",

View File

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

View File

@@ -845,6 +845,8 @@
"status.bookmark": "Guardar nos marcadores",
"status.cancel_reblog_private": "Retirar impulso",
"status.cannot_reblog": "Esta publicação não pode ser impulsionada",
"status.context.load_new_replies": "Novas respostas disponíveis",
"status.context.loading": "A verificar por mais respostas",
"status.continued_thread": "Continuação da conversa",
"status.copy": "Copiar hiperligação da publicação",
"status.delete": "Eliminar",

View File

@@ -1,6 +1,7 @@
{
"about.blocks": "Serbidores moderados",
"about.contact": "Cuntatu:",
"about.default_locale": "Predefinidu",
"about.disclaimer": "Mastodon est software de còdighe lìberu e unu màrchiu de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Peruna resone a disponimentu",
"about.domain_blocks.preamble": "Mastodon ti permitit de bìdere su cuntenutu de utentes de cale si siat àteru serbidore de su fediversu. Custas sunt etzetziones fatas in custu serbidore ispetzìficu.",
@@ -8,6 +9,7 @@
"about.domain_blocks.silenced.title": "Limitadu",
"about.domain_blocks.suspended.explanation": "Perunu datu de custu serbidore at a èssere protzessadu, immagasinadu o cuncambiadu; est impossìbile duncas cale si siat interatzione o comunicatzione cun is utentes de custu serbidore.",
"about.domain_blocks.suspended.title": "Suspèndidu",
"about.language_label": "Idioma",
"about.not_available": "Custa informatzione no est istada posta a disponimentu in custu serbidore.",
"about.powered_by": "Rete sotziale detzentralizada impulsada dae {mastodon}",
"about.rules": "Règulas de su serbidore",
@@ -19,13 +21,21 @@
"account.block_domain": "Bloca su domìniu {domain}",
"account.block_short": "Bloca",
"account.blocked": "Blocadu",
"account.blocking": "Blocadu",
"account.cancel_follow_request": "Annulla sa sighidura",
"account.copy": "Còpia su ligòngiu a su profilu",
"account.direct": "Mèntova a @{name} in privadu",
"account.disable_notifications": "Non mi notìfiches prus cando @{name} pùblichet messàgios",
"account.domain_blocking": "Blocamus su domìniu",
"account.edit_profile": "Modìfica profilu",
"account.enable_notifications": "Notìfica·mi cando @{name} pùblicat messàgios",
"account.endorse": "Cussìgia in su profilu tuo",
"account.familiar_followers_many": "Sighidu dae {name1}, {name2} e {othersCount, plural,one {un'àtera persone chi connosches} other {àteras # persones chi connosches}}",
"account.familiar_followers_one": "Sighidu dae {name1}",
"account.familiar_followers_two": "Sighidu dae {name1} e {name2}",
"account.featured": "In evidèntzia",
"account.featured.accounts": "Profilos",
"account.featured.hashtags": "Etichetas",
"account.featured_tags.last_status_at": "Ùrtima publicatzione in su {date}",
"account.featured_tags.last_status_never": "Peruna publicatzione",
"account.follow": "Sighi",
@@ -33,9 +43,11 @@
"account.followers": "Sighiduras",
"account.followers.empty": "Nemos sighit ancora custa persone.",
"account.followers_counter": "{count, plural, one {{counter} sighidura} other {{counter} sighiduras}}",
"account.followers_you_know_counter": "{counter} chi connosches",
"account.following": "Sighende",
"account.following_counter": "{count, plural, one {sighende a {counter}} other {sighende a {counter}}}",
"account.follows.empty": "Custa persone non sighit ancora a nemos.",
"account.follows_you": "Ti sighit",
"account.go_to_profile": "Bae a su profilu",
"account.hide_reblogs": "Cua is cumpartziduras de @{name}",
"account.in_memoriam": "In memoriam.",
@@ -50,18 +62,22 @@
"account.mute_notifications_short": "Pone is notìficas a sa muda",
"account.mute_short": "A sa muda",
"account.muted": "A sa muda",
"account.muting": "A sa muda",
"account.no_bio": "Peruna descritzione frunida.",
"account.open_original_page": "Aberi sa pàgina originale",
"account.posts": "Publicatziones",
"account.posts_with_replies": "Publicatziones e rispostas",
"account.remove_from_followers": "Cantzella a {name} dae is sighiduras",
"account.report": "Signala @{name}",
"account.requested": "Abetende s'aprovatzione. Incarca pro annullare sa rechesta de sighidura",
"account.requested_follow": "{name} at dimandadu de ti sighire",
"account.requests_to_follow_you": "Rechestas de sighidura",
"account.share": "Cumpartzi su profilu de @{name}",
"account.show_reblogs": "Ammustra is cumpartziduras de @{name}",
"account.statuses_counter": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
"account.unblock": "Isbloca a @{name}",
"account.unblock_domain": "Isbloca su domìniu {domain}",
"account.unblock_domain_short": "Isbloca",
"account.unblock_short": "Isbloca",
"account.unendorse": "Non cussiges in su profilu",
"account.unfollow": "Non sigas prus",
@@ -83,7 +99,22 @@
"alert.unexpected.message": "Ddoe est istada una faddina.",
"alert.unexpected.title": "Oh!",
"alt_text_badge.title": "Testu alternativu",
"alt_text_modal.add_alt_text": "Agiunghe testu alternativu",
"alt_text_modal.add_text_from_image": "Agiunghe testu dae un'immàgine",
"alt_text_modal.cancel": "Annulla",
"alt_text_modal.change_thumbnail": "Càmbia sa miniadura",
"alt_text_modal.done": "Fatu",
"announcement.announcement": "Annùntziu",
"annual_report.summary.archetype.booster": "Semper a s'ùrtima",
"annual_report.summary.followers.followers": "sighiduras",
"annual_report.summary.followers.total": "{count} totale",
"annual_report.summary.highlighted_post.possessive": "de {name}",
"annual_report.summary.most_used_app.most_used_app": "aplicatzione prus impreada",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "eticheta prus impreada",
"annual_report.summary.most_used_hashtag.none": "Peruna",
"annual_report.summary.new_posts.new_posts": "publicatziones noas",
"annual_report.summary.percentile.we_wont_tell_bernie": "No dd'amus a nàrrere a Bernie.",
"annual_report.summary.thanks": "Gràtzias de èssere parte de Mastodon!",
"attachments_list.unprocessed": "(non protzessadu)",
"audio.hide": "Cua s'àudio",
"block_modal.remote_users_caveat": "Amus a pedire a su serbidore {domain} de rispetare sa detzisione tua. Nointames custu, su rispetu no est garantidu ca unos cantos serbidores diant pòdere gestire is blocos de manera diferente. Is publicatzione pùblicas diant pòdere ancora èssere visìbiles a is utentes chi no ant fatu s'atzessu.",
@@ -107,6 +138,7 @@
"bundle_column_error.routing.body": "Impossìbile agatare sa pàgina rechesta. Seguru chi s'URL in sa barra de indiritzos est curretu?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Serra",
"bundle_modal_error.message": "Faddina in su carrigamentu de custu ischermu.",
"bundle_modal_error.retry": "Torra·bi a proare",
"closed_registrations.other_server_instructions": "Dae chi Mastodon est detzentralizadu, podes creare unu contu in un'àteru serbidore e interagire cun custu.",
"closed_registrations_modal.description": "Sa creatzione de contos in {domain} no est possìbile in custu momentu, però tene in cunsideru chi non tenes bisòngiu de unu contu ispetzìficu in {domain} pro impreare Mastodon.",
@@ -116,13 +148,16 @@
"column.blocks": "Persones blocadas",
"column.bookmarks": "Sinnalibros",
"column.community": "Lìnia de tempus locale",
"column.create_list": "Crea una lista",
"column.direct": "Mentziones privadas",
"column.directory": "Nàviga in is profilos",
"column.domain_blocks": "Domìnios blocados",
"column.edit_list": "Modifica sa lista",
"column.favourites": "Preferidos",
"column.firehose": "Publicatziones in direta",
"column.follow_requests": "Rechestas de sighidura",
"column.home": "Printzipale",
"column.list_members": "Gesti is persones de sa lista",
"column.lists": "Listas",
"column.mutes": "Persones a sa muda",
"column.notifications": "Notìficas",
@@ -135,6 +170,7 @@
"column_header.pin": "Apica",
"column_header.show_settings": "Ammustra is cunfiguratziones",
"column_header.unpin": "Boga dae pitzu",
"column_search.cancel": "Annulla",
"community.column_settings.local_only": "Isceti locale",
"community.column_settings.media_only": "Isceti multimediale",
"community.column_settings.remote_only": "Isceti remotu",
@@ -152,6 +188,7 @@
"compose_form.poll.duration": "Longària de su sondàgiu",
"compose_form.poll.multiple": "Sèberu mùltiplu",
"compose_form.poll.option_placeholder": "Optzione {number}",
"compose_form.poll.single": "Sèberu ùnicu",
"compose_form.poll.switch_to_multiple": "Muda su sondàgiu pro permìtere multi-optziones",
"compose_form.poll.switch_to_single": "Muda su sondàgiu pro permìtere un'optzione isceti",
"compose_form.poll.type": "Istile",
@@ -169,6 +206,8 @@
"confirmations.delete_list.confirm": "Cantzella",
"confirmations.delete_list.message": "Seguru chi boles cantzellare custa lista in manera permanente?",
"confirmations.delete_list.title": "Cantzellare sa lista?",
"confirmations.discard_draft.confirm": "Iscarta e sighi",
"confirmations.discard_draft.edit.cancel": "Sighi cun s'editzione",
"confirmations.discard_edit_media.confirm": "Iscarta",
"confirmations.discard_edit_media.message": "Tenes modìficas non sarvadas a is descritziones o a is anteprimas de is cuntenutos, ddas boles iscartare su matessi?",
"confirmations.logout.confirm": "Essi·nche",
@@ -256,6 +295,7 @@
"explore.trending_links": "Noas",
"explore.trending_statuses": "Publicatziones",
"explore.trending_tags": "Etichetas",
"featured_carousel.slide": "{index} de {total}",
"filter_modal.added.context_mismatch_title": "Su cuntestu non currispondet.",
"filter_modal.added.expired_title": "Filtru iscadidu.",
"filter_modal.added.review_and_configure_title": "Cunfiguratziones de filtru",
@@ -294,8 +334,12 @@
"footer.privacy_policy": "Polìtica de riservadesa",
"footer.source_code": "Ammustra su còdighe de orìgine",
"footer.status": "Istadu",
"footer.terms_of_service": "Cunditziones de su servìtziu",
"generic.saved": "Sarvadu",
"getting_started.heading": "Comente cumintzare",
"hashtag.admin_moderation": "Aberi s'interfache de moderatzione pro #{name}",
"hashtag.browse": "Nàviga in is publicatziones de #{hashtag}",
"hashtag.browse_from_account": "Nàviga in is publicatziones de @{name} in #{hashtag}",
"hashtag.column_header.tag_mode.all": "e {additional}",
"hashtag.column_header.tag_mode.any": "o {additional}",
"hashtag.column_header.tag_mode.none": "sena {additional}",
@@ -308,7 +352,10 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} partetzipante} other {{counter} partetzipantes}}",
"hashtag.counter_by_uses": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}} oe",
"hashtag.feature": "In evidèntzia in su profilu",
"hashtag.follow": "Sighi su hashtag",
"hashtag.mute": "Pone #{hashtag} a sa muda",
"hashtag.unfeature": "No ddu pòngias in evidèntzia in su profilu",
"hashtag.unfollow": "Non sigas prus s'eticheta",
"hashtags.and_other": "… e {count, plural, one {un'àteru} other {àteros #}}",
"hints.profiles.posts_may_be_missing": "Podet èssere chi ammanchent tzertas publicatziones de custu profilu.",
@@ -324,9 +371,14 @@
"ignore_notifications_modal.filter_instead": "Opuru filtra",
"ignore_notifications_modal.filter_to_act_users": "As a pòdere ancora atzetare, refudare o sinnalare a utentes",
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtrare agiudat a evitare possìbiles confusiones",
"info_button.label": "Agiudu",
"interaction_modal.go": "Sighi",
"interaction_modal.no_account_yet": "Non tenes galu perunu contu?",
"interaction_modal.on_another_server": "In un'àteru serbidore",
"interaction_modal.on_this_server": "In custu serbidore",
"interaction_modal.title.follow": "Sighi a {name}",
"interaction_modal.title.reply": "Risponde a sa publicatzione de {name}",
"interaction_modal.username_prompt": "Pro es., {example}",
"intervals.full.days": "{number, plural, one {# die} other {# dies}}",
"intervals.full.hours": "{number, plural, one {# ora} other {# oras}}",
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
@@ -339,6 +391,7 @@
"keyboard_shortcuts.direct": "pro abèrrere sa colunna de mèntovos privados",
"keyboard_shortcuts.down": "Move in bàsciu in sa lista",
"keyboard_shortcuts.enter": "Aberi una publicatzione",
"keyboard_shortcuts.favourite": "Publicatzione preferida",
"keyboard_shortcuts.favourites": "Aberi sa lista de preferidos",
"keyboard_shortcuts.federated": "Aberi sa lìnia de tempus federada",
"keyboard_shortcuts.heading": "Incurtzaduras de tecladu",
@@ -361,18 +414,25 @@
"keyboard_shortcuts.toggle_hidden": "Ammustra o cua su testu de is AC",
"keyboard_shortcuts.toggle_sensitivity": "Ammustra/cua elementos multimediales",
"keyboard_shortcuts.toot": "Cumintza a iscrìere una publicatzione noa",
"keyboard_shortcuts.translate": "pro tradùere una publicatzione",
"keyboard_shortcuts.unfocus": "Essi de s'àrea de cumpositzione de testu o de chirca",
"keyboard_shortcuts.up": "Move in susu in sa lista",
"lightbox.close": "Serra",
"lightbox.next": "Imbeniente",
"lightbox.previous": "Pretzedente",
"lightbox.zoom_in": "Ismànnia finas a sa mannària atuale",
"limited_account_hint.title": "Custu profilu est istadu cuadu dae sa moderatzione de {domain}.",
"link_preview.author": "Dae {name}",
"link_preview.shares": "{count, plural, one {{counter} publicatzione} other {{counter} publicatziones}}",
"lists.delete": "Cantzella sa lista",
"lists.edit": "Modìfica sa lista",
"lists.remove_member": "Cantzella",
"lists.replies_policy.followed": "Cale si siat persone chi sighis",
"lists.replies_policy.list": "Persones de sa lista",
"lists.replies_policy.none": "Nemos",
"lists.save": "Sarva",
"lists.search": "Chirca",
"lists.show_replies_to": "Include rispostas dae gente de sa lista a",
"load_pending": "{count, plural, one {# elementu nou} other {# elementos noos}}",
"loading_indicator.label": "Carrighende…",
"media_gallery.hide": "Cua",
@@ -387,8 +447,10 @@
"mute_modal.you_wont_see_mentions": "No as a bìdere is publicatziones chi mèntovent a custa persone.",
"mute_modal.you_wont_see_posts": "At a pòdere bìdere is publicatziones tuas, però tue no as a bìdere cussas suas.",
"navigation_bar.about": "Informatziones",
"navigation_bar.account_settings": "Crae e seguresa",
"navigation_bar.administration": "Amministratzione",
"navigation_bar.advanced_interface": "Aberi s'interfache web avantzada",
"navigation_bar.automated_deletion": "Cantzelladura automàtica de publicatziones",
"navigation_bar.blocks": "Persones blocadas",
"navigation_bar.bookmarks": "Sinnalibros",
"navigation_bar.direct": "Mentziones privadas",
@@ -398,13 +460,18 @@
"navigation_bar.follow_requests": "Rechestas de sighidura",
"navigation_bar.followed_tags": "Etichetas sighidas",
"navigation_bar.follows_and_followers": "Gente chi sighis e sighiduras",
"navigation_bar.import_export": "Importatzione e esportatzione",
"navigation_bar.lists": "Listas",
"navigation_bar.live_feed_local": "Canale in direta (locale)",
"navigation_bar.live_feed_public": "Canale in direta (pùblicu)",
"navigation_bar.logout": "Essi",
"navigation_bar.moderation": "Moderatzione",
"navigation_bar.more": "Àteru",
"navigation_bar.mutes": "Persones a sa muda",
"navigation_bar.opened_in_classic_interface": "Publicatziones, contos e àteras pàginas ispetzìficas sunt abertas in manera predefinida in s'interfache web clàssica.",
"navigation_bar.preferences": "Preferèntzias",
"navigation_bar.search": "Chirca",
"navigation_bar.search_trends": "Chirca / in tendèntzia",
"not_signed_in_indicator.not_signed_in": "Ti depes identificare pro atzèdere a custa resursa.",
"notification.admin.report": "{name} at sinnaladu a {target}",
"notification.admin.report_account": "{name} at sinnaladu {count, plural, one {una publicatzione} other {# publicatziones}} dae {target} pro {category}",
@@ -461,15 +528,19 @@
"notification_requests.minimize_banner": "Mìnima su bànner de notìficas filtradas",
"notification_requests.notifications_from": "Notìficas dae {name}",
"notification_requests.title": "Notìficas filtradas",
"notification_requests.view": "Mustra notìficas",
"notifications.clear": "Lìmpia notìficas",
"notifications.clear_confirmation": "Seguru chi boles isboidare in manera permanente totu is notìficas tuas?",
"notifications.clear_title": "Boles cantzellare is notìficas?",
"notifications.column_settings.admin.report": "Informes noos:",
"notifications.column_settings.admin.sign_up": "Registros noos:",
"notifications.column_settings.alert": "Notìficas de iscrivania",
"notifications.column_settings.favourite": "Preferidos:",
"notifications.column_settings.filter_bar.advanced": "Ammustra totu is categorias",
"notifications.column_settings.filter_bar.category": "Barra de filtru lestru",
"notifications.column_settings.follow": "Sighiduras noas:",
"notifications.column_settings.follow_request": "Rechestas noas de sighidura:",
"notifications.column_settings.group": "Grupu",
"notifications.column_settings.mention": "Mèntovos:",
"notifications.column_settings.poll": "Resurtados de su sondàgiu:",
"notifications.column_settings.push": "Notìficas push",
@@ -493,6 +564,8 @@
"notifications.permission_denied": "Is notìficas de iscrivania non sunt a disponimentu pro neghe de rechestas de permissu chi sunt istadas dennegadas in antis",
"notifications.permission_denied_alert": "Is notìficas de iscrivania non podent èssere abilitadas, ca su permissu de su navigadore est istadu dennegadu in antis",
"notifications.permission_required": "Is notìficas de iscrivania no sunt a disponimentu ca ammancat su permissu rechèdidu.",
"notifications.policy.accept": "Atzeta",
"notifications.policy.accept_hint": "Mustra in is notìficas",
"notifications.policy.filter_new_accounts.hint": "Creadu {days, plural, one {erisero} other {in is ùrtimas # dies}}",
"notifications.policy.filter_new_accounts_title": "Contos noos",
"notifications.policy.filter_not_followers_title": "Gente chi non ti sighit",
@@ -501,8 +574,15 @@
"notifications_permission_banner.enable": "Abilita is notìficas de iscrivania",
"notifications_permission_banner.how_to_control": "Pro retzire notìficas cando Mastodon no est abertu, abilita is notìficas de iscrivania. Podes controllare cun pretzisione is castas de interatziones chi ingendrant notìficas de iscrivania pro mèdiu de su butone {icon} in subra, cando sunt abilitadas.",
"notifications_permission_banner.title": "Non ti perdas mai nudda",
"onboarding.follows.back": "A coa",
"onboarding.follows.done": "Fatu",
"onboarding.follows.search": "Chirca",
"onboarding.follows.title": "Sighi a gente pro cumintzare",
"onboarding.profile.display_name": "Nòmine visìbile",
"onboarding.profile.note": "Biografia",
"onboarding.profile.save_and_continue": "Sarva e sighi",
"onboarding.profile.title": "Cunfiguratzione de profilu",
"onboarding.profile.upload_avatar": "Càrriga una fotografia de profilu",
"picture_in_picture.restore": "Torra·ddu a ue fiat",
"poll.closed": "Serradu",
"poll.refresh": "Atualiza",
@@ -597,6 +677,7 @@
"search_results.hashtags": "Etichetas",
"search_results.see_all": "Bide totu",
"search_results.statuses": "Publicatziones",
"search_results.title": "Chirca \"{q}\"",
"server_banner.about_active_users": "Gente chi at impreadu custu serbidore is ùrtimas 30 dies (Utentes cun Atividade a su Mese)",
"server_banner.active_users": "utentes ativos",
"server_banner.administered_by": "Amministradu dae:",

View File

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

View File

@@ -116,6 +116,20 @@ class ActivityPub::Activity
fetch_remote_original_status
end
def quote_from_request_json(json)
quoted_status_uri = value_or_id(json['object'])
quoting_status_uri = value_or_id(json['instrument'])
return if quoting_status_uri.nil? || quoted_status_uri.nil?
quoting_status = status_from_uri(quoting_status_uri)
return unless quoting_status.present? && quoting_status.quote.present?
quoted_status = status_from_uri(quoted_status_uri)
return unless quoted_status.present? && quoted_status.account == @account && quoting_status.quote.quoted_status == quoted_status
quoting_status.quote
end
def dereference_object!
return unless @object.is_a?(String)
@@ -143,6 +157,10 @@ class ActivityPub::Activity
@follow_request_from_object ||= FollowRequest.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end
def quote_request_from_object
@quote_request_from_object ||= Quote.find_by(quoted_account: @account, activity_uri: object_uri) unless object_uri.nil?
end
def follow_from_object
@follow_from_object ||= ::Follow.find_by(target_account: @account, uri: object_uri) unless object_uri.nil?
end

View File

@@ -4,10 +4,13 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
def perform
return accept_follow_for_relay if relay_follow?
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil?
case @object['type']
when 'Follow'
accept_embedded_follow
when 'QuoteRequest'
accept_embedded_quote_request
end
end
@@ -31,6 +34,29 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
RemoteAccountRefreshWorker.perform_async(request.target_account_id) if is_first_follow
end
def accept_embedded_quote_request
approval_uri = value_or_id(first_of_value(@json['result']))
return if approval_uri.nil?
quote = quote_from_request_json(@object)
return unless quote.present? && quote.status.local?
accept_quote!(quote)
end
def accept_quote!(quote)
approval_uri = value_or_id(first_of_value(@json['result']))
return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local?
# NOTE: we are not going through `ActivityPub::VerifyQuoteService` as the `Accept` is as authoritative
# as the stamp, but this means we are not checking the stamp, which may lead to inconsistencies
# in case of an implementation bug
quote.update!(state: :accepted, approval_uri: approval_uri)
DistributionWorker.perform_async(quote.status_id, { 'update' => true })
ActivityPub::StatusUpdateDistributionWorker.perform_async(quote.status_id, { 'updated_at' => Time.now.utc.iso8601 })
end
def accept_follow_for_relay
relay.update!(state: :accepted)
end

View File

@@ -5,10 +5,13 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
return reject_follow_for_relay if relay_follow?
return follow_request_from_object.reject! unless follow_request_from_object.nil?
return UnfollowService.new.call(follow_from_object.account, @account) unless follow_from_object.nil?
return reject_quote!(quote_request_from_object) unless quote_request_from_object.nil?
case @object['type']
when 'Follow'
reject_embedded_follow
when 'QuoteRequest'
reject_embedded_quote_request
end
end
@@ -29,6 +32,20 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
relay.update!(state: :rejected)
end
def reject_embedded_quote_request
quote = quote_from_request_json(@object)
return unless quote.present? && quote.status.local?
reject_quote!(quoting_status.quote)
end
def reject_quote!(quote)
return unless quote.quoted_account == @account && quote.status.local?
# TODO: broadcast an update?
quote.reject!
end
def relay
@relay ||= Relay.find_by(follow_activity_id: object_uri) unless object_uri.nil?
end

View File

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

View File

@@ -45,7 +45,9 @@ class EmojiFormatter
i += 1
if inside_shortname && text[i] == ':'
inside_shortname = false
# https://github.com/rubocop/rubocop/issues/14383
# False positive in line below, remove disable when resolved
inside_shortname = false # rubocop:disable Lint/UselessAssignment
shortcode = text[(shortname_start_index + 1)..(i - 1)]
char_after = text[i + 1]

View File

@@ -71,6 +71,8 @@ class StatusCacheHydrator
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
# TODO: performance optimization by not loading `Account` twice
payload[:quote_approval][:current_user] = status.quote_policy_for_account(Account.find_by(id: account_id)) if payload[:quote_approval]
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote]
end

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::ThreadingConcern
include Status::Visibility
include Status::InteractionPolicyConcern
MEDIA_ATTACHMENTS_LIMIT = 4
QUOTE_APPROVAL_POLICY_FLAGS = {
unknown: (1 << 0),
public: (1 << 1),
followers: (1 << 2),
followed: (1 << 3),
}.freeze
rate_limit by: :account, family: :statuses
self.discard_column = :deleted_at

View File

@@ -20,6 +20,11 @@ class StatusPolicy < ApplicationPolicy
end
end
# This is about requesting a quote post, not validating it
def quote?
record.quote_policy_for_account(current_account, preloaded_relations: @preloaded_relations) != :denied
end
def reblog?
!requires_mention? && (!private? || owned?) && show? && !blocking_author?
end

View File

@@ -3,7 +3,7 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer
include FormattingHelper
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :direct_message
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes, :direct_message
attributes :id, :type, :summary,
:in_reply_to, :published, :url,
@@ -32,6 +32,11 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
attribute :voters_count, if: :poll_and_voters_count?
attribute :quote, if: :quote?
attribute :quote, key: :_misskey_quote, if: :quote?
attribute :quote, key: :quote_uri, if: :quote?
attribute :quote_authorization, if: :quote_authorization?
def id
raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only]
@@ -206,6 +211,24 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
object.preloadable_poll&.voters_count
end
def quote?
object.quote&.present?
end
def quote_authorization?
object.quote&.approval_uri.present?
end
def quote
# TODO: handle inlining self-quotes
ActivityPub::TagManager.instance.uri_for(object.quote.quoted_status)
end
def quote_authorization
# TODO: approval of local quotes may work differently, perhaps?
object.quote.approval_uri
end
class MediaAttachmentSerializer < ActivityPub::Serializer
context_extensions :blurhash, :focal_point

View File

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

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

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
- account = @account
- fields = account.fields
- unless fields.empty? && account.note.blank?
- if @account.fields? || @account.note?
.admin-account-bio
- unless fields.empty?
- if @account.fields?
%div
.account__header__fields
- fields.each do |field|
%dl
%dt.emojify{ title: field.name }= prerender_custom_emojis(h(field.name), account.emojis)
%dd{ title: field.value, class: custom_field_classes(field) }
- if field.verified?
%span.verified__mark{ title: t('accounts.link_verified_on', date: l(field.verified_at)) }
= material_symbol 'check'
= prerender_custom_emojis(account_field_value_format(field, with_rel_me: false), account.emojis)
= render partial: 'field', collection: @account.fields, locals: { account: @account }
- if account.note.present?
- if @account.note?
%div
.account__header__content.emojify= prerender_custom_emojis(account_bio_format(account), account.emojis)
.account__header__content.emojify
= prerender_custom_emojis(account_bio_format(@account), @account.emojis)
= render 'admin/accounts/counters', account: @account

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
ActivityPub::ActivityPresenter.new(
id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @status.edited_at.to_i].join,
id: [ActivityPub::TagManager.instance.uri_for(@status), '#updates/', @options[:updated_at]&.to_datetime&.to_i || @status.edited_at.to_i].join,
type: 'Update',
actor: ActivityPub::TagManager.instance.uri_for(@status.account),
published: @status.edited_at,
published: @options[:updated_at]&.to_datetime || @status.edited_at,
to: ActivityPub::TagManager.instance.to(@status),
cc: ActivityPub::TagManager.instance.cc(@status),
virtual_object: @status

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -319,6 +319,7 @@ fr-CA:
create: Créer une annonce
title: Nouvelle annonce
preview:
disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur.
explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :'
title: Aperçu de la notification d'annonce
publish: Publier

View File

@@ -319,6 +319,7 @@ fr:
create: Créer une annonce
title: Nouvelle annonce
preview:
disclaimer: Étant donné que les utilisateurs ne peuvent pas s'en retirer, les notifications par courriel devraient être limitées à des annonces importantes telles que des violations de données personnelles ou des notifications de fermeture de serveur.
explanation_html: 'L''e-mail sera envoyé à <strong>%{display_count} utilisateurs</strong>. Le texte suivant sera inclus :'
title: Aperçu de la notification d'annonce
publish: Publier

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::Accept do
let(:sender) { Fabricate(:account) }
let(:sender) { Fabricate(:account, domain: 'example.com') }
let(:recipient) { Fabricate(:account) }
describe '#perform' do
@@ -48,5 +48,128 @@ RSpec.describe ActivityPub::Activity::Accept do
end
end
end
context 'with a QuoteRequest' do
let(:status) { Fabricate(:status, account: recipient) }
let(:quoted_status) { Fabricate(:status, account: sender) }
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status) }
let(:approval_uri) { "https://#{sender.domain}/approvals/1" }
let(:json) do
{
'@context': [
'https://www.w3.org/ns/activitystreams',
{
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
},
],
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: {
id: quote.activity_uri,
type: 'QuoteRequest',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
instrument: ActivityPub::TagManager.instance.uri_for(status),
},
result: approval_uri,
}.with_indifferent_access
end
it 'marks the quote as approved and distribute an update' do
expect { subject.perform }
.to change { quote.reload.accepted? }.from(false).to(true)
.and change { quote.reload.approval_uri }.to(approval_uri)
expect(DistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) })
end
context 'when the quoted status is not from the sender of the Accept' do
let(:quoted_status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com')) }
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, anything)
end
end
context 'when the quoting status is from an unrelated user' do
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'foobar.com')) }
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, anything)
end
end
context 'when approval_uri is missing' do
let(:approval_uri) { nil }
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, anything)
end
end
context 'when the QuoteRequest is referenced by its identifier' do
let(:json) do
{
'@context': [
'https://www.w3.org/ns/activitystreams',
{
QuoteRequest: 'https://w3id.org/fep/044f#QuoteRequest',
},
],
id: 'foo',
type: 'Accept',
actor: ActivityPub::TagManager.instance.uri_for(sender),
object: quote.activity_uri,
result: approval_uri,
}.with_indifferent_access
end
it 'marks the quote as approved and distribute an update' do
expect { subject.perform }
.to change { quote.reload.accepted? }.from(false).to(true)
.and change { quote.reload.approval_uri }.to(approval_uri)
expect(DistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to have_enqueued_sidekiq_job(status.id, { 'updated_at' => be_a(String) })
end
context 'when approval_uri is missing' do
let(:approval_uri) { nil }
it 'does not mark the quote as approved and does not distribute an update' do
expect { subject.perform }
.to not_change { quote.reload.accepted? }.from(false)
.and not_change { quote.reload.approval_uri }.from(nil)
expect(DistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, { 'update' => true })
expect(ActivityPub::StatusUpdateDistributionWorker)
.to_not have_enqueued_sidekiq_job(status.id, anything)
end
end
end
end
end
end

View File

@@ -125,5 +125,27 @@ RSpec.describe ActivityPub::Activity::Reject do
expect(relay.reload.rejected?).to be true
end
end
context 'with a QuoteRequest' do
let(:status) { Fabricate(:status, account: recipient) }
let(:quoted_status) { Fabricate(:status, account: sender) }
let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, activity_uri: 'https://abc-123/456') }
let(:approval_uri) { "https://#{sender.domain}/approvals/1" }
let(:object_json) do
{
id: 'https://abc-123/456',
type: 'QuoteRequest',
actor: ActivityPub::TagManager.instance.uri_for(recipient),
object: ActivityPub::TagManager.instance.uri_for(quoted_status),
instrument: ActivityPub::TagManager.instance.uri_for(status),
}.with_indifferent_access
end
it 'marks the quote as rejected' do
expect { subject.perform }
.to change { quote.reload.rejected? }.from(false).to(true)
end
end
end
end

View File

@@ -28,6 +28,18 @@ RSpec.describe StatusCacheHydrator do
end
end
context 'when handling a status with a quote policy', feature: :outgoing_quotes do
let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
before do
account.follow!(status.account)
end
it 'renders the same attributes as a full render' do
expect(subject).to eql(compare_to_hash)
end
end
context 'when handling a filtered status' do
let(:status) { Fabricate(:status, text: 'this toot is about that banned word') }

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
context 'with the permission of quote?' do
permissions :quote? do
it 'grants access when direct and account is viewer' do
status.visibility = :direct
expect(subject).to permit(status.account, status)
end
it 'grants access when direct and viewer is mentioned' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: alice)]
expect(subject).to permit(alice, status)
end
it 'grants access when direct and non-owner viewer is mentioned and mentions are loaded' do
status.visibility = :direct
status.mentions = [Fabricate(:mention, account: bob)]
status.active_mentions.load
expect(subject).to permit(bob, status)
end
it 'denies access when direct and viewer is not mentioned' do
viewer = Fabricate(:account)
status.visibility = :direct
expect(subject).to_not permit(viewer, status)
end
it 'denies access when private and viewer is not mentioned' do
viewer = Fabricate(:account)
status.visibility = :private
expect(subject).to_not permit(viewer, status)
end
it 'grants access when private and viewer is mentioned' do
status.visibility = :private
status.mentions = [Fabricate(:mention, account: bob)]
expect(subject).to permit(bob, status)
end
it 'denies access when private and non-viewer is mentioned' do
viewer = Fabricate(:account)
status.visibility = :private
status.mentions = [Fabricate(:mention, account: bob)]
expect(subject).to_not permit(viewer, status)
end
it 'denies access when private and account is following viewer' do
follow = Fabricate(:follow)
status.visibility = :private
status.account = follow.target_account
expect(subject).to_not permit(follow.account, status)
end
it 'denies access when public but policy does not allow anyone' do
viewer = Fabricate(:account)
expect(subject).to_not permit(viewer, status)
end
it 'grants access when public and policy allows everyone' do
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:public]
viewer = Fabricate(:account)
expect(subject).to permit(viewer, status)
end
it 'denies access when public and policy allows followers but viewer is not one' do
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
viewer = Fabricate(:account)
expect(subject).to_not permit(viewer, status)
end
it 'grants access when public and policy allows followers and viewer is one' do
status.quote_approval_policy = Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers]
viewer = Fabricate(:account)
viewer.follow!(status.account)
expect(subject).to permit(viewer, status)
end
end
end
context 'with the permission of reblog?' do
permissions :reblog? do
it 'denies access when private' do

View File

@@ -158,6 +158,27 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'with a self-quote post', feature: :outgoing_quotes do
let(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do
{
status: 'Hello world, this is a self-quote',
quoted_status_id: quoted_status.id,
}
end
it 'returns a quote post, as well as rate limit headers', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:quote]).to be_present
expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s
end
end
context 'with a safeguard' do
let!(:alice) { Fabricate(:account, username: 'alice') }
let!(:bob) { Fabricate(:account, username: 'bob') }

View File

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

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_account_visibility_direct.uri)) # Replies with direct visibility
end
context 'with a quote' do
let(:quoted_status) { Fabricate(:status) }
let(:approval_uri) { 'https://example.com/foo/bar' }
let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, approval_uri: approval_uri) }
it 'has the expected shape' do
expect(subject).to include({
'type' => 'Note',
'quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
'quoteUri' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
'_misskey_quote' => ActivityPub::TagManager.instance.uri_for(quote.quoted_status),
'quoteAuthorization' => approval_uri,
})
end
end
end

View File

@@ -291,6 +291,14 @@ RSpec.describe PostStatusService do
)
end
it 'correctly requests a quote for remote posts' do
account = Fabricate(:account)
quoted_status = Fabricate(:status, account: Fabricate(:account, domain: 'example.com'))
expect { subject.call(account, text: 'test', quoted_status: quoted_status) }
.to enqueue_sidekiq_job(ActivityPub::QuoteRequestWorker)
end
it 'returns existing status when used twice with idempotency key' do
account = Fabricate(:account)
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')

View File

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

View File

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

View File

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

View File

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

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') }
describe '#perform' do
context 'with an explicitly edited status' do
before do
follower.follow!(status.account)
@@ -25,9 +26,8 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
end
it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
subject.perform(status.id)
end
expect { subject.perform(status.id) }
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
end
end
@@ -37,8 +37,36 @@ RSpec.describe ActivityPub::StatusUpdateDistributionWorker do
end
it 'delivers to followers' do
expect_push_bulk_to_match(ActivityPub::DeliveryWorker, [[match_json_values(type: 'Update'), status.account.id, 'http://example.com', anything]]) do
subject.perform(status.id)
expect { subject.perform(status.id) }
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
end
end
end
context 'with an implicitly edited status' do
before do
follower.follow!(status.account)
end
context 'with public status' do
before do
status.update(visibility: :public)
end
it 'delivers to followers' do
expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) }
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
end
end
context 'with private status' do
before do
status.update(visibility: :private)
end
it 'delivers to followers' do
expect { subject.perform(status.id, { 'updated_at' => Time.now.utc.iso8601 }) }
.to enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Update'), status.account_id, 'http://example.com', anything)
end
end
end

View File

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