diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index f63a3ebf39..c3c41f3c5d 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -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.75.1. +# using RuboCop version 1.75.2. # 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 @@ -58,12 +58,6 @@ Style/FormatStringToken: Style/GuardClause: Enabled: false -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/HashTransformValues: - Exclude: - - 'app/serializers/rest/web_push_subscription_serializer.rb' - - 'app/services/import_service.rb' - # Configuration parameters: AllowedMethods. # AllowedMethods: respond_to_missing? Style/OptionalBooleanParameter: diff --git a/.ruby-version b/.ruby-version index 4d9d11cf50..6cb9d3dd0d 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.2 +3.4.3 diff --git a/Dockerfile b/Dockerfile index 330b9aae2f..f2ec8c4744 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ARG BASE_REGISTRY="docker.io" # Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.4.x"] # renovate: datasource=docker depName=docker.io/ruby -ARG RUBY_VERSION="3.4.2" +ARG RUBY_VERSION="3.4.3" # # Node.js version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"] # renovate: datasource=node-version depName=node ARG NODE_MAJOR_VERSION="22" diff --git a/FEDERATION.md b/FEDERATION.md index 2819fa935a..03ea5449de 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -13,6 +13,7 @@ - [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md) - [FEP-8fcf: Followers collection synchronization across servers](https://codeberg.org/fediverse/fep/src/branch/main/fep/8fcf/fep-8fcf.md) - [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md) +- [FEP-044f: Consent-respecting quote posts](https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md): partial support for incoming quote-posts ## ActivityPub in Mastodon diff --git a/Gemfile.lock b/Gemfile.lock index 86cfaa3132..0b21e544e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -94,7 +94,7 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.3.2) - aws-partitions (1.1080.0) + aws-partitions (1.1087.0) aws-sdk-core (3.215.1) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -120,7 +120,7 @@ GEM rack (>= 0.9.0) rouge (>= 1.0.0) bigdecimal (3.1.9) - bindata (2.5.0) + bindata (2.5.1) binding_of_caller (1.0.1) debug_inspector (>= 1.2.0) blurhash (0.1.8) @@ -160,7 +160,7 @@ GEM cocoon (1.2.15) color_diff (0.1) concurrent-ruby (1.3.5) - connection_pool (2.5.0) + connection_pool (2.5.1) cose (1.3.1) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) @@ -170,7 +170,7 @@ GEM crass (1.0.6) css_parser (1.21.1) addressable - csv (3.3.3) + csv (3.3.4) database_cleaner-active_record (2.2.0) activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) @@ -201,7 +201,7 @@ GEM domain_name (0.6.20240107) doorkeeper (5.8.2) railties (>= 5) - dotenv (3.1.7) + dotenv (3.1.8) drb (2.2.1) elasticsearch (7.17.11) elasticsearch-api (= 7.17.11) @@ -227,7 +227,7 @@ GEM fabrication (2.31.0) faker (3.5.1) i18n (>= 1.8.11, < 2) - faraday (2.12.2) + faraday (2.13.0) faraday-net_http (>= 2.0, < 3.5) json logger @@ -239,7 +239,7 @@ GEM net-http (>= 0.5.0) fast_blank (1.0.1) fastimage (2.4.0) - ffi (1.17.1) + ffi (1.17.2) ffi-compiler (1.3.2) ffi (>= 1.15.5) rake @@ -426,7 +426,7 @@ GEM mime-types (3.6.2) logger mime-types-data (~> 3.2015) - mime-types-data (3.2025.0402) + mime-types-data (3.2025.0408) mini_mime (1.1.5) mini_portile2 (2.8.8) minitest (5.25.5) @@ -495,6 +495,8 @@ GEM opentelemetry-common (~> 0.20) opentelemetry-sdk (~> 1.2) opentelemetry-semantic_conventions + opentelemetry-helpers-sql (0.1.1) + opentelemetry-api (~> 1.0) opentelemetry-helpers-sql-obfuscation (0.3.0) opentelemetry-common (~> 0.21) opentelemetry-instrumentation-action_mailer (0.4.0) @@ -548,8 +550,9 @@ GEM opentelemetry-instrumentation-net_http (0.23.0) opentelemetry-api (~> 1.0) opentelemetry-instrumentation-base (~> 0.23.0) - opentelemetry-instrumentation-pg (0.30.0) + opentelemetry-instrumentation-pg (0.30.1) opentelemetry-api (~> 1.0) + opentelemetry-helpers-sql opentelemetry-helpers-sql-obfuscation opentelemetry-instrumentation-base (~> 0.23.0) opentelemetry-instrumentation-rack (0.26.0) @@ -585,8 +588,8 @@ GEM ostruct (0.6.1) ox (2.14.22) bigdecimal (>= 3.0) - parallel (1.26.3) - parser (3.3.7.4) + parallel (1.27.0) + parser (3.3.8.0) ast (~> 2.4.1) racc parslet (2.0.0) @@ -751,7 +754,7 @@ GEM rubocop-ast (>= 1.44.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.44.0) + rubocop-ast (1.44.1) parser (>= 3.3.7.2) prism (~> 1.4) rubocop-capybara (2.22.1) @@ -1085,4 +1088,4 @@ RUBY VERSION ruby 3.4.1p0 BUNDLED WITH - 2.6.7 + 2.6.8 diff --git a/app/controllers/api/v1/accounts/endorsements_controller.rb b/app/controllers/api/v1/accounts/endorsements_controller.rb new file mode 100644 index 0000000000..1e21994a90 --- /dev/null +++ b/app/controllers/api/v1/accounts/endorsements_controller.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +class Api::V1::Accounts::EndorsementsController < Api::BaseController + include Authorization + + before_action -> { authorize_if_got_token! :read, :'read:accounts' }, only: :index + before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index + before_action :require_user!, except: :index + before_action :set_account + before_action :set_endorsed_accounts, only: :index + after_action :insert_pagination_headers, only: :index + + def index + cache_if_unauthenticated! + render json: @endorsed_accounts, each_serializer: REST::AccountSerializer + end + + def create + AccountPin.find_or_create_by!(account: current_account, target_account: @account) + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + def destroy + pin = AccountPin.find_by(account: current_account, target_account: @account) + pin&.destroy! + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def set_endorsed_accounts + @endorsed_accounts = @account.unavailable? ? [] : paginated_endorsed_accounts + end + + def paginated_endorsed_accounts + @account.endorsed_accounts.without_suspended.includes(:account_stat, :user).paginate_by_max_id( + limit_param(DEFAULT_ACCOUNTS_LIMIT), + params[:max_id], + params[:since_id] + ) + end + + def relationships_presenter + AccountRelationshipsPresenter.new([@account], current_user.account_id) + end + + def next_path + api_v1_account_endorsements_url pagination_params(max_id: pagination_max_id) if records_continue? + end + + def prev_path + api_v1_account_endorsements_url pagination_params(since_id: pagination_since_id) unless @endorsed_accounts.empty? + end + + def pagination_collection + @endorsed_accounts + end + + def records_continue? + @endorsed_accounts.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) + end +end diff --git a/app/controllers/api/v1/accounts/featured_tags_controller.rb b/app/controllers/api/v1/accounts/featured_tags_controller.rb index 0101fb469b..f95846366c 100644 --- a/app/controllers/api/v1/accounts/featured_tags_controller.rb +++ b/app/controllers/api/v1/accounts/featured_tags_controller.rb @@ -17,6 +17,6 @@ class Api::V1::Accounts::FeaturedTagsController < Api::BaseController end def set_featured_tags - @featured_tags = @account.suspended? ? [] : @account.featured_tags + @featured_tags = @account.unavailable? ? [] : @account.featured_tags end end diff --git a/app/controllers/api/v1/accounts/pins_controller.rb b/app/controllers/api/v1/accounts/pins_controller.rb deleted file mode 100644 index 0eb13c048c..0000000000 --- a/app/controllers/api/v1/accounts/pins_controller.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -class Api::V1::Accounts::PinsController < Api::BaseController - include Authorization - - before_action -> { doorkeeper_authorize! :write, :'write:accounts' } - before_action :require_user! - before_action :set_account - - def create - AccountPin.find_or_create_by!(account: current_account, target_account: @account) - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - def destroy - pin = AccountPin.find_by(account: current_account, target_account: @account) - pin&.destroy! - render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships_presenter - end - - private - - def set_account - @account = Account.find(params[:account_id]) - end - - def relationships_presenter - AccountRelationshipsPresenter.new([@account], current_user.account_id) - end -end diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb index 4bbbed2673..6bb884faa0 100644 --- a/app/controllers/api/v1/lists_controller.rb +++ b/app/controllers/api/v1/lists_controller.rb @@ -7,10 +7,6 @@ class Api::V1::ListsController < Api::BaseController before_action :require_user! before_action :set_list, except: [:index, :create] - rescue_from ArgumentError do |e| - render json: { error: e.to_s }, status: 422 - end - def index @lists = List.where(account: current_account).all render json: @lists, each_serializer: REST::ListSerializer diff --git a/app/javascript/mastodon/actions/accounts_typed.ts b/app/javascript/mastodon/actions/accounts_typed.ts index 058a68a099..fcdec97e08 100644 --- a/app/javascript/mastodon/actions/accounts_typed.ts +++ b/app/javascript/mastodon/actions/accounts_typed.ts @@ -1,7 +1,9 @@ import { createAction } from '@reduxjs/toolkit'; +import { apiRemoveAccountFromFollowers } from 'mastodon/api/accounts'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; export const revealAccount = createAction<{ id: string; @@ -95,3 +97,10 @@ export const fetchRelationshipsSuccess = createAction( 'relationships/fetch/SUCCESS', actionWithSkipLoadingTrue<{ relationships: ApiRelationshipJSON[] }>, ); + +export const removeAccountFromFollowers = createDataLoadingThunk( + 'accounts/remove_from_followers', + ({ accountId }: { accountId: string }) => + apiRemoveAccountFromFollowers(accountId), + (relationship) => ({ relationship }), +); diff --git a/app/javascript/mastodon/api/accounts.ts b/app/javascript/mastodon/api/accounts.ts index 717010ba74..c574a47459 100644 --- a/app/javascript/mastodon/api/accounts.ts +++ b/app/javascript/mastodon/api/accounts.ts @@ -18,3 +18,8 @@ export const apiFollowAccount = ( export const apiUnfollowAccount = (id: string) => apiRequestPost(`v1/accounts/${id}/unfollow`); + +export const apiRemoveAccountFromFollowers = (id: string) => + apiRequestPost( + `v1/accounts/${id}/remove_from_followers`, + ); diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx index a5d2deaae1..886d517fa9 100644 --- a/app/javascript/mastodon/components/dropdown_menu.tsx +++ b/app/javascript/mastodon/components/dropdown_menu.tsx @@ -71,6 +71,8 @@ type RenderItemFn = ( }, ) => React.ReactNode; +type ItemClickFn = (item: Item, index: number) => void; + type RenderHeaderFn = (items: Item[]) => React.ReactNode; interface DropdownMenuProps { @@ -81,10 +83,10 @@ interface DropdownMenuProps { openedViaKeyboard: boolean; renderItem?: RenderItemFn; renderHeader?: RenderHeaderFn; - onItemClick: (e: React.MouseEvent | React.KeyboardEvent) => void; + onItemClick?: ItemClickFn; } -const DropdownMenu = ({ +export const DropdownMenu = ({ items, loading, scrollable, @@ -176,20 +178,35 @@ const DropdownMenu = ({ [], ); + const handleItemClick = useCallback( + (e: React.MouseEvent | React.KeyboardEvent) => { + const i = Number(e.currentTarget.getAttribute('data-index')); + const item = items?.[i]; + + onClose(); + + if (!item) { + return; + } + + if (typeof onItemClick === 'function') { + e.preventDefault(); + onItemClick(item, i); + } else if (isActionItem(item)) { + e.preventDefault(); + item.action(); + } + }, + [onClose, onItemClick, items], + ); + const handleItemKeyUp = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { - onItemClick(e); + handleItemClick(e); } }, - [onItemClick], - ); - - const handleClick = useCallback( - (e: React.MouseEvent | React.KeyboardEvent) => { - onItemClick(e); - }, - [onItemClick], + [handleItemClick], ); const nativeRenderItem = (option: Item, i: number) => { @@ -209,7 +226,7 @@ const DropdownMenu = ({ element = ( .", "domain_pill.who_you_are": "Sidan handtaket ditt seier kven du er og kvar du er, kan folk interagere med deg på tvers av det sosiale nettverket av .", "domain_pill.your_handle": "Handtaket ditt:", - "domain_pill.your_server": "Din digitale heim, der alle innlegga dine bur i. Liker du ikkje dette? Byt til ein ny tenar når som helst og ta med fylgjarane dine òg.", + "domain_pill.your_server": "Din digitale heim, der alle innlegga dine bur. Liker du ikkje dette? Byt til ein ny tenar når som helst og ta med fylgjarane dine òg.", "domain_pill.your_username": "Din unike identifikator på denne tenaren. Det er mogleg å finne brukarar med same brukarnamn på forskjellige tenarar.", "embed.instructions": "Bygg inn denne statusen på nettsida di ved å kopiera koden nedanfor.", "embed.preview": "Slik kjem det til å sjå ut:", @@ -377,6 +378,8 @@ "generic.saved": "Lagra", "getting_started.heading": "Kom i gang", "hashtag.admin_moderation": "Opne moderasjonsgrensesnitt for #{name}", + "hashtag.browse": "Bla gjennom innlegg i #{hashtag}", + "hashtag.browse_from_account": "Bla gjennom innlegg frå @{name} i #{hashtag}", "hashtag.column_header.tag_mode.all": "og {additional}", "hashtag.column_header.tag_mode.any": "eller {additional}", "hashtag.column_header.tag_mode.none": "utan {additional}", @@ -390,6 +393,7 @@ "hashtag.counter_by_uses": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}}", "hashtag.counter_by_uses_today": "{count, plural,one {{counter} innlegg} other {{counter} innlegg}} i dag", "hashtag.follow": "Fylg emneknagg", + "hashtag.mute": "Demp @#{hashtag}", "hashtag.unfollow": "Slutt å fylgje emneknaggen", "hashtags.and_other": "…og {count, plural, one {}other {# fleire}}", "hints.profiles.followers_may_be_missing": "Kven som fylgjer denne profilen manglar kanskje.", @@ -802,11 +806,11 @@ "server_banner.about_active_users": "Personar som har brukt denne tenaren dei siste 30 dagane (Månadlege Aktive Brukarar)", "server_banner.active_users": "aktive brukarar", "server_banner.administered_by": "Administrert av:", - "server_banner.is_one_of_many": "{domain} er ein av dei mange uavhengige Mastodon-serverane du kan bruke til å delta i Fødiverset.", + "server_banner.is_one_of_many": "{domain} er ein av dei mange uavhengige Mastodon-tenarane du kan bruka til å delta i Allheimen.", "server_banner.server_stats": "Tenarstatistikk:", "sign_in_banner.create_account": "Opprett konto", - "sign_in_banner.follow_anyone": "Følg kven som helst på tvers av Fødiverset og sjå alt i kronologisk rekkjefølgje. Ingen algoritmar, reklamar eller clickbait i sikte.", - "sign_in_banner.mastodon_is": "Mastodon er den beste måten å følgje med på det som skjer.", + "sign_in_banner.follow_anyone": "Fylg kven som helst på tvers av Allheimen og sjå alt i kronologisk rekkjefylgje. Ingen algoritmar, reklame eller klikkfeller.", + "sign_in_banner.mastodon_is": "Mastodon er den beste måten å fylgja med på det som skjer.", "sign_in_banner.sign_in": "Logg inn", "sign_in_banner.sso_redirect": "Logg inn eller registrer deg", "status.admin_account": "Opne moderasjonsgrensesnitt for @{name}", @@ -872,7 +876,9 @@ "subscribed_languages.target": "Endre abonnerte språk for {target}", "tabs_bar.home": "Heim", "tabs_bar.notifications": "Varsel", + "terms_of_service.effective_as_of": "I kraft frå {date}", "terms_of_service.title": "Bruksvilkår", + "terms_of_service.upcoming_changes_on": "Komande endringar {date}", "time_remaining.days": "{number, plural, one {# dag} other {# dagar}} igjen", "time_remaining.hours": "{number, plural, one {# time} other {# timar}} igjen", "time_remaining.minutes": "{number, plural, one {# minutt} other {# minutt}} igjen", @@ -903,6 +909,12 @@ "video.expand": "Utvid video", "video.fullscreen": "Fullskjerm", "video.hide": "Gøym video", + "video.mute": "Demp", "video.pause": "Pause", - "video.play": "Spel av" + "video.play": "Spel av", + "video.skip_backward": "Hopp bakover", + "video.skip_forward": "Hopp framover", + "video.unmute": "Opphev demping", + "video.volume_down": "Volum ned", + "video.volume_up": "Volum opp" } diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index 584132ffe6..2df9216b1a 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -23,13 +23,11 @@ "account.copy": "Kopier lenke til profil", "account.direct": "Nevn @{name} privat", "account.disable_notifications": "Slutt å varsle meg når @{name} legger ut innlegg", - "account.domain_blocked": "Domene blokkert", "account.edit_profile": "Rediger profil", "account.enable_notifications": "Varsle meg når @{name} legger ut innlegg", "account.endorse": "Vis frem på profilen", "account.featured_tags.last_status_at": "Siste innlegg {date}", "account.featured_tags.last_status_never": "Ingen Innlegg", - "account.featured_tags.title": "{name} sine fremhevede emneknagger", "account.follow": "Følg", "account.follow_back": "Følg tilbake", "account.followers": "Følgere", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Demp varsler", "account.mute_short": "Demp", "account.muted": "Dempet", - "account.mutual": "Gjensidig", "account.no_bio": "Ingen beskrivelse oppgitt.", "account.open_original_page": "Gå til originalsiden", "account.posts": "Innlegg", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 616f8a64af..01c7f4838b 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -20,13 +20,11 @@ "account.copy": "Copiar lo ligam del perfil", "account.direct": "Mencionar @{name} en privat", "account.disable_notifications": "Quitar de m’avisar quand @{name} publica quicòm", - "account.domain_blocked": "Domeni amagat", "account.edit_profile": "Modificar lo perfil", "account.enable_notifications": "M’avisar quand @{name} publica quicòm", "account.endorse": "Mostrar pel perfil", "account.featured_tags.last_status_at": "Darrièra publicacion lo {date}", "account.featured_tags.last_status_never": "Cap de publicacion", - "account.featured_tags.title": "Etiquetas en avant de {name}", "account.follow": "Sègre", "account.follow_back": "Sègre en retorn", "account.followers": "Seguidors", @@ -47,7 +45,6 @@ "account.mute_notifications_short": "Amudir las notificacions", "account.mute_short": "Amudir", "account.muted": "Mes en silenci", - "account.mutual": "Mutual", "account.no_bio": "Cap de descripcion pas fornida.", "account.open_original_page": "Dobrir la pagina d’origina", "account.posts": "Tuts", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index c294bcbddb..9d8b2c46ba 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -15,7 +15,6 @@ "account.cancel_follow_request": "ਫ਼ਾਲੋ ਕਰਨ ਨੂੰ ਰੱਦ ਕਰੋ", "account.copy": "ਪਰੋਫਾਇਲ ਲਈ ਲਿੰਕ ਕਾਪੀ ਕਰੋ", "account.direct": "ਨਿੱਜੀ ਜ਼ਿਕਰ @{name}", - "account.domain_blocked": "ਡੋਮੇਨ ਉੱਤੇ ਪਾਬੰਦੀ", "account.edit_profile": "ਪਰੋਫਾਈਲ ਨੂੰ ਸੋਧੋ", "account.enable_notifications": "ਜਦੋਂ {name} ਪੋਸਟ ਕਰੇ ਤਾਂ ਮੈਨੂੰ ਸੂਚਨਾ ਦਿਓ", "account.endorse": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਫ਼ੀਚਰ", @@ -35,7 +34,6 @@ "account.mute_notifications_short": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਮੌਨ ਕਰੋ", "account.mute_short": "ਮੌਨ ਕਰੋ", "account.muted": "ਮੌਨ ਕੀਤੀਆਂ", - "account.mutual": "ਸਾਂਝੇ", "account.no_bio": "ਕੋਈ ਵਰਣਨ ਨਹੀਂ ਦਿੱਤਾ।", "account.open_original_page": "ਅਸਲ ਸਫ਼ੇ ਨੂੰ ਖੋਲ੍ਹੋ", "account.posts": "ਪੋਸਟਾਂ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index c6b2a5d412..828879d048 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -23,13 +23,11 @@ "account.copy": "Skopiuj link do profilu", "account.direct": "Napisz bezpośrednio do @{name}", "account.disable_notifications": "Przestań powiadamiać mnie o wpisach @{name}", - "account.domain_blocked": "Ukryto domenę", "account.edit_profile": "Edytuj profil", "account.enable_notifications": "Powiadamiaj mnie o wpisach @{name}", "account.endorse": "Wyróżnij na profilu", "account.featured_tags.last_status_at": "Ostatni post {date}", "account.featured_tags.last_status_never": "Brak postów", - "account.featured_tags.title": "Polecane hasztagi {name}", "account.follow": "Obserwuj", "account.follow_back": "Również obserwuj", "account.followers": "Obserwujący", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Wycisz powiadomienia", "account.mute_short": "Wycisz", "account.muted": "Wyciszony", - "account.mutual": "Znajomi", "account.no_bio": "Brak opisu.", "account.open_original_page": "Otwórz stronę oryginalną", "account.posts": "Wpisy", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 5279159d79..2010e3e74d 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -23,13 +23,11 @@ "account.copy": "Copiar link do perfil", "account.direct": "Mencione em privado @{name}", "account.disable_notifications": "Cancelar notificações de @{name}", - "account.domain_blocked": "Domínio bloqueado", "account.edit_profile": "Editar perfil", "account.enable_notifications": "Notificar novos toots de @{name}", "account.endorse": "Recomendar", "account.featured_tags.last_status_at": "Última publicação em {date}", "account.featured_tags.last_status_never": "Sem publicações", - "account.featured_tags.title": "Hashtags em destaque de {name}", "account.follow": "Seguir", "account.follow_back": "Seguir de volta", "account.followers": "Seguidores", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Silenciar notificações", "account.mute_short": "Silenciar", "account.muted": "Silenciado", - "account.mutual": "Mútuo", "account.no_bio": "Nenhuma descrição fornecida.", "account.open_original_page": "Abrir a página original", "account.posts": "Toots", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 32af518415..55ac026cd4 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -23,13 +23,11 @@ "account.copy": "Copiar hiperligação do perfil", "account.direct": "Mencionar @{name} em privado", "account.disable_notifications": "Parar de me notificar das publicações de @{name}", - "account.domain_blocked": "Domínio bloqueado", "account.edit_profile": "Editar perfil", "account.enable_notifications": "Notificar-me das publicações de @{name}", "account.endorse": "Destacar no perfil", "account.featured_tags.last_status_at": "Última publicação em {date}", "account.featured_tags.last_status_never": "Sem publicações", - "account.featured_tags.title": "Etiquetas destacadas por {name}", "account.follow": "Seguir", "account.follow_back": "Seguir também", "account.followers": "Seguidores", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Ocultar notificações", "account.mute_short": "Ocultar", "account.muted": "Ocultada", - "account.mutual": "Mútuo", "account.no_bio": "Nenhuma descrição fornecida.", "account.open_original_page": "Abrir a página original", "account.posts": "Publicações", diff --git a/app/javascript/mastodon/locales/ro.json b/app/javascript/mastodon/locales/ro.json index 56d577f3f0..ca18af81fa 100644 --- a/app/javascript/mastodon/locales/ro.json +++ b/app/javascript/mastodon/locales/ro.json @@ -23,13 +23,11 @@ "account.copy": "Copiază link-ul profilului", "account.direct": "Menționează pe @{name} în privat", "account.disable_notifications": "Nu îmi mai trimite notificări când postează @{name}", - "account.domain_blocked": "Domeniu blocat", "account.edit_profile": "Modifică profilul", "account.enable_notifications": "Trimite-mi o notificare când postează @{name}", "account.endorse": "Promovează pe profil", "account.featured_tags.last_status_at": "Ultima postare pe {date}", "account.featured_tags.last_status_never": "Fără postări", - "account.featured_tags.title": "Haștagurile recomandate de {name}", "account.follow": "Urmărește", "account.follow_back": "Urmăreşte înapoi", "account.followers": "Urmăritori", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Amuțește notificările", "account.mute_short": "Ignoră", "account.muted": "Pus pe silențios", - "account.mutual": "Mutual", "account.no_bio": "Nicio descriere furnizată.", "account.open_original_page": "Deschide pagina originală", "account.posts": "Postări", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index 15ae4b9e0a..e9831e07be 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -23,13 +23,14 @@ "account.copy": "Копировать ссылку на профиль", "account.direct": "Упомянуть @{name} лично", "account.disable_notifications": "Не уведомлять о постах от @{name}", - "account.domain_blocked": "Домен заблокирован", "account.edit_profile": "Редактировать", "account.enable_notifications": "Уведомлять о постах от @{name}", "account.endorse": "Рекомендовать в профиле", + "account.featured": "Избранное", + "account.featured.hashtags": "Хэштеги", + "account.featured.posts": "Посты", "account.featured_tags.last_status_at": "Последний пост {date}", "account.featured_tags.last_status_never": "Нет постов", - "account.featured_tags.title": "Избранные хэштеги {name}", "account.follow": "Подписаться", "account.follow_back": "Подписаться в ответ", "account.followers": "Подписчики", @@ -38,6 +39,7 @@ "account.following": "Подписки", "account.following_counter": "{count, plural, one {# подписка} many {# подписок} other {# подписки}}", "account.follows.empty": "Этот пользователь пока ни на кого не подписался.", + "account.follows_you": "Подписан(а) на вас", "account.go_to_profile": "Перейти к профилю", "account.hide_reblogs": "Скрыть продвижения от @{name}", "account.in_memoriam": "In Memoriam.", @@ -52,7 +54,7 @@ "account.mute_notifications_short": "Отключить уведомления", "account.mute_short": "Игнорировать", "account.muted": "Игнорируется", - "account.mutual": "Взаимные подписки", + "account.mutual": "Вы подписаны друг на друга", "account.no_bio": "Описание профиля отсутствует.", "account.open_original_page": "Открыть исходную страницу", "account.posts": "Посты", @@ -378,6 +380,7 @@ "generic.saved": "Сохранено", "getting_started.heading": "Добро пожаловать", "hashtag.admin_moderation": "Открыть интерфейс модератора для #{name}", + "hashtag.browse": "Посмотреть посты в #{hashtag}", "hashtag.column_header.tag_mode.all": "и {additional}", "hashtag.column_header.tag_mode.any": "или {additional}", "hashtag.column_header.tag_mode.none": "без {additional}", @@ -768,6 +771,7 @@ "report.unfollow_explanation": "Вы подписаны на этого пользователя. Чтобы не видеть его/её посты в своей домашней ленте, отпишитесь от него/неё.", "report_notification.attached_statuses": "{count, plural, one {{count} сообщение} few {{count} сообщения} many {{count} сообщений} other {{count} сообщений}} вложено", "report_notification.categories.legal": "Нарушение закона", + "report_notification.categories.legal_sentence": "запрещённый контент", "report_notification.categories.other": "Другое", "report_notification.categories.other_sentence": "другое", "report_notification.categories.spam": "Спам", diff --git a/app/javascript/mastodon/locales/ry.json b/app/javascript/mastodon/locales/ry.json index 8fd083efc3..79f6093e53 100644 --- a/app/javascript/mastodon/locales/ry.json +++ b/app/javascript/mastodon/locales/ry.json @@ -22,13 +22,11 @@ "account.copy": "Зкопіровати удкликованя на профіл", "account.direct": "Пошептати @{name}", "account.disable_notifications": "Бульше не сповіщати ми коли {name} пише", - "account.domain_blocked": "Домен заблокованый", "account.edit_profile": "Управити профіл", "account.enable_notifications": "Уповістити ня, кой {name} пише", "account.endorse": "Указовати на профілови", "account.featured_tags.last_status_at": "Датум послідньої публикації {date}", "account.featured_tags.last_status_never": "Ниє публикацій", - "account.featured_tags.title": "Ублюблені гештеґы {name}", "account.follow": "Пудписати ся", "account.follow_back": "Пудписати ся тоже", "account.followers": "Пудписникы", @@ -48,7 +46,6 @@ "account.mute_notifications_short": "Стишити голошіня", "account.mute_short": "Стишити", "account.muted": "Стишено", - "account.mutual": "Взайомно", "account.no_bio": "Описа ниє.", "account.open_original_page": "Удоперти ориґіналну сторунку", "account.posts": "Публикації", diff --git a/app/javascript/mastodon/locales/sa.json b/app/javascript/mastodon/locales/sa.json index 1ecc057023..c32a0d57cf 100644 --- a/app/javascript/mastodon/locales/sa.json +++ b/app/javascript/mastodon/locales/sa.json @@ -20,13 +20,11 @@ "account.cancel_follow_request": "अनुसरणयाचनामपनय", "account.direct": "गोपनीयरूपेण उल्लेखित-@{name}", "account.disable_notifications": "यदा @{name} स्थापयति तदा माम्मा ज्ञापय", - "account.domain_blocked": "प्रदेशो निषिद्धः", "account.edit_profile": "सम्पाद्यताम्", "account.enable_notifications": "यदा @{name} स्थापयति तदा मां ज्ञापय", "account.endorse": "व्यक्तिगतविवरणे वैशिष्ट्यम्", "account.featured_tags.last_status_at": "{date} दिने गतस्थापनम्", "account.featured_tags.last_status_never": "न पत्रम्", - "account.featured_tags.title": "{name} इत्यस्य विशेषहैस्टैगः", "account.follow": "अनुस्रियताम्", "account.followers": "अनुसर्तारः", "account.followers.empty": "नाऽनुसर्तारो वर्तन्ते", diff --git a/app/javascript/mastodon/locales/sc.json b/app/javascript/mastodon/locales/sc.json index c885078a73..c28c7f4f8d 100644 --- a/app/javascript/mastodon/locales/sc.json +++ b/app/javascript/mastodon/locales/sc.json @@ -23,13 +23,11 @@ "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_blocked": "Domìniu blocadu", "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.featured_tags.last_status_at": "Ùrtima publicatzione in su {date}", "account.featured_tags.last_status_never": "Peruna publicatzione", - "account.featured_tags.title": "Etichetas de {name} in evidèntzia", "account.follow": "Sighi", "account.follow_back": "Sighi tue puru", "account.followers": "Sighiduras", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Pone is notìficas a sa muda", "account.mute_short": "A sa muda", "account.muted": "A sa muda", - "account.mutual": "Pari-pari", "account.no_bio": "Peruna descritzione frunida.", "account.open_original_page": "Aberi sa pàgina originale", "account.posts": "Publicatziones", diff --git a/app/javascript/mastodon/locales/sco.json b/app/javascript/mastodon/locales/sco.json index 5960fb7760..ac037c1464 100644 --- a/app/javascript/mastodon/locales/sco.json +++ b/app/javascript/mastodon/locales/sco.json @@ -19,13 +19,11 @@ "account.blocked": "Dingied", "account.cancel_follow_request": "Resile follae requeest", "account.disable_notifications": "Stap notifyin me whan @{name} posts", - "account.domain_blocked": "Domain dingied", "account.edit_profile": "Edit profile", "account.enable_notifications": "Notify me whan @{name} posts", "account.endorse": "Shaw oan profile", "account.featured_tags.last_status_at": "Last post oan {date}", "account.featured_tags.last_status_never": "Nae posts", - "account.featured_tags.title": "{name}'s hielichtit hashtags", "account.follow": "Follae", "account.followers": "Follaers", "account.followers.empty": "Naebody follaes this uiser yit.", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index 7d909dbe1a..ef41d3c7dc 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -12,7 +12,6 @@ "account.block_short": "අවහිර", "account.blocked": "අවහිර කර ඇත", "account.disable_notifications": "@{name} පළ කරන විට මට දැනුම් නොදෙන්න", - "account.domain_blocked": "වසම අවහිර කර ඇත", "account.edit_profile": "පැතිකඩ සංස්කරණය", "account.enable_notifications": "@{name} පළ කරන විට මට දැනුම් දෙන්න", "account.endorse": "පැතිකඩෙහි විශේෂාංගය", diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json index d6aff6b56c..0a61462efb 100644 --- a/app/javascript/mastodon/locales/sk.json +++ b/app/javascript/mastodon/locales/sk.json @@ -23,13 +23,12 @@ "account.copy": "Skopírovať odkaz na profil", "account.direct": "Súkromne označiť @{name}", "account.disable_notifications": "Zrušiť upozornenia na príspevky od @{name}", - "account.domain_blocked": "Doména blokovaná", "account.edit_profile": "Upraviť profil", "account.enable_notifications": "Zapnúť upozornenia na príspevky od @{name}", "account.endorse": "Zobraziť na vlastnom profile", + "account.featured.posts": "Príspevky", "account.featured_tags.last_status_at": "Posledný príspevok dňa {date}", "account.featured_tags.last_status_never": "Žiadne príspevky", - "account.featured_tags.title": "Odporúčané hashtagy účtu {name}", "account.follow": "Sledovať", "account.follow_back": "Sledovať späť", "account.followers": "Sledovatelia", @@ -52,7 +51,6 @@ "account.mute_notifications_short": "Stíšiť upozornenia", "account.mute_short": "Stíšiť", "account.muted": "Účet stíšený", - "account.mutual": "Spoločné", "account.no_bio": "Nie je uvedený žiadny popis.", "account.open_original_page": "Otvoriť pôvodnú stránku", "account.posts": "Príspevky", @@ -346,6 +344,7 @@ "generic.saved": "Uložené", "getting_started.heading": "Začíname", "hashtag.admin_moderation": "Otvor moderovacie rozhranie pre #{name}", + "hashtag.browse": "Prehľadávať príspevky pod #{hashtag}", "hashtag.column_header.tag_mode.all": "a {additional}", "hashtag.column_header.tag_mode.any": "alebo {additional}", "hashtag.column_header.tag_mode.none": "bez {additional}", @@ -359,6 +358,7 @@ "hashtag.counter_by_uses": "{count, plural, one {{counter} príspevok} few {{counter} príspevky} many {{counter} príspevkov} other {{counter} príspevkov}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} príspevok} few {{counter} príspevky} many {{counter} príspevkov} other {{counter} príspevkov}} dnes", "hashtag.follow": "Sledovať hashtag", + "hashtag.mute": "Utlmiť #{hashtag}", "hashtag.unfollow": "Prestať sledovať hashtag", "hashtags.and_other": "…a {count, plural, other {# ďalších}}", "hints.profiles.followers_may_be_missing": "Nasledovatelia tohto profilu môžu chýbať.", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index e6c0b67427..547a910cf3 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -23,13 +23,11 @@ "account.copy": "Kopiraj povezavo do profila", "account.direct": "Zasebno omeni @{name}", "account.disable_notifications": "Ne obveščaj me več, ko ima @{name} novo objavo", - "account.domain_blocked": "Blokirana domena", "account.edit_profile": "Uredi profil", "account.enable_notifications": "Obvesti me, ko ima @{name} novo objavo", "account.endorse": "Izpostavi v profilu", "account.featured_tags.last_status_at": "Zadnja objava {date}", "account.featured_tags.last_status_never": "Ni objav", - "account.featured_tags.title": "Izpostavljeni ključniki osebe {name}", "account.follow": "Sledi", "account.follow_back": "Sledi nazaj", "account.followers": "Sledilci", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Utišaj obvestila", "account.mute_short": "Utišaj", "account.muted": "Utišan", - "account.mutual": "Vzajemno", "account.no_bio": "Ni opisa.", "account.open_original_page": "Odpri izvirno stran", "account.posts": "Objave", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index a80b3df80d..5586edf450 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -19,17 +19,20 @@ "account.block_domain": "Blloko përkatësinë {domain}", "account.block_short": "Bllokoje", "account.blocked": "E bllokuar", + "account.blocking": "Bllokim", "account.cancel_follow_request": "Tërhiq mbrapsht kërkesë për ndjekje", "account.copy": "Kopjoje lidhjen te profili", "account.direct": "Përmendje private për @{name}", "account.disable_notifications": "Resht së njoftuari mua, kur poston @{name}", - "account.domain_blocked": "Përkatësia u bllokua", + "account.domain_blocking": "Bllokim përkatësie", "account.edit_profile": "Përpunoni profilin", "account.enable_notifications": "Njoftomë, kur poston @{name}", "account.endorse": "Pasqyrojeni në profil", + "account.featured": "Të zgjedhur", + "account.featured.hashtags": "Hashtag-ë", + "account.featured.posts": "Postime", "account.featured_tags.last_status_at": "Postimi i fundit më {date}", "account.featured_tags.last_status_never": "Pa postime", - "account.featured_tags.title": "Hashtagë të zgjedhur të {name}", "account.follow": "Ndiqeni", "account.follow_back": "Ndiqe gjithashtu", "account.followers": "Ndjekës", @@ -38,6 +41,7 @@ "account.following": "Ndjekje", "account.following_counter": "{count, plural, one {{counter} i ndjekur} other {{counter} të ndjekur}}", "account.follows.empty": "Ky përdorues ende s’ndjek kënd.", + "account.follows_you": "Ju ndjek", "account.go_to_profile": "Kalo te profili", "account.hide_reblogs": "Fshih përforcime nga @{name}", "account.in_memoriam": "In Memoriam.", @@ -52,7 +56,8 @@ "account.mute_notifications_short": "Mos shfaq njoftime", "account.mute_short": "Mos i shfaq", "account.muted": "Heshtuar", - "account.mutual": "Reciproke", + "account.muting": "Heshtim", + "account.mutual": "Ndiqni njëri-tjetrin", "account.no_bio": "S’u dha përshkrim.", "account.open_original_page": "Hap faqen origjinale", "account.posts": "Mesazhe", @@ -60,6 +65,7 @@ "account.report": "Raportojeni @{name}", "account.requested": "Në pritje të miratimit. Që të anuloni kërkesën për ndjekje, klikojeni", "account.requested_follow": "{name} ka kërkuar t’ju ndjekë", + "account.requests_to_follow_you": "Kërkesa për t’ju ndjekur", "account.share": "Ndajeni profilin e @{name} me të tjerët", "account.show_reblogs": "Shfaq përforcime nga @{name}", "account.statuses_counter": "{count, plural, one {{counter} postim} other {{counter} postime}}", @@ -289,6 +295,9 @@ "emoji_button.search_results": "Përfundime kërkimi", "emoji_button.symbols": "Simbole", "emoji_button.travel": "Udhëtime & Vende", + "empty_column.account_featured.me": "Ende s’keni paraqitur gjë si të zgjedhur. E dinit se mund të përdorni si të tilla në profilin tuaj postime, hashtag-ë që përdorni më shpesh dhe madje llogaritë e shokëve tuaj?", + "empty_column.account_featured.other": "{acct} s’ka gjë si të zgjedhur. E dinit se mund të përdorni si të tilla në profilin tuaj postime, hashtag-ë që përdorni më shpesh dhe madje llogaritë e shokëve tuaj?", + "empty_column.account_featured_other.unknown": "Kjo llogari s’ka ende gjë të zgjedhur.", "empty_column.account_hides_collections": "Ky përdorues ka zgjedhur të mos e japë këtë informacion", "empty_column.account_suspended": "Llogaria u pezullua", "empty_column.account_timeline": "S’ka mesazhe këtu!", @@ -373,6 +382,8 @@ "generic.saved": "U ruajt", "getting_started.heading": "Si t’ia fillohet", "hashtag.admin_moderation": "Hap ndërfaqe moderimi për #{name}", + "hashtag.browse": "Shfletoni postime me #{hashtag}", + "hashtag.browse_from_account": "Shfletoni postime nga @{name} me #{hashtag}", "hashtag.column_header.tag_mode.all": "dhe {additional}", "hashtag.column_header.tag_mode.any": "ose {additional}", "hashtag.column_header.tag_mode.none": "pa {additional}", @@ -386,6 +397,7 @@ "hashtag.counter_by_uses": "{count, plural, one {{counter} postim} other {{counter} postime}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} postim} other {{counter} postime}} sot", "hashtag.follow": "Ndiqe hashtag-un", + "hashtag.mute": "Heshtoje #{hashtag}", "hashtag.unfollow": "Hiqe ndjekjen e hashtag-ut", "hashtags.and_other": "…dhe {count, plural, one {}other {# më tepër}}", "hints.profiles.followers_may_be_missing": "Mund të mungojnë ndjekës për këtë profil.", diff --git a/app/javascript/mastodon/locales/sr-Latn.json b/app/javascript/mastodon/locales/sr-Latn.json index 3d077cbe22..06aac93189 100644 --- a/app/javascript/mastodon/locales/sr-Latn.json +++ b/app/javascript/mastodon/locales/sr-Latn.json @@ -22,13 +22,11 @@ "account.copy": "Kopiraj vezu u profil", "account.direct": "Privatno pomeni @{name}", "account.disable_notifications": "Zaustavi obaveštavanje za objave korisnika @{name}", - "account.domain_blocked": "Domen je blokiran", "account.edit_profile": "Uredi profil", "account.enable_notifications": "Obavesti me kada @{name} objavi", "account.endorse": "Istakni na profilu", "account.featured_tags.last_status_at": "Poslednja objava {date}", "account.featured_tags.last_status_never": "Nema objava", - "account.featured_tags.title": "Istaknute heš oznake korisnika {name}", "account.follow": "Prati", "account.follow_back": "Uzvrati praćenje", "account.followers": "Pratioci", @@ -51,7 +49,6 @@ "account.mute_notifications_short": "Isključi obaveštenja", "account.mute_short": "Isključi", "account.muted": "Ignorisan", - "account.mutual": "Zajednički", "account.no_bio": "Nema opisa.", "account.open_original_page": "Otvori originalnu stranicu", "account.posts": "Objave", diff --git a/app/javascript/mastodon/locales/sr.json b/app/javascript/mastodon/locales/sr.json index 43c57b3e25..ba972db844 100644 --- a/app/javascript/mastodon/locales/sr.json +++ b/app/javascript/mastodon/locales/sr.json @@ -22,13 +22,11 @@ "account.copy": "Копирај везу у профил", "account.direct": "Приватно помени @{name}", "account.disable_notifications": "Заустави обавештавање за објаве корисника @{name}", - "account.domain_blocked": "Домен је блокиран", "account.edit_profile": "Уреди профил", "account.enable_notifications": "Обавести ме када @{name} објави", "account.endorse": "Истакни на профилу", "account.featured_tags.last_status_at": "Последња објава {date}", "account.featured_tags.last_status_never": "Нема објава", - "account.featured_tags.title": "Истакнуте хеш ознаке корисника {name}", "account.follow": "Прати", "account.follow_back": "Узврати праћење", "account.followers": "Пратиоци", @@ -51,7 +49,6 @@ "account.mute_notifications_short": "Искључи обавештења", "account.mute_short": "Искључи", "account.muted": "Игнорисан", - "account.mutual": "Заједнички", "account.no_bio": "Нема описа.", "account.open_original_page": "Отвори оригиналну страницу", "account.posts": "Објаве", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 1e2e9c585b..6af287e6cc 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -23,13 +23,11 @@ "account.copy": "Kopiera länk till profil", "account.direct": "Nämn @{name} privat", "account.disable_notifications": "Sluta meddela mig när @{name} skriver ett inlägg", - "account.domain_blocked": "Domän blockerad", "account.edit_profile": "Redigera profil", "account.enable_notifications": "Notifiera mig när @{name} gör inlägg", "account.endorse": "Visa på profil", "account.featured_tags.last_status_at": "Senaste inlägg den {date}", "account.featured_tags.last_status_never": "Inga inlägg", - "account.featured_tags.title": "{name}s utvalda hashtaggar", "account.follow": "Följ", "account.follow_back": "Följ tillbaka", "account.followers": "Följare", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "Stäng av aviseringsljud", "account.mute_short": "Tysta", "account.muted": "Tystad", - "account.mutual": "Ömsesidig", "account.no_bio": "Ingen beskrivning angiven.", "account.open_original_page": "Öppna den ursprungliga sidan", "account.posts": "Inlägg", diff --git a/app/javascript/mastodon/locales/szl.json b/app/javascript/mastodon/locales/szl.json index b7b930422c..5250e12a8b 100644 --- a/app/javascript/mastodon/locales/szl.json +++ b/app/javascript/mastodon/locales/szl.json @@ -15,7 +15,6 @@ "account.block": "Zablokuj @{name}", "account.block_domain": "Zablokuj domena {domain}", "account.cancel_follow_request": "Withdraw follow request", - "account.domain_blocked": "Domena zablokowanŏ", "account.media": "Mydia", "account.mute": "Wycisz @{name}", "account.posts": "Toots", diff --git a/app/javascript/mastodon/locales/ta.json b/app/javascript/mastodon/locales/ta.json index 69c0029b7f..29446835f6 100644 --- a/app/javascript/mastodon/locales/ta.json +++ b/app/javascript/mastodon/locales/ta.json @@ -13,7 +13,6 @@ "account.blocked": "முடக்கப்பட்டது", "account.cancel_follow_request": "Withdraw follow request", "account.disable_notifications": "@{name} பதிவிட்டல் எனக்கு தெரியபடுத்த வேண்டாம்", - "account.domain_blocked": "மறைக்கப்பட்டத் தளங்கள்", "account.edit_profile": "சுயவிவரத்தை மாற்று", "account.enable_notifications": "@{name} பதிவிட்டல் எனக்குத் தெரியப்படுத்தவும்", "account.endorse": "சுயவிவரத்தில் வெளிப்படுத்து", diff --git a/app/javascript/mastodon/locales/te.json b/app/javascript/mastodon/locales/te.json index 959c380787..4d50b6cb14 100644 --- a/app/javascript/mastodon/locales/te.json +++ b/app/javascript/mastodon/locales/te.json @@ -5,7 +5,6 @@ "account.block_domain": "{domain} నుంచి అన్నీ దాచిపెట్టు", "account.blocked": "బ్లాక్ అయినవి", "account.cancel_follow_request": "Withdraw follow request", - "account.domain_blocked": "డొమైన్ దాచిపెట్టబడినది", "account.edit_profile": "ప్రొఫైల్ని సవరించండి", "account.endorse": "ప్రొఫైల్లో చూపించు", "account.follow": "అనుసరించు", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index d9e607f856..a3143c26f0 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -23,13 +23,11 @@ "account.copy": "คัดลอกลิงก์ไปยังโปรไฟล์", "account.direct": "กล่าวถึง @{name} แบบส่วนตัว", "account.disable_notifications": "หยุดแจ้งเตือนฉันเมื่อ @{name} โพสต์", - "account.domain_blocked": "ปิดกั้นโดเมนอยู่", "account.edit_profile": "แก้ไขโปรไฟล์", "account.enable_notifications": "แจ้งเตือนฉันเมื่อ @{name} โพสต์", "account.endorse": "แสดงในโปรไฟล์", "account.featured_tags.last_status_at": "โพสต์ล่าสุดเมื่อ {date}", "account.featured_tags.last_status_never": "ไม่มีโพสต์", - "account.featured_tags.title": "แฮชแท็กที่น่าสนใจของ {name}", "account.follow": "ติดตาม", "account.follow_back": "ติดตามกลับ", "account.followers": "ผู้ติดตาม", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "ซ่อนการแจ้งเตือน", "account.mute_short": "ซ่อน", "account.muted": "ซ่อนอยู่", - "account.mutual": "คนที่มีร่วมกัน", "account.no_bio": "ไม่ได้ให้คำอธิบาย", "account.open_original_page": "เปิดหน้าดั้งเดิม", "account.posts": "โพสต์", diff --git a/app/javascript/mastodon/locales/tok.json b/app/javascript/mastodon/locales/tok.json index 59dd34eb5c..81a2c9bbbc 100644 --- a/app/javascript/mastodon/locales/tok.json +++ b/app/javascript/mastodon/locales/tok.json @@ -23,13 +23,11 @@ "account.copy": "o pali same e linja pi lipu jan", "account.direct": "len la o mu e @{name}", "account.disable_notifications": "@{name} li toki la o mu ala e mi", - "account.domain_blocked": "sina wile ala lukin e ma ni", "account.edit_profile": "o ante e lipu mi", "account.enable_notifications": "@{name} li toki la o toki e toki ona tawa mi", "account.endorse": "lipu jan la o suli e ni", "account.featured_tags.last_status_at": "sitelen pini pi jan ni li lon tenpo {date}", "account.featured_tags.last_status_never": "toki ala li lon", - "account.featured_tags.title": "{name} la kulupu ni pi toki suli li pona", "account.follow": "o kute", "account.follow_back": "jan ni li kute e sina. o kute", "account.followers": "jan kute", @@ -52,7 +50,6 @@ "account.mute_notifications_short": "o kute ala e mu tan jan ni", "account.mute_short": "o kute ala", "account.muted": "sina kute ala e jan ni", - "account.mutual": "jan pona sona", "account.no_bio": "lipu li weka.", "account.open_original_page": "o open e lipu open", "account.posts": "toki suli", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index a33317ac9c..edc7b3db72 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -19,17 +19,20 @@ "account.block_domain": "{domain} alan adını engelle", "account.block_short": "Engelle", "account.blocked": "Engellendi", + "account.blocking": "Engelleme", "account.cancel_follow_request": "Takip isteğini geri çek", "account.copy": "Gönderi bağlantısını kopyala", "account.direct": "@{name} kullanıcısına özel olarak değin", "account.disable_notifications": "@{name} kişisinin gönderi bildirimlerini kapat", - "account.domain_blocked": "Alan adı engellendi", + "account.domain_blocking": "Alan adını engelleme", "account.edit_profile": "Profili düzenle", "account.enable_notifications": "@{name} kişisinin gönderi bildirimlerini aç", "account.endorse": "Profilimde öne çıkar", + "account.featured": "Öne çıkan", + "account.featured.hashtags": "Etiketler", + "account.featured.posts": "Gönderiler", "account.featured_tags.last_status_at": "Son gönderinin tarihi {date}", "account.featured_tags.last_status_never": "Gönderi yok", - "account.featured_tags.title": "{name} kişisinin öne çıkan etiketleri", "account.follow": "Takip et", "account.follow_back": "Geri takip et", "account.followers": "Takipçi", @@ -38,6 +41,7 @@ "account.following": "Takip Ediliyor", "account.following_counter": "{count, plural, one {{counter} takip edilen} other {{counter} takip edilen}}", "account.follows.empty": "Bu kullanıcı henüz kimseyi takip etmiyor.", + "account.follows_you": "Seni takip ediyor", "account.go_to_profile": "Profile git", "account.hide_reblogs": "@{name} kişisinin boostlarını gizle", "account.in_memoriam": "Hatırasına.", @@ -52,7 +56,8 @@ "account.mute_notifications_short": "Bildirimleri sessize al", "account.mute_short": "Sessize al", "account.muted": "Susturuldu", - "account.mutual": "Karşılıklı", + "account.muting": "Sessize alınıyor", + "account.mutual": "Birbirinizi takip ediyorsunuz", "account.no_bio": "Herhangi bir açıklama belirtilmedi.", "account.open_original_page": "Asıl sayfayı aç", "account.posts": "Gönderiler", @@ -60,6 +65,7 @@ "account.report": "@{name} adlı kişiyi bildir", "account.requested": "Onay bekleniyor. Takip isteğini iptal etmek için tıklayın", "account.requested_follow": "{name} size takip isteği gönderdi", + "account.requests_to_follow_you": "Size takip isteği gönderdi", "account.share": "@{name} adlı kişinin profilini paylaş", "account.show_reblogs": "@{name} kişisinin yeniden paylaşımlarını göster", "account.statuses_counter": "{count, plural, one {{counter} gönderi} other {{counter} gönderi}}", @@ -294,6 +300,9 @@ "emoji_button.search_results": "Arama sonuçları", "emoji_button.symbols": "Semboller", "emoji_button.travel": "Seyahat ve Yerler", + "empty_column.account_featured.me": "Henüz hiçbir şeyi öne çıkarmadınız. Gönderilerinizi, en çok kullandığınız etiketleri ve hatta arkadaşlarınızın hesaplarını profilinizde öne çıkarabileceğinizi biliyor muydunuz?", + "empty_column.account_featured.other": "{acct} henüz hiçbir şeyi öne çıkarmadı. Gönderilerinizi, en çok kullandığınız etiketleri ve hatta arkadaşlarınızın hesaplarını profilinizde öne çıkarabileceğinizi biliyor muydunuz?", + "empty_column.account_featured_other.unknown": "Bu hesap henüz hiçbir şeyi öne çıkarmadı.", "empty_column.account_hides_collections": "Bu kullanıcı bu bilgiyi sağlamayı tercih etmemiştir", "empty_column.account_suspended": "Hesap askıya alındı", "empty_column.account_timeline": "Burada hiç gönderi yok!", @@ -378,6 +387,8 @@ "generic.saved": "Kaydet", "getting_started.heading": "Başlarken", "hashtag.admin_moderation": "#{name} için denetim arayüzünü açın", + "hashtag.browse": "#{hashtag} gönderilerine gözat", + "hashtag.browse_from_account": "@{name} kişisinin #{hashtag} gönderilerine gözat", "hashtag.column_header.tag_mode.all": "ve {additional}", "hashtag.column_header.tag_mode.any": "ya da {additional}", "hashtag.column_header.tag_mode.none": "{additional} olmadan", @@ -391,6 +402,7 @@ "hashtag.counter_by_uses": "{count, plural, one {{counter} gönderi} other {{counter} gönderi}}", "hashtag.counter_by_uses_today": "bugün {count, plural, one {{counter} gönderi} other {{counter} gönderi}}", "hashtag.follow": "Etiketi takip et", + "hashtag.mute": "#{hashtag} gönderilerini sessize al", "hashtag.unfollow": "Etiketi takibi bırak", "hashtags.and_other": "…ve {count, plural, one {}other {# fazlası}}", "hints.profiles.followers_may_be_missing": "Bu profilin takipçileri eksik olabilir.", diff --git a/app/javascript/mastodon/locales/tt.json b/app/javascript/mastodon/locales/tt.json index 602d676361..39abff4503 100644 --- a/app/javascript/mastodon/locales/tt.json +++ b/app/javascript/mastodon/locales/tt.json @@ -21,13 +21,11 @@ "account.cancel_follow_request": "Киләсе сорау", "account.copy": "Профиль сылтамасын күчереп ал", "account.disable_notifications": "@{name} язулары өчен белдерүләр сүндерү", - "account.domain_blocked": "Домен блокланган", "account.edit_profile": "Профильне үзгәртү", "account.enable_notifications": "@{name} язулары өчен белдерүләр яндыру", "account.endorse": "Профильдә тәкъдим итү", "account.featured_tags.last_status_at": "Соңгы хәбәр {date}", "account.featured_tags.last_status_never": "Хәбәрләр юк", - "account.featured_tags.title": "{name} тәкъдим ителгән хэштеглар", "account.follow": "Язылу", "account.followers": "Язылучы", "account.followers.empty": "Әле беркем дә язылмаган.", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index eb7931f02c..ffb96b816c 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -23,13 +23,14 @@ "account.copy": "Копіювати посилання на профіль", "account.direct": "Особиста згадка @{name}", "account.disable_notifications": "Не повідомляти мене про дописи @{name}", - "account.domain_blocked": "Домен заблоковано", "account.edit_profile": "Редагувати профіль", "account.enable_notifications": "Повідомляти мене про дописи @{name}", "account.endorse": "Рекомендувати у моєму профілі", + "account.featured": "Рекомендоване", + "account.featured.hashtags": "Хештеги", + "account.featured.posts": "Дописи", "account.featured_tags.last_status_at": "Останній допис {date}", "account.featured_tags.last_status_never": "Немає дописів", - "account.featured_tags.title": "{name} виділяє хештеґи", "account.follow": "Підписатися", "account.follow_back": "Стежити також", "account.followers": "Підписники", @@ -38,6 +39,7 @@ "account.following": "Ви стежите", "account.following_counter": "{count, plural, one {{counter} підписка} few {{counter} підписки} many {{counter} підписок} other {{counter} підписка}}", "account.follows.empty": "Цей користувач ще ні на кого не підписався.", + "account.follows_you": "Підписаний(-а) на вас", "account.go_to_profile": "Перейти до профілю", "account.hide_reblogs": "Сховати поширення від @{name}", "account.in_memoriam": "Пам'ятник.", @@ -52,7 +54,6 @@ "account.mute_notifications_short": "Не сповіщати", "account.mute_short": "Ігнорувати", "account.muted": "Приховується", - "account.mutual": "Взаємно", "account.no_bio": "Немає опису.", "account.open_original_page": "Відкрити оригінальну сторінку", "account.posts": "Дописи", @@ -391,6 +392,7 @@ "hashtag.counter_by_uses": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} допис} few {{counter} дописи} many {{counter} дописів} other {{counter} допис}} сьогодні", "hashtag.follow": "Стежити за хештегом", + "hashtag.mute": "Ігнорувати #{hashtag}", "hashtag.unfollow": "Не стежити за хештегом", "hashtags.and_other": "…і {count, plural, other {ще #}}", "hints.profiles.followers_may_be_missing": "Підписники цього профілю можуть бути не показані.", diff --git a/app/javascript/mastodon/locales/ur.json b/app/javascript/mastodon/locales/ur.json index cd50b512b4..706541ffb9 100644 --- a/app/javascript/mastodon/locales/ur.json +++ b/app/javascript/mastodon/locales/ur.json @@ -16,13 +16,11 @@ "account.cancel_follow_request": "Withdraw follow request", "account.direct": "نجی طور پر @{name} کا ذکر کریں", "account.disable_notifications": "جب @{name} پوسٹ کرے تو مجھ مطلع نہ کریں", - "account.domain_blocked": "پوشیدہ ڈومین", "account.edit_profile": "مشخص ترمیم کریں", "account.enable_notifications": "جب @{name} پوسٹ کرے تو مجھ مطلع کریں", "account.endorse": "مشکص پر نمایاں کریں", "account.featured_tags.last_status_at": "آخری پوسٹ {date} کو", "account.featured_tags.last_status_never": "کوئی مراسلہ نہیں", - "account.featured_tags.title": "{name} کے نمایاں ہیش ٹیگز", "account.follow": "پیروی کریں", "account.follow_back": "اکاؤنٹ کو فالو بیک ", "account.followers": "پیروکار", @@ -43,7 +41,6 @@ "account.mute_notifications_short": "نوٹیفیکیشنز کو خاموش کریں", "account.mute_short": "خاموش", "account.muted": "خاموش کردہ", - "account.mutual": "میوچول اکاؤنٹ", "account.no_bio": "کوئی تفصیل نہیں دی گئی۔", "account.open_original_page": "اصل صفحہ کھولیں", "account.posts": "ٹوٹ", diff --git a/app/javascript/mastodon/locales/uz.json b/app/javascript/mastodon/locales/uz.json index e58ab35444..92673f9044 100644 --- a/app/javascript/mastodon/locales/uz.json +++ b/app/javascript/mastodon/locales/uz.json @@ -19,13 +19,11 @@ "account.blocked": "Bloklangan", "account.cancel_follow_request": "Kuzatuv so‘rovini bekor qilish", "account.disable_notifications": "@{name} post qo‘yganida menga xabar berishni to‘xtating", - "account.domain_blocked": "Domen bloklangan", "account.edit_profile": "Profilni tahrirlash", "account.enable_notifications": "@{name} post qo‘yganida menga xabar olish", "account.endorse": "Profildagi xususiyat", "account.featured_tags.last_status_at": "Oxirgi post: {date}", "account.featured_tags.last_status_never": "Habarlar yo'q", - "account.featured_tags.title": "{name} ning taniqli hashtaglari", "account.follow": "Obuna bo‘lish", "account.followers": "Obunachilar", "account.followers.empty": "Bu foydalanuvchini hali hech kim kuzatmaydi.", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 21a7e5da47..1e435af2a3 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -19,17 +19,20 @@ "account.block_domain": "Chặn mọi thứ từ {domain}", "account.block_short": "Chặn", "account.blocked": "Đã chặn", + "account.blocking": "Đang chặn", "account.cancel_follow_request": "Thu hồi yêu cầu theo dõi", "account.copy": "Sao chép địa chỉ", "account.direct": "Nhắn riêng @{name}", "account.disable_notifications": "Tắt thông báo khi @{name} đăng tút", - "account.domain_blocked": "Tên miền đã chặn", + "account.domain_blocking": "Máy chủ đang chủ", "account.edit_profile": "Sửa hồ sơ", "account.enable_notifications": "Nhận thông báo khi @{name} đăng tút", "account.endorse": "Tôn vinh người này", + "account.featured": "Nêu bật", + "account.featured.hashtags": "Hashtag", + "account.featured.posts": "Tút", "account.featured_tags.last_status_at": "Tút gần nhất {date}", "account.featured_tags.last_status_never": "Chưa có tút", - "account.featured_tags.title": "Hashtag của {name}", "account.follow": "Theo dõi", "account.follow_back": "Theo dõi lại", "account.followers": "Người theo dõi", @@ -38,6 +41,7 @@ "account.following": "Đang theo dõi", "account.following_counter": "{count, plural, other {{counter} Đang theo dõi}}", "account.follows.empty": "Người này chưa theo dõi ai.", + "account.follows_you": "Đang theo dõi bạn", "account.go_to_profile": "Xem hồ sơ", "account.hide_reblogs": "Ẩn tút @{name} đăng lại", "account.in_memoriam": "Tưởng Niệm.", @@ -52,7 +56,8 @@ "account.mute_notifications_short": "Ẩn thông báo", "account.mute_short": "Ẩn", "account.muted": "Đã ẩn", - "account.mutual": "Đang theo dõi nhau", + "account.muting": "Đang ẩn", + "account.mutual": "Theo dõi nhau", "account.no_bio": "Chưa có miêu tả.", "account.open_original_page": "Mở trang gốc", "account.posts": "Tút", @@ -60,6 +65,7 @@ "account.report": "Báo cáo @{name}", "account.requested": "Đang chờ chấp thuận. Nhấp vào đây để hủy yêu cầu theo dõi", "account.requested_follow": "{name} yêu cầu theo dõi bạn", + "account.requests_to_follow_you": "Yêu cầu theo dõi bạn", "account.share": "Chia sẻ @{name}", "account.show_reblogs": "Hiện tút do @{name} đăng lại", "account.statuses_counter": "{count, plural, other {{counter} Tút}}", @@ -294,6 +300,9 @@ "emoji_button.search_results": "Kết quả tìm kiếm", "emoji_button.symbols": "Biểu tượng", "emoji_button.travel": "Du lịch", + "empty_column.account_featured.me": "Bạn chưa có nội dung nêu bật. Bạn có biết rằng bạn có thể giới thiệu tút, hashtag mà bạn sử dụng nhiều nhất và thậm chí cả tài khoản của bạn bè trên trang cá nhân của mình không?", + "empty_column.account_featured.other": "{acct} chưa nêu bật gì. Bạn có biết rằng bạn có thể giới thiệu tút, hashtag mà bạn sử dụng nhiều nhất và thậm chí cả tài khoản của bạn bè trên trang cá nhân của mình không?", + "empty_column.account_featured_other.unknown": "Người này chưa nêu bật nội dung gì.", "empty_column.account_hides_collections": "Người này đã chọn ẩn thông tin", "empty_column.account_suspended": "Tài khoản vô hiệu hóa", "empty_column.account_timeline": "Chưa có tút nào!", @@ -378,6 +387,8 @@ "generic.saved": "Đã lưu", "getting_started.heading": "Quản lý", "hashtag.admin_moderation": "Mở giao diện quản trị #{name}", + "hashtag.browse": "Tìm tút #{hashtag}", + "hashtag.browse_from_account": "Tìm tút của @{name} có chứa #{hashtag}", "hashtag.column_header.tag_mode.all": "và {additional}", "hashtag.column_header.tag_mode.any": "hoặc {additional}", "hashtag.column_header.tag_mode.none": "mà không {additional}", @@ -391,6 +402,7 @@ "hashtag.counter_by_uses": "{count, plural, other {{counter} tút}}", "hashtag.counter_by_uses_today": "{count, plural, other {{counter} tút}} hôm nay", "hashtag.follow": "Theo dõi hashtag", + "hashtag.mute": "Ẩn #{hashtag}", "hashtag.unfollow": "Bỏ theo dõi hashtag", "hashtags.and_other": "…và {count, plural, other {# nữa}}", "hints.profiles.followers_may_be_missing": "Số người theo dõi có thể không đầy đủ.", diff --git a/app/javascript/mastodon/locales/zgh.json b/app/javascript/mastodon/locales/zgh.json index 334365acbe..48f5f51626 100644 --- a/app/javascript/mastodon/locales/zgh.json +++ b/app/javascript/mastodon/locales/zgh.json @@ -6,7 +6,6 @@ "account.block_domain": "ⴳⴷⵍ ⵉⴳⵔ {domain}", "account.blocked": "ⵉⵜⵜⵓⴳⴷⵍ", "account.cancel_follow_request": "Withdraw follow request", - "account.domain_blocked": "ⵉⵜⵜⵓⴳⴷⵍ ⵉⴳⵔ", "account.edit_profile": "ⵙⵏⴼⵍ ⵉⴼⵔⵙ", "account.follow": "ⴹⴼⵕ", "account.followers": "ⵉⵎⴹⴼⴰⵕⵏ", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 050e1308b6..90c689289e 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -19,17 +19,20 @@ "account.block_domain": "屏蔽 {domain} 实例", "account.block_short": "屏蔽", "account.blocked": "已屏蔽", + "account.blocking": "屏蔽中", "account.cancel_follow_request": "撤回关注请求", "account.copy": "复制个人资料链接", "account.direct": "私下提及 @{name}", "account.disable_notifications": "当 @{name} 发布嘟文时不要通知我", - "account.domain_blocked": "域名已屏蔽", + "account.domain_blocking": "正在屏蔽中的域名", "account.edit_profile": "修改个人资料", "account.enable_notifications": "当 @{name} 发布嘟文时通知我", "account.endorse": "在个人资料中推荐此用户", + "account.featured": "精选", + "account.featured.hashtags": "话题", + "account.featured.posts": "嘟文", "account.featured_tags.last_status_at": "上次发言于 {date}", "account.featured_tags.last_status_never": "暂无嘟文", - "account.featured_tags.title": "{name} 的精选标签", "account.follow": "关注", "account.follow_back": "回关", "account.followers": "关注者", @@ -38,6 +41,7 @@ "account.following": "正在关注", "account.following_counter": "{count, plural, other {{counter} 正在关注}}", "account.follows.empty": "此用户目前未关注任何人。", + "account.follows_you": "关注了你", "account.go_to_profile": "前往个人资料页", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", "account.in_memoriam": "谨此悼念。", @@ -52,7 +56,8 @@ "account.mute_notifications_short": "关闭通知", "account.mute_short": "隐藏", "account.muted": "已隐藏", - "account.mutual": "互相关注", + "account.muting": "正在静音", + "account.mutual": "你们互相关注", "account.no_bio": "未提供描述。", "account.open_original_page": "打开原始页面", "account.posts": "嘟文", @@ -60,11 +65,13 @@ "account.report": "举报 @{name}", "account.requested": "正在等待对方同意。点击取消发送关注请求", "account.requested_follow": "{name} 向你发送了关注请求", + "account.requests_to_follow_you": "请求关注您", "account.share": "分享 @{name} 的个人资料", "account.show_reblogs": "显示来自 @{name} 的转嘟", "account.statuses_counter": "{count, plural, other {{counter} 条嘟文}}", "account.unblock": "取消屏蔽 @{name}", "account.unblock_domain": "取消屏蔽 {domain} 域名", + "account.unblock_domain_short": "取消屏蔽", "account.unblock_short": "取消屏蔽", "account.unendorse": "不在个人资料中推荐此用户", "account.unfollow": "取消关注", @@ -390,6 +397,7 @@ "hashtag.counter_by_uses": "{count, plural, other {{counter} 条嘟文}}", "hashtag.counter_by_uses_today": "今日 {count, plural, other {{counter} 条嘟文}}", "hashtag.follow": "关注话题", + "hashtag.mute": "停止提醒 #{hashtag}", "hashtag.unfollow": "取消关注话题", "hashtags.and_other": "… 和另外 {count, plural, other {# 个话题}}", "hints.profiles.followers_may_be_missing": "该账号的关注者列表可能没有完全显示。", @@ -905,6 +913,12 @@ "video.expand": "展开视频", "video.fullscreen": "全屏", "video.hide": "隐藏视频", + "video.mute": "停止提醒", "video.pause": "暂停", - "video.play": "播放" + "video.play": "播放", + "video.skip_backward": "后退", + "video.skip_forward": "前进", + "video.unmute": "恢复提醒", + "video.volume_down": "音量减小", + "video.volume_up": "提高音量" } diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index d6ffb2b6bf..745f7a4689 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -23,13 +23,11 @@ "account.copy": "複製個人檔案連結", "account.direct": "私下提及 @{name}", "account.disable_notifications": "當 @{name} 發文時不要再通知我", - "account.domain_blocked": "網域被封鎖", "account.edit_profile": "修改個人檔案", "account.enable_notifications": "當 @{name} 發文時通知我", "account.endorse": "在個人檔案中推薦對方", "account.featured_tags.last_status_at": "上次帖文於 {date}", "account.featured_tags.last_status_never": "暫無文章", - "account.featured_tags.title": "{name} 的精選標籤", "account.follow": "關注", "account.follow_back": "追蹤對方", "account.followers": "追蹤者", @@ -51,7 +49,6 @@ "account.mute_notifications_short": "靜音通知", "account.mute_short": "靜音", "account.muted": "靜音", - "account.mutual": "互相追蹤", "account.no_bio": "未提供描述。", "account.open_original_page": "打開原始頁面", "account.posts": "帖文", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index f2b8665e49..e3d2170d4f 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -19,17 +19,20 @@ "account.block_domain": "封鎖來自 {domain} 網域的所有內容", "account.block_short": "封鎖", "account.blocked": "已封鎖", + "account.blocking": "封鎖中", "account.cancel_follow_request": "收回跟隨請求", "account.copy": "複製個人檔案連結", "account.direct": " @{name}", "account.disable_notifications": "取消來自 @{name} 嘟文的通知", - "account.domain_blocked": "已封鎖網域", + "account.domain_blocking": "封鎖中網域", "account.edit_profile": "編輯個人檔案", "account.enable_notifications": "當 @{name} 嘟文時通知我", "account.endorse": "於個人檔案推薦對方", + "account.featured": "精選內容", + "account.featured.hashtags": "主題標籤", + "account.featured.posts": "嘟文", "account.featured_tags.last_status_at": "上次嘟文於 {date}", "account.featured_tags.last_status_never": "沒有嘟文", - "account.featured_tags.title": "{name} 的推薦主題標籤", "account.follow": "跟隨", "account.follow_back": "跟隨回去", "account.followers": "跟隨者", @@ -38,6 +41,7 @@ "account.following": "跟隨中", "account.following_counter": "正在跟隨 {count,plural,other {{count} 人}}", "account.follows.empty": "這位使用者尚未跟隨任何人。", + "account.follows_you": "已跟隨您", "account.go_to_profile": "前往個人檔案", "account.hide_reblogs": "隱藏來自 @{name} 的轉嘟", "account.in_memoriam": "謹此悼念。", @@ -52,7 +56,8 @@ "account.mute_notifications_short": "靜音推播通知", "account.mute_short": "靜音", "account.muted": "已靜音", - "account.mutual": "互相跟隨", + "account.muting": "靜音", + "account.mutual": "跟隨彼此", "account.no_bio": "無個人檔案描述。", "account.open_original_page": "檢視原始頁面", "account.posts": "嘟文", @@ -60,6 +65,7 @@ "account.report": "檢舉 @{name}", "account.requested": "正在等候審核。按一下以取消跟隨請求", "account.requested_follow": "{name} 要求跟隨您", + "account.requests_to_follow_you": "要求跟隨您", "account.share": "分享 @{name} 的個人檔案", "account.show_reblogs": "顯示來自 @{name} 的轉嘟", "account.statuses_counter": "{count, plural, other {{count} 則嘟文}}", @@ -294,6 +300,9 @@ "emoji_button.search_results": "搜尋結果", "emoji_button.symbols": "符號", "emoji_button.travel": "旅遊與地點", + "empty_column.account_featured.me": "您尚未有任何精選內容。您知道您可以將您的嘟文、常用主題標籤、甚至您朋友們的帳號作為您個人檔案上之精選內容嗎?", + "empty_column.account_featured.other": "{acct} 尚未有任何精選內容。您知道您可以將您的嘟文、常用主題標籤、甚至您朋友們的帳號作為您個人檔案上之精選內容嗎?", + "empty_column.account_featured_other.unknown": "此帳號尚未有任何精選內容。", "empty_column.account_hides_collections": "這位使用者選擇不提供此資訊", "empty_column.account_suspended": "帳號已被停權", "empty_column.account_timeline": "這裡還沒有嘟文!", @@ -378,6 +387,8 @@ "generic.saved": "已儲存", "getting_started.heading": "開始使用", "hashtag.admin_moderation": "開啟 #{name} 的管理介面", + "hashtag.browse": "瀏覽於 #{hashtag} 之嘟文", + "hashtag.browse_from_account": "瀏覽來自 @{name} 於 #{hashtag} 之嘟文", "hashtag.column_header.tag_mode.all": "以及 {additional}", "hashtag.column_header.tag_mode.any": "或是 {additional}", "hashtag.column_header.tag_mode.none": "而無需 {additional}", @@ -391,6 +402,7 @@ "hashtag.counter_by_uses": "{count, plural, one {{counter} 則} other {{counter} 則}}嘟文", "hashtag.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文", "hashtag.follow": "跟隨主題標籤", + "hashtag.mute": "靜音 #{hashtag}", "hashtag.unfollow": "取消跟隨主題標籤", "hashtags.and_other": "…及其他 {count, plural, other {# 個}}", "hints.profiles.followers_may_be_missing": "此個人檔案之跟隨者或有缺失。", diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 73468c04fe..330dcd93d0 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -491,9 +491,9 @@ export const composeReducer = (state = initialState, action) => { if (action.status.get('poll')) { map.set('poll', ImmutableMap({ - options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), - multiple: action.status.getIn(['poll', 'multiple']), - expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])), + options: ImmutableList(action.status.get('poll').options.map(x => x.title)), + multiple: action.status.get('poll').multiple, + expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at), })); } }); @@ -520,9 +520,9 @@ export const composeReducer = (state = initialState, action) => { if (action.status.get('poll')) { map.set('poll', ImmutableMap({ - options: action.status.getIn(['poll', 'options']).map(x => x.get('title')), - multiple: action.status.getIn(['poll', 'multiple']), - expires_in: expiresInFromExpiresAt(action.status.getIn(['poll', 'expires_at'])), + options: ImmutableList(action.status.get('poll').options.map(x => x.title)), + multiple: action.status.get('poll').multiple, + expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at), })); } }); diff --git a/app/javascript/mastodon/reducers/relationships.ts b/app/javascript/mastodon/reducers/relationships.ts index dcca11b203..9df81c75ea 100644 --- a/app/javascript/mastodon/reducers/relationships.ts +++ b/app/javascript/mastodon/reducers/relationships.ts @@ -24,6 +24,7 @@ import { pinAccountSuccess, unpinAccountSuccess, fetchRelationshipsSuccess, + removeAccountFromFollowers, } from '../actions/accounts_typed'; import { blockDomainSuccess, @@ -109,7 +110,8 @@ export const relationshipsReducer: Reducer = ( unmuteAccountSuccess.match(action) || pinAccountSuccess.match(action) || unpinAccountSuccess.match(action) || - isFulfilled(submitAccountNote)(action) + isFulfilled(submitAccountNote)(action) || + isFulfilled(removeAccountFromFollowers)(action) ) return normalizeRelationship(state, action.payload.relationship); else if (fetchRelationshipsSuccess.match(action)) diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index 8d3e641eda..dc3466b7f3 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -17,7 +17,12 @@ body { font-weight: 400; color: $primary-text-color; text-rendering: optimizelegibility; - font-feature-settings: 'kern'; + + // Disable kerning for Japanese text to preserve monospaced alignment for readability + &:not(:lang(ja)) { + font-feature-settings: 'kern'; + } + text-size-adjust: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0%); -webkit-tap-highlight-color: transparent; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 5d011d8f32..279636c446 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4781,19 +4781,6 @@ a.status-card { } } -.relationship-tag { - color: $white; - margin-bottom: 4px; - display: block; - background-color: rgba($black, 0.45); - backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); - font-size: 11px; - text-transform: uppercase; - font-weight: 700; - padding: 2px 6px; - border-radius: 4px; -} - .setting-toggle { display: flex; align-items: center; @@ -6679,7 +6666,8 @@ a.status-card { pointer-events: none; } -.media-gallery__alt__label { +.media-gallery__alt__label, +.relationship-tag { display: block; text-align: center; color: $white; @@ -6700,6 +6688,11 @@ a.status-card { } } +.relationship-tag { + text-transform: uppercase; + cursor: default; +} + .media-gallery__alt__popover { background: rgba($black, 0.65); backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%); @@ -7774,8 +7767,11 @@ noscript { &__info { position: absolute; - top: 10px; - inset-inline-start: 10px; + top: 20px; + inset-inline-end: 20px; + display: flex; + flex-wrap: wrap; + gap: 2px; } &__image { diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 4b2549ba96..23afda32cd 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -45,9 +45,12 @@ class ActivityPub::Activity::Create < ActivityPub::Activity @unresolved_mentions = [] @silenced_account_ids = [] @params = {} + @quote = nil + @quote_uri = nil process_status_params process_tags + process_quote process_audience ApplicationRecord.transaction do @@ -55,6 +58,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity attach_tags(@status) attach_mentions(@status) attach_counts(@status) + attach_quote(@status) end resolve_thread(@status) @@ -189,6 +193,16 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end end + def attach_quote(status) + return if @quote.nil? + + @quote.status = status + @quote.save + ActivityPub::VerifyQuoteService.new.call(@quote, fetchable_quoted_uri: @quote_uri, request_id: @options[:request_id]) + rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS + ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, @quote.id, @quote_uri, { 'request_id' => @options[:request_id] }) + end + def process_tags return if @object['tag'].nil? @@ -203,6 +217,17 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end end + def process_quote + return unless Mastodon::Feature.inbound_quotes_enabled? + + @quote_uri = @status_parser.quote_uri + return if @quote_uri.blank? + + approval_uri = @status_parser.quote_approval_uri + approval_uri = nil if unsupported_uri_scheme?(approval_uri) + @quote = Quote.new(account: @account, approval_uri: approval_uri) + end + def process_hashtag(tag) return if tag['name'].blank? diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 61f6ca6997..69b7bd0354 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -5,7 +5,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity if @account.uri == object_uri delete_person else - delete_note + delete_object end end @@ -17,7 +17,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity end end - def delete_note + def delete_object return if object_uri.nil? with_redis_lock("delete_status_in_progress:#{object_uri}", raise_on_failure: false) do @@ -32,21 +32,38 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity Tombstone.find_or_create_by(uri: object_uri, account: @account) end - @status = Status.find_by(uri: object_uri, account: @account) - @status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present? - - return if @status.nil? - - forwarder.forward! if forwarder.forwardable? - delete_now! + case @object['type'] + when 'QuoteAuthorization' + revoke_quote + when 'Note', 'Question' + delete_status + else + delete_status || revoke_quote + end end end + def delete_status + @status = Status.find_by(uri: object_uri, account: @account) + @status ||= Status.find_by(uri: @object['atomUri'], account: @account) if @object.is_a?(Hash) && @object['atomUri'].present? + + return if @status.nil? + + forwarder.forward! if forwarder.forwardable? + RemoveStatusService.new.call(@status, redraft: false) + + true + end + + def revoke_quote + @quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account) + return if @quote.nil? + + ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! + @quote.reject! + end + def forwarder @forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status) end - - def delete_now! - RemoveStatusService.new.call(@status, redraft: false) - end end diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index 3d2be3c66c..c13ed49635 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -101,6 +101,16 @@ class ActivityPub::Parser::StatusParser @object.dig(:shares, :totalItems) end + def quote_uri + %w(quote _misskey_quote quoteUrl quoteUri).filter_map do |key| + value_or_id(as_array(@object[key]).first) + end.first + end + + def quote_approval_uri + as_array(@object['quoteAuthorization']).first + end + private def raw_language_code diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 676e9e62a0..8821c23d13 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -71,6 +71,23 @@ 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) + payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id) if payload[:quote] + end + + def hydrate_quote_payload(empty_payload, quote, account_id) + # TODO: properly handle quotes, including visibility and access control + + empty_payload.tap do |payload| + # Nothing to do if we're in the shallow (depth limit) case + next unless payload.key?(:quoted_status) + + # TODO: handle hiding a rendered status or showing a non-rendered status according to visibility + if quote&.quoted_status.nil? + payload[:quoted_status] = nil + elsif payload[:quoted_status].present? + payload[:quoted_status] = StatusCacheHydrator.new(quote.quoted_status).hydrate(account_id) + end + end end def mapped_applied_custom_filter(account_id, status) diff --git a/app/models/concerns/status/snapshot_concern.rb b/app/models/concerns/status/snapshot_concern.rb index 0289710904..269545ce8b 100644 --- a/app/models/concerns/status/snapshot_concern.rb +++ b/app/models/concerns/status/snapshot_concern.rb @@ -25,6 +25,7 @@ module Status::SnapshotConcern poll_options: preloadable_poll&.options&.dup, account_id: account_id || self.account_id, created_at: at_time || edited_at, + quote_id: quote&.id, rate_limit: rate_limit ) end diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index d653c55310..07bbfd4373 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -33,7 +33,7 @@ class CustomFilter < ApplicationRecord include Expireable include Redisable - enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action + enum :action, { warn: 0, hide: 1, blur: 2 }, suffix: :action, validate: true belongs_to :account has_many :keywords, class_name: 'CustomFilterKeyword', inverse_of: :custom_filter, dependent: :destroy diff --git a/app/models/list.rb b/app/models/list.rb index cd01774539..76c116ce24 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -18,7 +18,7 @@ class List < ApplicationRecord PER_ACCOUNT_LIMIT = 50 - enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show + enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true belongs_to :account diff --git a/app/models/quote.rb b/app/models/quote.rb new file mode 100644 index 0000000000..8e21d9b481 --- /dev/null +++ b/app/models/quote.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: quotes +# +# id :bigint(8) not null, primary key +# activity_uri :string +# approval_uri :string +# state :integer default("pending"), not null +# created_at :datetime not null +# updated_at :datetime not null +# account_id :bigint(8) not null +# quoted_account_id :bigint(8) +# quoted_status_id :bigint(8) +# status_id :bigint(8) not null +# +class Quote < ApplicationRecord + BACKGROUND_REFRESH_INTERVAL = 1.week.freeze + REFRESH_DEADLINE = 6.hours + + enum :state, + { pending: 0, accepted: 1, rejected: 2, revoked: 3 }, + validate: true + + belongs_to :status + belongs_to :quoted_status, class_name: 'Status', optional: true + + belongs_to :account + belongs_to :quoted_account, class_name: 'Account', optional: true + + before_validation :set_accounts + + validates :activity_uri, presence: true, if: -> { account.local? && quoted_account&.remote? } + validate :validate_visibility + + def accept! + update!(state: :accepted) + end + + def reject! + if accepted? + update!(state: :revoked) + elsif !revoked? + update!(state: :rejected) + end + end + + def schedule_refresh_if_stale! + return unless quoted_status_id.present? && approval_uri.present? && updated_at <= BACKGROUND_REFRESH_INTERVAL.ago + + ActivityPub::QuoteRefreshWorker.perform_in(rand(REFRESH_DEADLINE), id) + end + + private + + def set_accounts + self.account = status.account + self.quoted_account = quoted_status&.account + end + + def validate_visibility + return if account_id == quoted_account_id || quoted_status.nil? || quoted_status.distributable? + + errors.add(:quoted_status_id, :visibility_mismatch) + end +end diff --git a/app/models/status.rb b/app/models/status.rb index cdff5a2ac3..f9afdcca8b 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -93,6 +93,7 @@ class Status < ApplicationRecord has_one :status_stat, inverse_of: :status, dependent: nil has_one :poll, inverse_of: :status, dependent: :destroy has_one :trend, class_name: 'StatusTrend', inverse_of: :status, dependent: nil + has_one :quote, inverse_of: :status, dependent: :destroy validates :uri, uniqueness: true, presence: true, unless: :local? validates :text, presence: true, unless: -> { with_media? || reblog? } @@ -154,16 +155,18 @@ class Status < ApplicationRecord :status_stat, :tags, :preloadable_poll, + quote: { status: { account: [:account_stat, user: :role] } }, preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, account: [:account_stat, user: :role], active_mentions: :account, reblog: [ :application, - :tags, :media_attachments, :conversation, :status_stat, + :tags, :preloadable_poll, + quote: { status: { account: [:account_stat, user: :role] } }, preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } }, account: [:account_stat, user: :role], active_mentions: :account, diff --git a/app/models/status_edit.rb b/app/models/status_edit.rb index 6e25a6f3bb..a64ef34905 100644 --- a/app/models/status_edit.rb +++ b/app/models/status_edit.rb @@ -15,6 +15,7 @@ # media_descriptions :text is an Array # poll_options :string is an Array # sensitive :boolean +# quote_id :bigint(8) # class StatusEdit < ApplicationRecord diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb index 5d53040fb2..2d95db82da 100644 --- a/app/presenters/status_relationships_presenter.rb +++ b/app/presenters/status_relationships_presenter.rb @@ -16,11 +16,11 @@ class StatusRelationshipsPresenter @filters_map = {} else statuses = statuses.compact - status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.uniq.compact - conversation_ids = statuses.filter_map(&:conversation_id).uniq - pinnable_status_ids = statuses.map(&:proper).filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) } + status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id, s.proper.quote&.quoted_status_id] }.uniq.compact + conversation_ids = statuses.flat_map { |s| [s.proper.conversation_id, s.proper.quote&.quoted_status&.conversation_id] }.uniq.compact + pinnable_status_ids = statuses.flat_map { |s| [s.proper, s.proper.quote&.quoted_status] }.compact.filter_map { |s| s.id if s.account_id == current_account_id && PINNABLE_VISIBILITIES.include?(s.visibility) } - @filters_map = build_filters_map(statuses, current_account_id).merge(options[:filters_map] || {}) + @filters_map = build_filters_map(statuses.flat_map { |s| [s, s.proper.quote&.quoted_status] }.compact.uniq, current_account_id).merge(options[:filters_map] || {}) @reblogs_map = Status.reblogs_map(status_ids, current_account_id).merge(options[:reblogs_map] || {}) @favourites_map = Status.favourites_map(status_ids, current_account_id).merge(options[:favourites_map] || {}) @bookmarks_map = Status.bookmarks_map(status_ids, current_account_id).merge(options[:bookmarks_map] || {}) diff --git a/app/serializers/rest/base_quote_serializer.rb b/app/serializers/rest/base_quote_serializer.rb new file mode 100644 index 0000000000..0434f342c9 --- /dev/null +++ b/app/serializers/rest/base_quote_serializer.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class REST::BaseQuoteSerializer < ActiveModel::Serializer + attributes :state + + def state + return object.state unless object.accepted? + + # Extra states when a status is unavailable + return 'deleted' if object.quoted_status.nil? + return 'unauthorized' if status_filter.filtered? + + object.state + end + + def quoted_status + object.quoted_status if object.accepted? && object.quoted_status.present? && !status_filter.filtered? + end + + private + + def status_filter + @status_filter ||= StatusFilter.new(object.quoted_status, current_user&.account, instance_options[:relationships] || {}) + end +end diff --git a/app/serializers/rest/quote_serializer.rb b/app/serializers/rest/quote_serializer.rb new file mode 100644 index 0000000000..6f2eede0ea --- /dev/null +++ b/app/serializers/rest/quote_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class REST::QuoteSerializer < REST::BaseQuoteSerializer + has_one :quoted_status, serializer: REST::ShallowStatusSerializer +end diff --git a/app/serializers/rest/shallow_quote_serializer.rb b/app/serializers/rest/shallow_quote_serializer.rb new file mode 100644 index 0000000000..1f5f229d43 --- /dev/null +++ b/app/serializers/rest/shallow_quote_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::ShallowQuoteSerializer < REST::BaseQuoteSerializer + attribute :quoted_status_id + + def quoted_status_id + quoted_status&.id&.to_s + end +end diff --git a/app/serializers/rest/shallow_status_serializer.rb b/app/serializers/rest/shallow_status_serializer.rb new file mode 100644 index 0000000000..ca0ac8f4f4 --- /dev/null +++ b/app/serializers/rest/shallow_status_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class REST::ShallowStatusSerializer < REST::StatusSerializer + has_one :quote, key: :quote, serializer: REST::ShallowQuoteSerializer + + # It looks like redefining one `has_one` requires redefining all inherited ones + has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer + has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer +end diff --git a/app/serializers/rest/status_edit_serializer.rb b/app/serializers/rest/status_edit_serializer.rb index f7a48797d1..30e318a6aa 100644 --- a/app/serializers/rest/status_edit_serializer.rb +++ b/app/serializers/rest/status_edit_serializer.rb @@ -10,6 +10,8 @@ class REST::StatusEditSerializer < ActiveModel::Serializer has_many :ordered_media_attachments, key: :media_attachments, serializer: REST::MediaAttachmentSerializer has_many :emojis, serializer: REST::CustomEmojiSerializer + has_one :quote, serializer: REST::QuoteSerializer, if: -> { object.quote_id.present? } + attribute :poll, if: -> { object.poll_options.present? } def content @@ -19,4 +21,8 @@ class REST::StatusEditSerializer < ActiveModel::Serializer def poll { options: object.poll_options.map { |title| { title: title } } } end + + def quote + object.quote_id == status.quote&.id ? status.quote : Quote.new(state: :pending) + end end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index e108c789c7..e0761af7f2 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -29,6 +29,7 @@ class REST::StatusSerializer < ActiveModel::Serializer has_many :tags has_many :emojis, serializer: REST::CustomEmojiSerializer + 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 diff --git a/app/serializers/rest/web_push_subscription_serializer.rb b/app/serializers/rest/web_push_subscription_serializer.rb index 4cb980bb93..01825a3bb0 100644 --- a/app/serializers/rest/web_push_subscription_serializer.rb +++ b/app/serializers/rest/web_push_subscription_serializer.rb @@ -6,7 +6,7 @@ class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer delegate :standard, to: :object def alerts - (object.data&.dig('alerts') || {}).each_with_object({}) { |(k, v), h| h[k] = ActiveModel::Type::Boolean.new.cast(v) } + (object.data&.dig('alerts') || {}).transform_values { |v| ActiveModel::Type::Boolean.new.cast(v) } end def server_key diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index fd5a8c2d46..6a1066a05d 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -16,6 +16,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @account = status.account @media_attachments_changed = false @poll_changed = false + @quote_changed = false @request_id = request_id # Only native types can be updated at the moment @@ -158,7 +159,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService @status.sensitive = @account.sensitized? || @status_parser.sensitive || false @status.language = @status_parser.language - @significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed + @significant_changes = text_significantly_changed? || @status.spoiler_text_changed? || @media_attachments_changed || @poll_changed || @quote_changed @status.edited_at = @status_parser.edited_at if significant_changes? @@ -183,6 +184,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService update_tags! update_mentions! update_emojis! + update_quote! end def update_tags! @@ -262,6 +264,45 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end end + def update_quote! + return unless Mastodon::Feature.inbound_quotes_enabled? + + quote = nil + quote_uri = @status_parser.quote_uri + + if quote_uri.present? + approval_uri = @status_parser.quote_approval_uri + approval_uri = nil if unsupported_uri_scheme?(approval_uri) + + if @status.quote.present? + # If the quoted post has changed, discard the old object and create a new one + if @status.quote.quoted_status.present? && ActivityPub::TagManager.instance.uri_for(@status.quote.quoted_status) != quote_uri + @status.quote.destroy + quote = Quote.create(status: @status, approval_uri: approval_uri) + @quote_changed = true + else + quote = @status.quote + quote.update(approval_uri: approval_uri, state: :pending) if quote.approval_uri != @status_parser.quote_approval_uri + end + else + quote = Quote.create(status: @status, approval_uri: approval_uri) + @quote_changed = true + end + end + + if quote.present? + begin + quote.save + ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quote_uri, request_id: @request_id) + rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS + ActivityPub::RefetchAndVerifyQuoteWorker.perform_in(rand(30..600).seconds, quote.id, quote_uri, { 'request_id' => @request_id }) + end + elsif @status.quote.present? + @status.quote.destroy! + @quote_changed = true + end + end + def update_counts! likes = @status_parser.favourites_count shares = @status_parser.reblogs_count diff --git a/app/services/activitypub/verify_quote_service.rb b/app/services/activitypub/verify_quote_service.rb new file mode 100644 index 0000000000..0803d62d3a --- /dev/null +++ b/app/services/activitypub/verify_quote_service.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +class ActivityPub::VerifyQuoteService < BaseService + include JsonLdHelper + + # Optionally fetch quoted post, and verify the quote is authorized + def call(quote, fetchable_quoted_uri: nil, prefetched_body: nil, request_id: nil) + @request_id = request_id + @quote = quote + @fetching_error = nil + + fetch_quoted_post_if_needed!(fetchable_quoted_uri) + return if fast_track_approval! || quote.approval_uri.blank? + + @json = fetch_approval_object(quote.approval_uri, prefetched_body:) + return quote.reject! if @json.nil? + + return if non_matching_uri_hosts?(quote.approval_uri, value_or_id(@json['attributedTo'])) + return unless matching_type? && matching_quote_uri? + + # Opportunistically import embedded posts if needed + return if import_quoted_post_if_needed!(fetchable_quoted_uri) && fast_track_approval! + + # Raise an error if we failed to fetch the status + raise @fetching_error if @quote.status.nil? && @fetching_error + + return unless matching_quoted_post? && matching_quoted_author? + + quote.accept! + end + + private + + # FEP-044f defines rules that don't require the approval flow + def fast_track_approval! + return false if @quote.quoted_status_id.blank? + + # Always allow someone to quote themselves + if @quote.account_id == @quote.quoted_account_id + @quote.accept! + + true + end + + # Always allow someone to quote posts in which they are mentioned + if @quote.quoted_status.active_mentions.exists?(mentions: { account_id: @quote.account_id }) + @quote.accept! + + true + else + false + end + end + + def fetch_approval_object(uri, prefetched_body: nil) + if prefetched_body.nil? + fetch_resource(uri, true, @quote.account.followers.local.first, raise_on_error: :temporary) + else + body_to_json(prefetched_body, compare_id: uri) + end + end + + def matching_type? + supported_context?(@json) && equals_or_includes?(@json['type'], 'QuoteAuthorization') + end + + def matching_quote_uri? + ActivityPub::TagManager.instance.uri_for(@quote.status) == value_or_id(@json['interactingObject']) + end + + def fetch_quoted_post_if_needed!(uri) + return if uri.nil? || @quote.quoted_status.present? + + status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status) + status ||= ActivityPub::FetchRemoteStatusService.new.call(uri, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id) + + @quote.update(quoted_status: status) if status.present? + rescue Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS => e + @fetching_error = e + end + + def import_quoted_post_if_needed!(uri) + # No need to fetch if we already have a post + return if uri.nil? || @quote.quoted_status_id.present? || !@json['interactionTarget'].is_a?(Hash) + + # NOTE: Replacing the object's context by that of the parent activity is + # not sound, but it's consistent with the rest of the codebase + object = @json['interactionTarget'].merge({ '@context' => @json['@context'] }) + + # It's not safe to fetch if the inlined object is cross-origin or doesn't match expectations + return if object['id'] != uri || non_matching_uri_hosts?(@quote.approval_uri, object['id']) + + status = ActivityPub::FetchRemoteStatusService.new.call(object['id'], prefetched_body: object, on_behalf_of: @quote.account.followers.local.first, request_id: @request_id) + + if status.present? + @quote.update(quoted_status: status) + true + else + false + end + end + + def matching_quoted_post? + return false if @quote.quoted_status_id.blank? + + ActivityPub::TagManager.instance.uri_for(@quote.quoted_status) == value_or_id(@json['interactionTarget']) + end + + def matching_quoted_author? + ActivityPub::TagManager.instance.uri_for(@quote.quoted_account) == value_or_id(@json['attributedTo']) + end +end diff --git a/app/views/admin/announcements/previews/show.html.haml b/app/views/admin/announcements/previews/show.html.haml index fdfbf598b5..54d5d45ed6 100644 --- a/app/views/admin/announcements/previews/show.html.haml +++ b/app/views/admin/announcements/previews/show.html.haml @@ -7,6 +7,8 @@ = material_symbol 'chevron_left' = t('admin.announcements.back') +.flash-message.info= t('admin.announcements.preview.disclaimer') + %p.lead = t('admin.announcements.preview.explanation_html', count: @user_count, display_count: number_with_delimiter(@user_count)) diff --git a/app/workers/activitypub/quote_refresh_worker.rb b/app/workers/activitypub/quote_refresh_worker.rb new file mode 100644 index 0000000000..7dabfddc80 --- /dev/null +++ b/app/workers/activitypub/quote_refresh_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ActivityPub::QuoteRefreshWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', retry: 3, dead: false, lock: :until_executed, lock_ttl: 1.day.to_i + + def perform(quote_id) + quote = Quote.find_by(id: quote_id) + return if quote.nil? || quote.updated_at > Quote::BACKGROUND_REFRESH_INTERVAL.ago + + quote.touch + ActivityPub::VerifyQuoteService.new.call(quote) + end +end diff --git a/app/workers/activitypub/refetch_and_verify_quote_worker.rb b/app/workers/activitypub/refetch_and_verify_quote_worker.rb new file mode 100644 index 0000000000..0c7ecd9b2a --- /dev/null +++ b/app/workers/activitypub/refetch_and_verify_quote_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ActivityPub::RefetchAndVerifyQuoteWorker + include Sidekiq::Worker + include ExponentialBackoff + include JsonLdHelper + + sidekiq_options queue: 'pull', retry: 3 + + def perform(quote_id, quoted_uri, options = {}) + quote = Quote.find(quote_id) + ActivityPub::VerifyQuoteService.new.call(quote, fetchable_quoted_uri: quoted_uri, request_id: options[:request_id]) + rescue ActiveRecord::RecordNotFound + # Do nothing + true + rescue Mastodon::UnexpectedResponseError => e + raise e unless response_error_unsalvageable?(e.response) + end +end diff --git a/config/locales/activerecord.el.yml b/config/locales/activerecord.el.yml index 58e7a7df3f..4bb344bc6d 100644 --- a/config/locales/activerecord.el.yml +++ b/config/locales/activerecord.el.yml @@ -55,6 +55,8 @@ el: too_soon: είναι πολύ σύντομα, πρέπει να είναι μετά από %{date} user: attributes: + date_of_birth: + below_limit: είναι κάτω από το όριο ηλικίας email: blocked: χρησιμοποιεί μη επιτρεπόμενο πάροχο e-mail unreachable: δεν φαίνεται να υπάρχει diff --git a/config/locales/activerecord.nn.yml b/config/locales/activerecord.nn.yml index f47bafe0b7..ae73057dcc 100644 --- a/config/locales/activerecord.nn.yml +++ b/config/locales/activerecord.nn.yml @@ -49,8 +49,14 @@ nn: attributes: reblog: taken: av innlegg eksisterer allereie + terms_of_service: + attributes: + effective_date: + too_soon: er for snart, må vera seinare enn %{date} user: attributes: + date_of_birth: + below_limit: er under aldersgrensa email: blocked: bruker ein forboden epostleverandør unreachable: ser ikkje ut til å eksistere diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 1bdf202fca..37cd0b3bf6 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -325,6 +325,7 @@ cs: create: Vytvořit oznámení title: Nové oznámení preview: + disclaimer: Vzhledem k tomu, že se od nich uživatelé nemohou odhlásit, měla by být e-mailová upozornění omezena na důležitá oznámení, jako je narušení osobních údajů nebo oznámení o uzavření serveru. explanation_html: 'E-mail bude odeslán %{display_count} uživatelům. Následující text bude zahrnut do onoho e-mailu:' title: Náhled oznámení publish: Zveřejnit diff --git a/config/locales/cy.yml b/config/locales/cy.yml index edd89fedc6..7251167bae 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -331,6 +331,7 @@ cy: create: Creu cyhoeddiad title: Cyhoeddiad newydd preview: + disclaimer: Gan nad oes modd i ddefnyddwyr eu hosgoi, dylai hysbysiadau e-bost gael eu cyfyngu i gyhoeddiadau pwysig fel tor-data personol neu hysbysiadau cau gweinydd. explanation_html: 'Bydd yr e-bost yn cael ei anfon at %{display_count} defnyddiwr . Bydd y testun canlynol yn cael ei gynnwys yn yr e-bost:' title: Hysbysiad rhagolwg cyhoeddiad publish: Cyhoeddi @@ -517,7 +518,7 @@ cy: created_at: Crëwyd am delete: Dileu ip: Cyfeiriad IP - request_body: Gofyn am y corff + request_body: Corff y cais title: Adalwasau Dadfygio providers: active: Gweithredol @@ -925,7 +926,7 @@ cy: deleted: Dilëwyd favourites: Ffefrynnau history: Hanes fersiynau - in_reply_to: Ymateb i + in_reply_to: Mewn ymateb i language: Iaith media: title: Cyfryngau @@ -1789,7 +1790,7 @@ cy: subject: 'Dilynwr yn aros: %{name}' title: Cais dilynwr newydd mention: - action: Ateb + action: Ymateb body: 'Caswoch eich crybwyll gan %{name} yn:' subject: Cawsoch eich crybwyll gan %{name} title: Crywbylliad newydd @@ -1958,7 +1959,7 @@ cy: back: Nôl i Mastodon delete: Dileu cyfrif development: Datblygu - edit_profile: Golygu proffil + edit_profile: Golygu'r proffil export: Allforio featured_tags: Prif hashnodau import: Mewnforio diff --git a/config/locales/da.yml b/config/locales/da.yml index b0314e0f74..63a414811e 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -319,6 +319,7 @@ da: create: Opret bekendtgørelse title: Ny bekendtgørelse preview: + disclaimer: Da brugere ikke kan fravælge e-mailnotifikationer, bør disse begrænses til vigtige emner som f.eks. personlige databrud eller serverlukninger. explanation_html: 'E-mailen sendes til %{display_count} brugere. Flg. tekst medtages i e-mailen:' title: Forhåndsvis annonceringsnotifikation publish: Publicér diff --git a/config/locales/de.yml b/config/locales/de.yml index ab0bc53fcc..0842ab73d7 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -319,6 +319,7 @@ de: create: Ankündigung erstellen title: Neue Ankündigung preview: + disclaimer: Da sich Profile nicht davon abmelden können, sollten Benachrichtigungen per E-Mail auf wichtige Ankündigungen wie z. B. zu Datenpannen oder Serverabschaltung beschränkt sein. explanation_html: 'Die E-Mail wird an %{display_count} Nutzer*innen gesendet. Folgendes wird in der E-Mail enthalten sein:' title: Vorschau der Ankündigung publish: Veröffentlichen @@ -497,6 +498,7 @@ de: registration_requested: Registrierung angefordert registrations: confirm: Bestätigen + description: Sie haben eine Registrierung von einer FASP erhalten. Lehnen Sie ab, wenn Sie dies nicht initiiert haben. Wenn Sie dies initiiert haben, vergleichen Sie Namen und Fingerabdruck vor der Bestätigung der Registrierung. reject: Ablehnen title: FASP-Registrierung bestätigen save: Speichern diff --git a/config/locales/doorkeeper.ms.yml b/config/locales/doorkeeper.ms.yml index f89def7b85..aadce76efd 100644 --- a/config/locales/doorkeeper.ms.yml +++ b/config/locales/doorkeeper.ms.yml @@ -128,11 +128,11 @@ ms: crypto: Penyulitan hujung ke hujung favourites: Sukaan filters: Penapis - follow: Ikut, Senyap dan Blok + follow: Ikutan, Redaman dan Sekatan follows: Ikutan lists: Senarai media: Lampiran media - mutes: Senyapkan + mutes: Redaman notifications: Pemberitahuan push: Pemberitahuan segera reports: Laporan @@ -173,7 +173,7 @@ ms: read:filters: lihat penapis anda read:follows: lihat senarai yang anda ikuti read:lists: lihat senarai anda - read:mutes: lihat senarai yang anda senyapkan + read:mutes: lihat redamanku read:notifications: lihat notifikasi anda read:reports: lihat laporan anda read:search: cari bagi pihak anda @@ -182,13 +182,13 @@ ms: write:accounts: ubaisuai profail anda write:blocks: domain dan akaun blok write:bookmarks: menandabuku hantaran - write:conversations: senyapkan dan padamkan perbualan + write:conversations: redamkan dan padamkan perbualan write:favourites: hantaran disukai write:filters: cipta penapis write:follows: ikut orang write:lists: cipta senarai write:media: memuat naik fail media - write:mutes: membisukan orang dan perbualan + write:mutes: redamkan orang dan perbualan write:notifications: kosongkan pemberitahuan anda write:reports: melaporkan orang lain write:statuses: terbitkan hantaran diff --git a/config/locales/el.yml b/config/locales/el.yml index d2f4fbba01..9e82063f98 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -319,6 +319,7 @@ el: create: Δημιουργία ανακοίνωσης title: Νέα ανακοίνωση preview: + disclaimer: Δεδομένου ότι οι χρήστες δεν μπορούν να εξαιρεθούν από αυτά, οι ειδοποιήσεις μέσω ηλεκτρονικού ταχυδρομείου θα πρέπει να περιορίζονται σε σημαντικές ανακοινώσεις, όπως η παραβίαση προσωπικών δεδομένων ή οι ειδοποιήσεις κλεισίματος διακομιστή. explanation_html: 'Το email θα αποσταλεί σε %{display_count} χρήστες. Το ακόλουθο κείμενο θα συμπεριληφθεί στο e-mail:' title: Προεπισκόπηση ειδοποίησης ανακοίνωσης publish: Δημοσίευση @@ -507,6 +508,8 @@ el: select_capabilities: Επέλεξε Δυνατότητες sign_in: Σύνδεση status: Κατάσταση + title: Πάροχοι Δευτερεύουσας Υπηρεσίας Fediverse + title: FASP follow_recommendations: description_html: "Ακολουθώντας συστάσεις βοηθάει τους νέους χρήστες να βρουν γρήγορα ενδιαφέρον περιεχόμενο. Όταν ένας χρήστης δεν έχει αλληλεπιδράσει με άλλους αρκετά για να διαμορφώσει εξατομικευμένες συστάσεις, συνιστώνται αυτοί οι λογαριασμοί. Υπολογίζονται εκ νέου σε καθημερινή βάση από ένα σύνολο λογαριασμών με τις υψηλότερες πρόσφατες αλληλεπιδράσεις και μεγαλύτερο αριθμό τοπικών ακόλουθων για μια δεδομένη γλώσσα." language: Για τη γλώσσα @@ -971,6 +974,7 @@ el: chance_to_review_html: "Οι παραγόμενοι όροι υπηρεσίας δε θα δημοσιεύονται αυτόματα. Θα έχεις την ευκαιρία να εξετάσεις το αποτέλεσμα. Παρακαλούμε συμπλήρωσε τις απαιτούμενες πληροφορίες για να συνεχίσεις." explanation_html: Το πρότυπο όρων υπηρεσίας που παρέχονται είναι μόνο για ενημερωτικούς σκοπούς και δε θα πρέπει να ερμηνεύονται ως νομικές συμβουλές για οποιοδήποτε θέμα. Παρακαλούμε συμβουλέψου τον νομικό σου σύμβουλο σχετικά με την περίπτωσή σου και τις συγκεκριμένες νομικές ερωτήσεις που έχεις. title: Ρύθμιση Όρων Παροχής Υπηρεσιών + going_live_on_html: Ενεργό, σε ισχύ από %{date} history: Ιστορικό live: Ενεργό no_history: Δεν υπάρχουν ακόμα καταγεγραμμένες αλλαγές στους όρους παροχής υπηρεσιών. @@ -1936,6 +1940,10 @@ el: recovery_instructions_html: Αν ποτέ δεν έχεις πρόσβαση στο κινητό σου, μπορείς να χρησιμοποιήσεις έναν από τους παρακάτω κωδικούς ανάκτησης για να αποκτήσεις πρόσβαση στο λογαριασμό σου. Διαφύλαξε τους κωδικούς ανάκτησης. Για παράδειγμα, μπορείς να τους εκτυπώσεις και να τους φυλάξεις μαζί με άλλα σημαντικά σου έγγραφα. webauthn: Κλειδιά ασφαλείας user_mailer: + announcement_published: + description: 'Οι διαχειριστές του %{domain} κάνουν μια ανακοίνωση:' + subject: Ανακοίνωση διακομιστή + title: Ανακοίνωση διακομιστή %{domain} appeal_approved: action: Ρυθμίσεις Λογαριασμού explanation: Η έφεση του παραπτώματος εναντίον του λογαριασμού σου στις %{strike_date}, που υπέβαλες στις %{appeal_date} έχει εγκριθεί. Ο λογαριασμός σου είναι και πάλι σε καλή κατάσταση. @@ -1968,6 +1976,8 @@ el: terms_of_service_changed: agreement: Συνεχίζοντας να χρησιμοποιείς το %{domain}, συμφωνείς με αυτούς τους όρους. Αν διαφωνείς με τους ενημερωμένους όρους, μπορείς να τερματίσεις τη συμφωνία σου με το %{domain} ανά πάσα στιγμή διαγράφοντας τον λογαριασμό σου. changelog: 'Με μια ματιά, αυτό σημαίνει αυτή η ενημέρωση για σένα:' + description: 'Λαμβάνεις αυτό το email επειδή κάνουμε κάποιες αλλαγές στους όρους παροχής υπηρεσιών μας στο %{domain}. Αυτές οι ενημερώσεις θα τεθούν σε ισχύ στις %{date}. Σε ενθαρρύνουμε να εξετάσεις πλήρως τους ενημερωμένους όρους εδώ:' + description_html: Λαμβάνεις αυτό το email επειδή κάνουμε κάποιες αλλαγές στους όρους παροχής υπηρεσιών μας στο %{domain}. Αυτές οι ενημερώσεις θα τεθούν σε ισχύ στις %{date}. Σε ενθαρρύνουμε να εξετάσεις πλήρως τους ενημερωμένους όρους εδώ. sign_off: Η ομάδα του %{domain} subject: Ενημερώσεις στους όρους παροχής υπηρεσιών μας subtitle: Οι όροι παροχής υπηρεσιών του %{domain} αλλάζουν diff --git a/config/locales/en.yml b/config/locales/en.yml index 4c5e1466f7..f0e1f86c4e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -319,6 +319,7 @@ en: create: Create announcement title: New announcement preview: + disclaimer: As users cannot opt out of them, email notifications should be limited to important announcements such as personal data breach or server closure notifications. explanation_html: 'The email will be sent to %{display_count} users. The following text will be included in the e-mail:' title: Preview announcement notification publish: Publish diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index dd92aed5f6..ab3d307fd8 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -319,6 +319,7 @@ es-AR: create: Crear anuncio title: Nuevo anuncio preview: + disclaimer: Como los usuarios no pueden excluirse de ellas, las notificaciones por correo electrónico deberían limitarse a anuncios importantes como la violación de datos personales o las notificaciones de cierre del servidor. explanation_html: 'El correo electrónico se enviará a %{display_count} usuarios. En el correo electrónico se incluirá el siguiente texto:' title: Previsualizar la notificación del anuncio publish: Publicar diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index a9192740fe..3032b663a1 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -319,6 +319,7 @@ es-MX: create: Crear anuncio title: Nuevo anuncio preview: + disclaimer: Como los usuarios no pueden optar por no recibirlas, las notificaciones por correo electrónico deben limitarse a anuncios importantes, como la violación de datos personales o las notificaciones de cierre de servidores. explanation_html: 'El correo electrónico se enviará a %{display_count} usuarios. En el correo electrónico se incluirá el siguiente texto:' title: Vista previa de la notificación del anuncio publish: Publicar diff --git a/config/locales/es.yml b/config/locales/es.yml index ed09dfb76b..9e0feb9d18 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -3,7 +3,7 @@ es: about: about_mastodon_html: 'La red social del futuro: ¡Sin anuncios, sin vigilancia corporativa, diseño ético, y descentralización! ¡Sé dueño de tu información con Mastodon!' contact_missing: No establecido - contact_unavailable: No disponible + contact_unavailable: N/D hosted_on: Mastodon alojado en %{domain} title: Acerca de accounts: @@ -319,6 +319,7 @@ es: create: Crear anuncio title: Nuevo anuncio preview: + disclaimer: Como los usuarios no pueden optar por no recibirlas, las notificaciones por correo electrónico deben limitarse a anuncios importantes, como la violación de datos personales o las notificaciones de cierre de servidores. explanation_html: 'El correo electrónico se enviará a %{display_count} usuarios. En el correo electrónico se incluirá el siguiente texto:' title: Vista previa de la notificación del anuncio publish: Publicar diff --git a/config/locales/fi.yml b/config/locales/fi.yml index fcb180518f..9a5bf97afe 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -319,6 +319,7 @@ fi: create: Luo tiedote title: Uusi tiedote preview: + disclaimer: Koska käyttäjät eivät voi kieltäytyä niistä, sähköposti-ilmoitukset tulee rajata tärkeisiin tiedotteisiin, kuten ilmoituksiin henkilötietojen tietoturvaloukkauksista tai palvelimen sulkeutumisesta. explanation_html: "%{display_count} käyttäjälle lähetetään sähköpostia. Sähköpostiviestiin sisällytetään seuraava teksti:" title: Esikatsele tiedoteilmoitus publish: Julkaise diff --git a/config/locales/fo.yml b/config/locales/fo.yml index ec38374070..8e611c346d 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -319,6 +319,7 @@ fo: create: Stovna kunngerð title: Nýggj kunngerð preview: + disclaimer: Av tí at brúkarar ikki kunnu velja tær frá, eiga teldupostfráboðanir at vera avmarkaðar til týdningarmiklar kunngerðir, sosum trygdarbrot og boð um at ambætarin verður tikin niður. explanation_html: 'Teldubrævið verður sent til %{display_count} brúkarar. Fylgjandi tekstur kemur við í teldubrævið:' title: Undanvís fráboðan um kunngerð publish: Legg út diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index dd751c0678..e85952f91c 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -482,6 +482,15 @@ fr-CA: new: title: Importer des blocages de domaine no_file: Aucun fichier sélectionné + fasp: + providers: + registrations: + confirm: Confirmer + reject: Rejeter + save: Enregistrer + select_capabilities: Sélectionnez les Capacités + sign_in: Se connecter + status: État follow_recommendations: description_html: "Les recommandations d'abonnement aident les nouvelles personnes à trouver rapidement du contenu intéressant. Si un·e utilisateur·rice n'a pas assez interagi avec les autres pour avoir des recommandations personnalisées, ces comptes sont alors recommandés. La sélection est mise à jour quotidiennement depuis un mélange de comptes ayant le plus d'interactions récentes et le plus grand nombre d'abonné·e·s locaux pour une langue donnée." language: Pour la langue diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6ad2733f3e..25218bd019 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -482,6 +482,15 @@ fr: new: title: Importer des blocages de domaine no_file: Aucun fichier sélectionné + fasp: + providers: + registrations: + confirm: Confirmer + reject: Rejeter + save: Enregistrer + select_capabilities: Sélectionnez les Capacités + sign_in: Se connecter + status: État follow_recommendations: description_html: "Les recommandations d'abonnement aident les nouvelles personnes à trouver rapidement du contenu intéressant. Si un·e utilisateur·rice n'a pas assez interagi avec les autres pour avoir des recommandations personnalisées, ces comptes sont alors recommandés. La sélection est mise à jour quotidiennement depuis un mélange de comptes ayant le plus d'interactions récentes et le plus grand nombre d'abonné·e·s locaux pour une langue donnée." language: Pour la langue diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 1da5c146c7..ed51799e8a 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -319,6 +319,7 @@ gl: create: Crear anuncio title: Novo anuncio preview: + disclaimer: As usuarias non poden omitilas, as notificiacións por correo deberían limitarse a anuncios importantes como fugas de datos personais ou notificación do cese do servizo. explanation_html: 'Vaise enviar o correo a %{display_count} usuarias. Incluirase o seguinte texto no correo:' title: Previsualización da notificación do anuncio publish: Publicar diff --git a/config/locales/he.yml b/config/locales/he.yml index 6d575bc564..457569d05a 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -325,6 +325,7 @@ he: create: יצירת הכרזה title: הכרזה חדשה preview: + disclaimer: כיוון שהמשתמשים לא יכולים לבטל אותם, הודעות דוא"ל צריכות להיות מוגבלות בשימוש להודעות חשובות כגון הודעות על גניבת מידע אישי או הודעות על סגירת השרת. explanation_html: 'הדואל ישלח אל %{display_count} משתמשיםות. להלן המלל שישלח בדואל:' title: צפיה מקדימה בהודעה publish: פרסום diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 272dc0eb21..ef5d8d97e6 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -319,6 +319,7 @@ hu: create: Közlemény létrehozása title: Új közlemény preview: + disclaimer: Mivel a felhasználók nem iratkozhatnak le róluk, az e-mailes értesítéseket érdemes a fontos bejelentésekre korlátozni, mint a személyes adatokat érintő adatvédelmi incidensek vagy a kiszolgáló bezárására vonatkozó értesítések. explanation_html: 'Az e-mail %{display_count} felhasználónak lesz elküldve. A következő szöveg fog szerepelni a levélben:' title: Közleményértesítés előnézete publish: Közzététel diff --git a/config/locales/is.yml b/config/locales/is.yml index ef407a6a0e..0516fa4eb8 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -319,6 +319,7 @@ is: create: Búa til auglýsingu title: Ný auglýsing preview: + disclaimer: Þar sem notendur geta ekki afþakkað þær ætti aðeins að nota tilkynningar í tölvupósti fyrir mikilvægar upplýsingar á borð við persónuleg gagnabrot eða lokanir á netþjónum. explanation_html: 'Tölvupósturinn verður sendur til %{display_count} notenda. Eftirfarandi texti verður í meginmáli póstsins:' title: Forskoða tilkynninguna publish: Birta diff --git a/config/locales/it.yml b/config/locales/it.yml index 95096b07c8..5c0a45f39a 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -319,6 +319,7 @@ it: create: Crea annuncio title: Nuovo annuncio preview: + disclaimer: Poiché gli utenti non possono disattivarle, le notifiche e-mail dovrebbero essere limitate ad annunci importanti, come notifiche di violazione dei dati personali o di chiusura del server. explanation_html: 'L''e-mail verrà inviata a %{display_count} utenti. Il seguente testo sarà incluso nell''e-mail:' title: Anteprima della notifica dell'annuncio publish: Pubblica diff --git a/config/locales/ko.yml b/config/locales/ko.yml index c4ba1fa9e7..1ce5ef2da9 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -316,6 +316,7 @@ ko: create: 공지사항 생성 title: 새 공지사항 preview: + disclaimer: 사용자들은 수신설정을 끌 수 없기 때문에 이메일 알림은 개인정보 유출이나 서버 종료와 같은 중요한 공지사항에만 사용해야 합니다. explanation_html: "%{display_count} 명의 사용자에게 이메일이 발송됩니다. 다음 내용이 이메일에 포함됩니다:" title: 공지사항 알림 미리보기 publish: 게시 diff --git a/config/locales/lv.yml b/config/locales/lv.yml index fad8a2609e..345b11116f 100644 --- a/config/locales/lv.yml +++ b/config/locales/lv.yml @@ -2,7 +2,7 @@ lv: about: about_mastodon_html: 'Nākotnes sabiedriskais tīkls: bez reklāmām, bez korporatīvās novērošanas, ētiska projektēšana un decentralizēšana. Pārvaldi savus datus ar Mastodon!' - contact_missing: Nav uzstādīts + contact_missing: Nav iestatīts contact_unavailable: N/A hosted_on: Mastodon mitināts %{domain} title: Par @@ -19,10 +19,10 @@ lv: pin_errors: following: Tev ir jāseko personai, kuru vēlies atbalstīt posts: - one: Ziņa - other: Ziņas - zero: Ziņu - posts_tab_heading: Ziņas + one: Ieraksts + other: Ieraksti + zero: Ierakstu + posts_tab_heading: Ieraksti self_follow_error: Nav ļauts sekot savam kontam admin: account_actions: @@ -120,7 +120,7 @@ lv: public: Publisks push_subscription_expires: PuSH abonements beidzas redownload: Atsvaidzināt profilu - redownloaded_msg: "%{username} profils sekmīgi atsvaidzināts no izcelsmes" + redownloaded_msg: "%{username} profils sekmīgi atsvaidzināts no pirmavota" reject: Noraidīt rejected_msg: "%{username} reģistrēšanās pieteikums sekmīgi noraidīts" remote_suspension_irreversible: Šī konta dati ir neatgriezeniski dzēsti. @@ -152,7 +152,7 @@ lv: targeted_reports: Ziņojuši citi silence: Ierobežot silenced: Ierobežots - statuses: Ziņas + statuses: Ieraksti strikes: Iepriekšējie streiki subscribe: Abonēt suspend: Apturēt @@ -176,7 +176,7 @@ lv: whitelisted: Atļauts federācijai action_logs: action_types: - approve_appeal: Apstiprināt Apelāciju + approve_appeal: Apstiprināt pārsūdzību approve_user: Apstiprināt lietotāju assigned_to_self_report: Piešķirt Pārskatu change_email_user: Mainīt lietotāja e-pasta adresi @@ -218,11 +218,11 @@ lv: memorialize_account: Saglabāt Kontu Piemiņai promote_user: Izceltt Lietotāju publish_terms_of_service: Publicēt pakalpojuma izmantošanas noteikumus - reject_appeal: Noraidīt Apelāciju + reject_appeal: Noraidīt pārsūdzību reject_user: Noraidīt lietotāju remove_avatar_user: Noņemt profila attēlu reopen_report: Atkārtoti Atvērt Ziņojumu - resend_user: Atkārtoti nosūtīt Apstiprinājuma Pastu + resend_user: Atkārtoti nosūtīt apstiprinājuma e-pasta ziņojumu reset_password_user: Atiestatīt Paroli resolve_report: Atrisināt Ziņojumu sensitive_account: Uzspiesti atzimēt kontu kā jūtīgu @@ -241,7 +241,7 @@ lv: update_status: Atjaunināt ziņu update_user_role: Atjaunināt lomu actions: - approve_appeal_html: "%{name} apstiprināja satura pārraudzības lēmuma iebildumu no %{target}" + approve_appeal_html: "%{name} apstiprināja satura pārraudzības lēmuma pārsūdzību no %{target}" approve_user_html: "%{name} apstiprināja reģistrēšanos no %{target}" assigned_to_self_report_html: "%{name} piešķīra pārskatu %{target} sev" change_email_user_html: "%{name} nomainīja lietotāja %{target} e-pasta adresi" @@ -277,7 +277,7 @@ lv: memorialize_account_html: "%{name} pārvērta %{target} kontu par atmiņas lapu" promote_user_html: "%{name} paaugstināja lietotāju %{target}" publish_terms_of_service_html: "%{name} padarīja pieejamus pakalpojuma izmantošanas noteikumu atjauninājumus" - reject_appeal_html: "%{name} noraidīja satura pārraudzības lēmuma iebildumu no %{target}" + reject_appeal_html: "%{name} noraidīja satura pārraudzības lēmuma pārsūdzību no %{target}" reject_user_html: "%{name} noraidīja reģistrēšanos no %{target}" remove_avatar_user_html: "%{name} noņēma %{target} profila attēlu" reopen_report_html: "%{name} atkārtoti atvēra ziņojumu %{target}" @@ -368,9 +368,9 @@ lv: new_users: jauni lietotāji opened_reports: atvērtie ziņojumi pending_appeals_html: - one: "%{count} izskatāmā apelācija" - other: "%{count} izskatāmās apelācijas" - zero: "%{count} izskatāmo apelāciju" + one: "%{count} izskatāma pārsūdzība" + other: "%{count} izskatāmas pārsūdzības" + zero: "%{count} izskatāmu pārsūdzību" pending_reports_html: one: "%{count}ziņojums gaida" other: "%{count}ziņojumi gaida" @@ -393,8 +393,8 @@ lv: website: Tīmekļa vietne disputes: appeals: - empty: Apelācijas netika atrastas. - title: Apelācijas + empty: Netika atrasta neviena pārsūdzība. + title: Pārsūdzības domain_allows: add_new: Atļaut federāciju ar domēnu created_msg: Domēns tika sekmīgi atļauts federācijai @@ -560,7 +560,7 @@ lv: instance_languages_dimension: Populārākās valodas instance_media_attachments_measure: saglabātie multivides pielikumi instance_reports_measure: ziņojumi par viņiem - instance_statuses_measure: saglabātās ziņas + instance_statuses_measure: saglabātie ieraksti delivery: all: Visas clear: Notīrīt piegādes kļūdas @@ -625,7 +625,7 @@ lv: disable: Atspējot disabled: Atspējots enable: Iespējot - enable_hint: Kad tas būs iespējots, tavs serveris abonēs visas publiskās ziņas no šī releja un sāks tam sūtīt šī servera publiskās ziņas. + enable_hint: Tiklīdz iespējots, serveris abonēs visus šī releja publiskos ierakstus un sāks tam sūtīt šī iservera publiskos ierakstus. enabled: Iespējots inbox_url: Releja URL pending: Gaida apstiprinājumu no releja @@ -709,7 +709,7 @@ lv: silence_html: 'Jūs gatavojaties ierobežot @%{acct} kontu. Tas:' suspend_html: 'Jūs gatavojaties apturēt @%{acct} kontu. Tas:' actions: - delete_html: Noņemt aizskarošās ziņas + delete_html: Noņemt aizskarošos ierakstus mark_as_sensitive_html: Atzīmēt aizskarošo ierakstu informācijas nesējus kā jūtīgus silence_html: Ievērojami ierobežo @%{acct} sasniedzamību, padarot viņa profilu un saturu redzamu tikai cilvēkiem, kas jau seko tam vai pašrocīgi uzmeklē profilu suspend_html: Apturēt @%{acct}, padarot viņu profilu un saturu nepieejamu un neiespējamu mijiedarbību ar @@ -720,7 +720,7 @@ lv: record_strike_html: Ierakstiet brīdinājumu pret @%{acct}, lai palīdzētu jums izvērst turpmākus pārkāpumus no šī konta send_email_html: Nosūtīt @%{acct} brīdinājuma e-pasta ziņojumu warning_placeholder: Izvēles papildu pamatojums satura pārraudzības darbībai. - target_origin: Ziņotā konta izcelsme + target_origin: Konta, par kuru ziņots, izcelsme title: Ziņojumi unassign: Atsaukt unknown_action_msg: 'Nezināms konts: %{action}' @@ -757,8 +757,8 @@ lv: invite_users_description: Ļauj lietotājiem uzaicināt jaunus cilvēkus uz šo serveri manage_announcements: Pārvaldīt Paziņojumus manage_announcements_description: Ļauj lietotājiem pārvaldīt paziņojumus serverī - manage_appeals: Pārvaldīt Pārsūdzības - manage_appeals_description: Ļauj lietotājiem pārskatīt iebildumus pret satura pārraudzības darbībām + manage_appeals: Pārvaldīt pārsūdzības + manage_appeals_description: Ļauj lietotājiem pārskatīt pārsūdzības pret satura pārraudzības darbībām manage_blocks: Pārvaldīt Bloķus manage_blocks_description: Ļauj lietotājiem liegt e-pasta pakalpojumu sniedzējus un IP adreses manage_custom_emojis: Pārvaldīt Pielāgotās Emocijzīmes @@ -882,12 +882,12 @@ lv: title: Multivide metadata: Metadati no_history: Šis ieraksts nav bijis labots - no_status_selected: Neviena ziņa netika mainīta, jo neviena netika atlasīta + no_status_selected: Neviens ieraksts netika mainīts, jo nekas netika atlasīts open: Atvērt ziņu - original_status: Oriģinālā ziņa + original_status: Sākotnējais ieraksts reblogs: Reblogi replied_to_html: Atbildēja %{acct_link} - status_changed: Ziņa mainīta + status_changed: Ieraksts izmainīts status_title: Publicēja @%{name} title: Konta ieraksti - @%{name} trending: Aktuāli @@ -904,8 +904,8 @@ lv: silence: "%{name} ierobežoja %{target} kontu" suspend: "%{name} apturēja %{target} kontu" appeal_approved: Pārsūdzēts - appeal_pending: Apelācija tiek izskatīta - appeal_rejected: Apelācija noraidīta + appeal_pending: Pārsūdzība gaida izskatīšanu + appeal_rejected: Pārsūdzība noraidīta system_checks: database_schema_check: message_html: Ir nepabeigtas datubāzes migrācijas. Lūgums palaist tās, lai nodrošinātu, ka lietotne darbojas, kā paredzēts @@ -1117,9 +1117,9 @@ lv: sensitive: lai atzīmētu viņu kontu kā jūtīgu silence: lai ierobežotu viņu kontu suspend: lai apturētu viņu kontu - body: "%{target} iebilst %{action_taken_by} satura pārraudzības lēmumam no %{date}, kas bija %{type}. Viņi rakstīja:" - next_steps: Vari apstiprināt iebildumu, lai atsauktu satura pārraudzības lēmumu, vai neņemt to vērā. - subject: "%{username} iebilst satura pārraudzības lēmumam par %{instance}" + body: "%{target} iebilst %{action_taken_by} satura pārraudzības lēmumam no %{date}, kas bija, %{type}. Viņi rakstīja:" + next_steps: Vari apstiprināt pārsūdzību, lai atsauktu satura pārraudzības lēmumu, vai neņemt to vērā. + subject: "%{username} pārsūdz satura pārraudzības lēmumam par %{instance}" new_critical_software_updates: body: Ir izlaistas jaunas Mastodon svarīgās versijas, iespējams, vēlēsies to atjaunināt pēc iespējas ātrāk! subject: "%{instance} ir pieejami svarīgi Mastodon atjauninājumi!" @@ -1183,7 +1183,7 @@ lv: hint_html: Vēl tikai viena lieta! Mums ir jāapstiprina, ka tu esi cilvēks (tas ir tāpēc, lai mēs varētu nepieļaut surogātpasta izsūtīšanu!). Atrisini tālāk norādīto CAPTCHA un noklikšķini uz "Turpināt". title: Drošības pārbaude confirmations: - awaiting_review: E-pasta adrese ir apstiprināta. %{domain} darbinieki tagad pārskata reģistrāciju. Tiks saņemts e-pasta ziņojums, ja viņi apstiprinās kontu. + awaiting_review: E-pasta adrese ir apstiprināta. %{domain} personāls tagad pārskata reģistrāciju. Tiks saņemts e-pasta ziņojums, ja viņi apstiprinās kontu. awaiting_review_title: Tava reģistrācija tiek izskatīta clicking_this_link: klikšķinot šo saiti login_link: pieteikties @@ -1311,19 +1311,19 @@ lv: disputes: strikes: action_taken: Veiktā darbība - appeal: Apelācija + appeal: Pārsūdzēt appeal_approved: Šis brīdinājums tika sekmīgi pārsūdzēts un vairs nav spēkā - appeal_rejected: Apelācija ir noraidīta - appeal_submitted_at: Apelācija iesniegta + appeal_rejected: Pārsūdzība ir noraidīta + appeal_submitted_at: Pārsūdzība iesniegta appealed_msg: Tava pārsūdzība ir iesniegta. Ja tā tiks apstiprināta, Tev tiks paziņots. appeals: - submit: Iesniegt apelāciju - approve_appeal: Apstiprināt apelāciju + submit: Iesniegt pārsūdzību + approve_appeal: Apstiprināt pārsūdzību associated_report: Saistītais ziņojums created_at: Datēts - description_html: Šīs ir darbības, kas veiktas pret Tavu kontu, un brīdinājumi, kurus Tev ir nosūtījuši %{instance} darbinieki. + description_html: Šīs ir darbības, kas veiktas pret Tavu kontu, un brīdinājumi, kurus Tev ir nosūtījuši %{instance} personāls. recipient: Adresēts - reject_appeal: Noraidīt apelāciju + reject_appeal: Noraidīt pārsūdzību status: 'Publikācija #%{id}' status_removed: Publikācija jau ir noņemta no sistēmas title: "%{action} kopš %{date}" @@ -1336,7 +1336,7 @@ lv: silence: Konta ierobežošana suspend: Konta apturēšana your_appeal_approved: Tava pārsūdzība tika apstiprināta - your_appeal_pending: Jūs esat iesniedzis apelāciju + your_appeal_pending: Tu iesniedzi pārsūdzību your_appeal_rejected: Tava pārsūdzība tika noraidīta edit_profile: basic_information: Pamata informācija @@ -1939,7 +1939,7 @@ lv: sensitive_content: Jūtīgs saturs strikes: errors: - too_late: Brīdinājuma apstrīdēšanas laiks ir nokavēts + too_late: Par vēlu pārsūdzēt šo brīdinājumu tags: does_not_match_previous_name: nesakrīt ar iepriekšējo nosaukumu terms_of_service: @@ -1983,12 +1983,13 @@ lv: action: Konta iestatījumi explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, ir apstiprināta. Tavs konts atkal ir labā stāvoklī. subject: Tava %{date} iesniegtā pārsūdzība tika apstiprināta - title: Apelācija apstiprināta + subtitle: Tavs konts atkal ir labā stāvoklī. + title: Pārsūdzība apstiprināta appeal_rejected: explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, tika noraidīta. subject: Tava %{date} iesniegta pārsūdzība tika noraidīta subtitle: Tava pārsūdzība tika noraidīta. - title: Apelācija noraidīta + title: Pārsūdzība noraidīta backup_ready: explanation: Tu pieprasīji pilnu sava Mastodon konta rezerves kopiju. extra: Tā tagad ir gatava lejupielādei. @@ -2010,13 +2011,14 @@ lv: terms_of_service_changed: agreement: Ar %{domain} izmantošanas tuprināšanu tiek piekrists šiem noteikumiem. Ja ir iebildumi pret atjauninātajiem noteikumiem, savu piekrišanu var atcelt jebkurā laikā ar sava konta izdzēšanu. changelog: 'Šeit īsumā ir aprakstīts, ko šis atjauninājums nozīmē:' + description: 'Šis e-pasta ziņojums tika saņemts, jo mēs veicam dažas izmaiņas savos pakalpojuma izmantošanas noteikumos %{domain}. Šie atjauninājumi stāsies spēkā %{date}. Mēs aicinām pārskatīt pilnus atjauninātos noteikumus šeit:' sign_off: "%{domain} komanda" subject: Mūsu pakalpojuma izmantošanas noteikumu atjauninājumi subtitle: Mainās %{domain} pakalpojuma izmantošanas noteikumi title: Svarīgs atjauninājums warning: - appeal: Iesniegt apelāciju - appeal_description: Ja uzskatāt, ka tā ir kļūda, varat iesniegt apelāciju %{instance} darbiniekiem. + appeal: Iesniegt pārsūdzību + appeal_description: Ja uzskati, ka tā ir kļūda, vari iesniegt pārsūdzību %{instance} personālam. categories: spam: Spams violation: Saturs pārkāpj šādas kopienas pamatnostādnes @@ -2054,7 +2056,10 @@ lv: edit_profile_title: Pielāgo savu profilu explanation: Šeit ir daži padomi, kā sākt darbu feature_action: Uzzināt vairāk + feature_audience_title: Veido savu sekotāju pulku ar pārliecību + feature_control_title: Turi savu laika joslu savā pārvaldībā feature_creativity: Mastodon nodrošina skaņas, video un attēlu ierakstus, pieejamības aprakstus, aptaujas, satura brīdinājumus, animētus profila attēlus, pielāgotas emocijzīmes, sīktēlu apgriešanas vadīklas un vēl, lai palīdzētu Tev sevi izpaust tiešsaistē. Vai Tu izplati savu mākslu, mūziku vai aplādes, Mastodon ir šeit ar Tevi. + feature_creativity_title: Nepārspējams radošums feature_moderation: Mastodon nodod lēmumu pieņemšanu atpakaļ Tavās rokās. Katrs serveris izveido savus noteikumus un nosacījumus, kas tiek nodrošināti vietēji, ne kā lieliem uzņēmumiem piederošos sabiedriskajos medijiem, padarot katru serveri par vispielāgojamāko un visatsaucīgāko dažādu cilvēku kopu vajadzībām. Pievienojies serverim, kura noteikumiem Tu piekrīti, vai izvieto savu! feature_moderation_title: Satura pārraudzība, kādai tai būtu jābūt follow_action: Sekot diff --git a/config/locales/ms.yml b/config/locales/ms.yml index 7fc8adcf41..68f4e872b6 100644 --- a/config/locales/ms.yml +++ b/config/locales/ms.yml @@ -28,6 +28,7 @@ ms: created_msg: Catatan penyederhanaan telah berjaya dicipta! destroyed_msg: Catatan penyederhanaan telah berjaya dipadam! accounts: + add_email_domain_block: Sekat domain e-mel approve: Luluskan approved_msg: Berjaya meluluskan permohonan pendaftaran %{username} are_you_sure: Adakah anda pasti? @@ -146,7 +147,7 @@ ms: suspension_irreversible: Data akaun ini telah dipadam secara kekal. Anda boleh nyahgantungkannya untuk membuatkan akaun ini boleh digunakan semula tetapi data lama tidak akan diperolehi. suspension_reversible_hint_html: Akaun ini telah digantung, dan datanya akan dibuang pada %{date}. Sebelum tarikh itu, akaun ini boleh diperoleh semula tanpa kesan buruk. Jika anda mahu memadamkan kesemua data akaun ini serta-merta, anda boleh melakukannya di bawah. title: Akaun - unblock_email: Menyahsekat alamat e-mel + unblock_email: Nyahsekat alamat e-mel unblocked_email_msg: Alamat e-mel %{username} berjaya dinyahsekat unconfirmed_email: E-mel belum disahkan undo_sensitized: Nyahtanda sensitif @@ -169,17 +170,21 @@ ms: confirm_user: Sahkan Pengguna create_account_warning: Cipta Amaran create_announcement: Cipta Pengumuman + create_canonical_email_block: Cipta Penyekatan E-mel create_custom_emoji: Cipta Emoji Tersendiri create_domain_allow: Cipta Pelepasan Domain create_domain_block: Cipta Penyekatan Domain + create_email_domain_block: Cipta Penyekatan Domain E-mel create_ip_block: Cipta peraturan alamat IP create_unavailable_domain: Cipta Domain Tidak Tersedia create_user_role: Cipta Peranan demote_user: Turunkan Taraf Pengguna destroy_announcement: Padam Pengumuman + destroy_canonical_email_block: Padam Penyekatan E-mel destroy_custom_emoji: Padam Emoji Tersendiri destroy_domain_allow: Padam Pelepasan Domain destroy_domain_block: Padam Penyekatan Domain + destroy_email_domain_block: Padam Penyekatan Domain E-mel destroy_instance: Padamkan Domain destroy_ip_block: Padam peraturan alamat IP destroy_status: Padam Hantaran @@ -203,7 +208,7 @@ ms: silence_account: Diamkan Akaun suspend_account: Gantungkan Akaun unassigned_report: Menyahtugaskan Laporan - unblock_email_account: Menyahsekat alamat e-mel + unblock_email_account: Nyahsekat alamat e-mel unsensitive_account: Nyahtanda media di akaun anda sebagai sensitif unsilence_account: Nyahdiamkan Akaun unsuspend_account: Nyahgantungkan Akaun @@ -1144,7 +1149,7 @@ ms: csv: CSV domain_blocks: Domain disekat lists: Senarai - mutes: Awak bisu + mutes: Redaman anda storage: Storan Media featured_tags: add_new: Tambah baharu @@ -1235,10 +1240,11 @@ ms: domain_blocking: Mengimport domain yang disekat following: Mengimport akaun diikuti lists: Mengimport senarai - muting: Mengimport akaun diredam + muting: Mengimport akaun teredam type: Jenis import type_groups: constructive: Ikutan & Penanda Halaman + destructive: Sekatan dan redaman types: blocking: Senarai menyekat bookmarks: Penanda buku diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 84608307d4..5acd93ca13 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -319,6 +319,7 @@ nl: create: Mededeling aanmaken title: Nieuwe mededeling preview: + disclaimer: Omdat gebruikers zich niet voor deze e-mails kunnen afmelden, moeten e-mailmeldingen worden beperkt tot belangrijke aankondigingen, zoals het lekken van gebruikersgegevens of meldingen over het sluiten van deze server. explanation_html: 'De e-mail wordt verzonden naar %{display_count} gebruikers. De volgende tekst wordt in het bericht opgenomen:' title: Voorbeeld van mededeling publish: Inschakelen diff --git a/config/locales/nn.yml b/config/locales/nn.yml index bcdcf01d9a..30ddcaeb6a 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -21,7 +21,7 @@ nn: one: Tut other: Tut posts_tab_heading: Tut - self_follow_error: Det er ikkje tillate å følgje din eigen konto + self_follow_error: Du kan ikkje fylgja deg sjølv admin: account_actions: action: Utfør @@ -309,6 +309,7 @@ nn: title: Revisionslogg unavailable_instance: "(domenenamn er utilgjengeleg)" announcements: + back: Tilbake til kunngjeringane destroyed_msg: Kunngjøringen er slettet! edit: title: Rediger kunngjøring @@ -317,6 +318,10 @@ nn: new: create: Lag kunngjøring title: Ny kunngjøring + preview: + disclaimer: Av di folk ikkje kan velja bort epostvarsel, bør du avgrensa dei til viktige kunngjeringar som datainnbrot eller varsel om at tenaren skal stengja. + explanation_html: 'Denne eposten blir send til %{display_count} folk. Denne teksten vil stå i eposten:' + title: Førehandsvis kunngjeringa publish: Publiser published_msg: Kunngjøring publisert! scheduled_for: Planlagt for %{time} @@ -475,11 +480,41 @@ nn: new: title: Importer domeneblokkeringar no_file: Inga fil vald + fasp: + debug: + callbacks: + created_at: Oppretta + delete: Slett + ip: IP-adresse + request_body: Meldingskropp i førespurnaden + title: Avlusingstilbakekall + providers: + active: Aktiv + base_url: Basisadresse + callback: Tilbakekall + delete: Slett + edit: Rediger leverandør + finish_registration: Fullfør registrering + name: Namn + providers: Leverandørar + public_key_fingerprint: Offentleg nøkkelavtrykk + registration_requested: Nokon vil registrera seg + registrations: + confirm: Stadfest + description: Du har fått ei registrering frå ein tenesteleverandør. Avslå registreringa viss du ikkje sette i gang dette. Viss du sette i gang registreringa, må du samanlikna namnet og nøkkelavtrykket nøye før du stadfestar registreringa. + reject: Avslå + title: Stadfest registrering via tenestetilbydar + save: Lagre + select_capabilities: Vel eigenskapar + sign_in: Logg inn + status: Status + title: Tilleggstenesteleverandørar for allheimen + title: Tenestleverandør follow_recommendations: description_html: "Fylgjeforslag hjelper nye brukarar å finna interessant innhald raskt. Om ein brukar ikkje har samhandla nok med andre til å få tilpassa fylgjeforslag, blir desse kontoane føreslått i staden. Dei blir rekna ut på nytt kvar dag ut frå ei blanding av kva kontoar som har mykje nyleg aktivitet og høgast tal på fylgjarar på eit bestemt språk." language: For språk status: Status - suppress: Demp følgjeforslag + suppress: Demp fylgjeforslag suppressed: Dempa title: Fylgjeforslag unsuppress: Nullstill fylgjeforslag @@ -939,6 +974,7 @@ nn: chance_to_review_html: "Dei genererte bruksvilkåra blir ikkje lagde ut automatisk Du får høve til å sjå gjennom resultatet og fylla inn dei detaljane som trengst." explanation_html: Malen for bruksvilkår er berre til informasjon, og du bør ikkje gå ut frå han som juridiske råd. Viss du har spørsmål om lovverk, bør du spørja ein advokat. title: Oppsett for bruksvilkår + going_live_on_html: I bruk frå %{date} history: Historikk live: Direkte no_history: Det er ikkje registrert nokon endringar i bruksvilkåra enno. @@ -1681,9 +1717,9 @@ nn: confirm_remove_selected_follows: Er du sikker på at du ikkje vil fylgja desse? dormant: I dvale follow_failure: Greidde ikkje fylgja alle kontoane du valde. - follow_selected_followers: Følg valgte tilhengere + follow_selected_followers: Fylg desse som fylgjer deg followers: Fylgjarar - following: Følginger + following: Folk du fylgjer invited: Innboden last_active: Sist aktiv most_recent: Sist @@ -1904,6 +1940,10 @@ nn: recovery_instructions_html: Hvis du skulle miste tilgang til telefonen din, kan du bruke en av gjenopprettingskodene nedenfor til å gjenopprette tilgang til din konto. Oppbevar gjenopprettingskodene sikkert, for eksempel ved å skrive dem ut og gjemme dem på et lurt sted bare du vet om. webauthn: Sikkerhetsnøkler user_mailer: + announcement_published: + description: 'Styrarane på %{domain} har ei kunngjering:' + subject: Kunngjering om tenesta + title: Kunngjering frå %{domain} appeal_approved: action: Kontoinnstillingar explanation: Apellen på prikken mot din kontor på %{strike_date} som du la inn på %{appeal_date} har blitt godkjend. Din konto er nok ein gong i god stand. @@ -1936,6 +1976,8 @@ nn: terms_of_service_changed: agreement: Viss du held fram å bruka %{domain}, seier du deg einig i vilkåra. Viss du er usamd i dei oppdaterte vilkåra, kan du slutta å bruka %{domain} når du vil ved å sletta brukarkontoen din. changelog: 'Denne oppdateringa, kort fortalt:' + description: 'Du får denne eposten fordi me har endra tenestvilkåra på %{domain}. Desse endringane kjem i kraft %{date}. Me oppmodar deg til å sjå på dei oppdaterte vilkåra her:' + description_html: Du får denne eposten fordi me har endra tenestvilkåra på %{domain}. Desse endringane kjem i kraft %{date}. Me oppmodar deg til å sjå på dei oppdaterte vilkåra her. sign_off: Folka på %{domain} subject: Endra bruksvilkår subtitle: Bruksvilkåra på %{domain} er endra @@ -1945,7 +1987,7 @@ nn: appeal_description: Om du meiner dette er ein feil, kan du sende inn ei klage til gjengen i %{instance}. categories: spam: Søppelpost - violation: Innhald bryter følgjande retningslinjer + violation: Innhaldet bryt med desse retningslinene explanation: delete_statuses: Nokre av innlegga dine er bryt éin eller fleire retningslinjer, og har så blitt fjerna av moderatorene på %{instance}. disable: Du kan ikkje lenger bruke kontoen, men profilen din og andre data er intakt. Du kan be om ein sikkerhetskopi av dine data, endre kontoinnstillingar eller slette din konto. diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 8c06332695..dca4fff60c 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -496,8 +496,19 @@ ru: title: Импорт доменных блокировок no_file: Файл не выбран fasp: + debug: + callbacks: + delete: Удалить + ip: IP-адрес providers: + base_url: Основной URL + delete: Удалить + registrations: + confirm: Подтвердить + reject: Отклонить + save: Сохранить sign_in: + status: Пост follow_recommendations: description_html: "Следуйте рекомендациям, чтобы помочь новым пользователям быстро находить интересный контент. Если пользователь не взаимодействовал с другими в достаточной степени, чтобы сформировать персонализированные рекомендации, вместо этого рекомендуется использовать эти учетные записи. Они пересчитываются на ежедневной основе на основе комбинации аккаунтов с наибольшим количеством недавних взаимодействий и наибольшим количеством местных подписчиков для данного языка." language: Для языка diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index 62de683213..3b82cd3c75 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -75,6 +75,7 @@ el: filters: action: Επιλέξτε ποια ενέργεια θα εκτελεστεί όταν μια δημοσίευση ταιριάζει με το φίλτρο actions: + blur: Απόκρυψη πολυμέσων πίσω από μια προειδοποίηση, χωρίς να κρύβεται το ίδιο το κείμενο hide: Πλήρης αποκρυψη του φιλτραρισμένου περιεχομένου, συμπεριφέρεται σαν να μην υπήρχε warn: Απόκρυψη φιλτραρισμένου περιεχομένου πίσω από μια προειδοποίηση που αναφέρει τον τίτλο του φίλτρου form_admin_settings: @@ -88,6 +89,7 @@ el: favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο. mascot: Παρακάμπτει την εικονογραφία στην προηγμένη διεπαφή ιστού. media_cache_retention_period: Τα αρχεία πολυμέσων από αναρτήσεις που γίνονται από απομακρυσμένους χρήστες αποθηκεύονται προσωρινά στο διακομιστή σου. Όταν οριστεί μια θετική τιμή, τα μέσα θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών. Αν τα δεδομένα πολυμέσων ζητηθούν μετά τη διαγραφή τους, θα γίνει ε, αν το πηγαίο περιεχόμενο είναι ακόμα διαθέσιμο. Λόγω περιορισμών σχετικά με το πόσο συχνά οι κάρτες προεπισκόπησης συνδέσμων συνδέονται σε ιστοσελίδες τρίτων, συνιστάται να ορίσεις αυτή την τιμή σε τουλάχιστον 14 ημέρες ή οι κάρτες προεπισκόπησης συνδέσμων δεν θα ενημερώνονται κατ' απάιτηση πριν από εκείνη την ώρα. + min_age: Οι χρήστες θα κληθούν να επιβεβαιώσουν την ημερομηνία γέννησής τους κατά την εγγραφή peers_api_enabled: Μια λίστα με ονόματα τομέα που συνάντησε αυτός ο διακομιστής στο fediverse. Δεν περιλαμβάνονται δεδομένα εδώ για το αν συναλλάσσετε με ένα συγκεκριμένο διακομιστή, μόνο ότι ο διακομιστής σας το ξέρει. Χρησιμοποιείται από υπηρεσίες που συλλέγουν στατιστικά στοιχεία για την συναλλαγή με γενική έννοια. profile_directory: Ο κατάλογος προφίλ παραθέτει όλους τους χρήστες που έχουν επιλέξει να είναι ανακαλύψιμοι. require_invite_text: 'Όταν η εγγραφή απαιτεί χειροκίνητη έγκριση, κάνε το πεδίο κειμένου: «Γιατί θέλετε να συμμετάσχετε;» υποχρεωτικό αντί για προαιρετικό' @@ -132,14 +134,21 @@ el: name: Μπορείς να αλλάξεις μόνο το πλαίσιο των χαρακτήρων, για παράδειγμα για να γίνει περισσότερο ευανάγνωστο terms_of_service: changelog: Μπορεί να δομηθεί με σύνταξη Markdown. + effective_date: Ένα λογικό χρονικό πλαίσιο μπορεί να κυμαίνεται οποτεδήποτε από 10 έως 30 ημέρες από την ημερομηνία που ενημερώνετε τους χρήστες σας. text: Μπορεί να δομηθεί με σύνταξη Markdown. terms_of_service_generator: admin_email: Οι νομικές ανακοινώσεις περιλαμβάνουν αντικρούσεις, δικαστικές αποφάσεις, αιτήματα που έχουν ληφθεί και αιτήματα επιβολής του νόμου. + arbitration_address: Μπορεί να είναι το ίδιο με τη φυσική διεύθυνση παραπάνω, ή “Μ/Δ” εάν χρησιμοποιείται email. + arbitration_website: Μπορεί να είναι μια φόρμα ιστού ή “Μ/Δ” εάν χρησιμοποιείται email. + choice_of_law: Η πόλη, η περιοχή, το έδαφος ή οι εσωτερικοί ουσιαστικοί νόμοι των οποίων διέπουν όλες τις αξιώσεις. dmca_address: Για τους φορείς των ΗΠΑ, χρησιμοποιήστε τη διεύθυνση που έχει καταχωρηθεί στο DMCA Designated Agent Directory. A P.O. Η λίστα είναι διαθέσιμη κατόπιν απευθείας αιτήματος, Χρησιμοποιήστε το αίτημα απαλλαγής από την άδεια χρήσης του καθορισμένου από το DMCA Agent Post Office Box για να στείλετε email στο Γραφείο Πνευματικών Δικαιωμάτων και περιγράψτε ότι είστε συντονιστής περιεχομένου με βάση το σπίτι, ο οποίος φοβάται την εκδίκηση ή την απόδοση για τις ενέργειές σας και πρέπει να χρησιμοποιήσετε ένα P.. Box για να αφαιρέσετε τη διεύθυνση οικίας σας από τη δημόσια προβολή. + dmca_email: Μπορεί να είναι το ίδιο email που χρησιμοποιείται για “Διεύθυνση email για νομικές ανακοινώσεις” παραπάνω. domain: Μοναδικό αναγνωριστικό της διαδικτυακής υπηρεσίας που παρέχεις. jurisdiction: Ανέφερε τη χώρα όπου ζει αυτός που πληρώνει τους λογαριασμούς. Εάν πρόκειται για εταιρεία ή άλλη οντότητα, ανέφερε τη χώρα όπου υφίσταται, και την πόλη, περιοχή, έδαφος ή πολιτεία ανάλογα με την περίπτωση. + min_age: Δεν πρέπει να είναι κάτω από την ελάχιστη ηλικία που απαιτείται από τους νόμους της δικαιοδοσίας σας. user: chosen_languages: Όταν ενεργοποιηθεί, στη δημόσια ροή θα εμφανίζονται τουτ μόνο από τις επιλεγμένες γλώσσες + date_of_birth: Πρέπει να βεβαιωθείς ότι είσαι τουλάχιστον %{age} για να χρησιμοποιήσεις το Mastodon. Δεν θα το αποθηκεύσουμε. role: Ο ρόλος ελέγχει ποια δικαιώματα έχει ο χρήστης. user_role: color: Το χρώμα που θα χρησιμοποιηθεί για το ρόλο σε ολόκληρη τη διεπαφή, ως RGB σε δεκαεξαδική μορφή @@ -252,6 +261,7 @@ el: name: Ετικέτα filters: actions: + blur: Απόκρυψη πολυμέσων με προειδοποίηση hide: Πλήρης απόκρυψη warn: Απόκρυψη με προειδοποίηση form_admin_settings: @@ -265,6 +275,7 @@ el: favicon: Favicon mascot: Προσαρμοσμένη μασκότ (απαρχαιωμένο) media_cache_retention_period: Περίοδος διατήρησης προσωρινής μνήμης πολυμέσων + min_age: Ελάχιστη απαιτούμενη ηλικία peers_api_enabled: Δημοσίευση λίστας των εντοπισμένων διακομιστών στο API profile_directory: Ενεργοποίηση καταλόγου προφίλ registrations_mode: Ποιος μπορεί να εγγραφεί @@ -330,16 +341,22 @@ el: usable: Να επιτρέπεται η τοπική χρήση αυτής της ετικέτας από αναρτήσεις terms_of_service: changelog: Τι άλλαξε; + effective_date: Ημερομηνία έναρξης ισχύος text: Όροι Παροχής Υπηρεσιών terms_of_service_generator: admin_email: Διεύθυνση email για τις νομικές ανακοινώσεις arbitration_address: Φυσική διεύθυνση για τις ανακοινώσεις διαιτησίας arbitration_website: Ιστοσελίδα για την υποβολή ειδοποιήσεων διαιτησίας + choice_of_law: Επιλογή νόμου dmca_address: Φυσική διεύθυνση για ειδοποιήσεις DMCA/πνευματικών δικαιωμάτων dmca_email: Διεύθυνση email για ειδοποιήσεις DMCA/πνευματικών δικαιωμάτων domain: Τομέας jurisdiction: Νομική δικαιοδοσία + min_age: Ελάχιστη ηλικία user: + date_of_birth_1i: Ημέρα + date_of_birth_2i: Μήνας + date_of_birth_3i: Έτος role: Ρόλος time_zone: Ζώνη ώρας user_role: diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index a823604584..661028fff0 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -146,6 +146,7 @@ ko: min_age: 관할지역의 법률에서 요구하는 최저 연령보다 작으면 안 됩니다. user: chosen_languages: 체크하면, 선택 된 언어로 작성된 게시물들만 공개 타임라인에 보여집니다 + date_of_birth: 마스토돈을 사용하려면 %{age}세 이상임을 확인해야 합니다. 이 정보는 저장되지 않습니다. role: 역할은 사용자가 어떤 권한을 가지게 될 지 결정합니다. user_role: color: 색상은 사용자 인터페이스에서 역할을 나타내기 위해 사용되며, RGB 16진수 형식입니다 diff --git a/config/locales/simple_form.lv.yml b/config/locales/simple_form.lv.yml index 0c362b0a30..287ce36a5d 100644 --- a/config/locales/simple_form.lv.yml +++ b/config/locales/simple_form.lv.yml @@ -25,7 +25,7 @@ lv: type_html: Izvēlies, ko darīt ar %{acct} types: disable: Neļauj lietotājam izmantot savu kontu, bet neizdzēs vai neslēp tā saturu. - none: Izmanto šo, lai nosūtītu lietotājam brīdinājumu, neradot nekādas citas darbības. + none: Šis ir izmantojams, lai nosūtītu lietotājam brīdinājumu bez jebkādu citu darbību izraisīšanas. sensitive: Visus šī lietotāja informācijas nesēju pielikumus uzspiesti atzīmēt kā jūtīgus. silence: Neļaut lietotājam veikt ierakstus ar publisku redzamību, paslēpt viņa ierakstus un paziņojumus no cilvēkiem, kas tam neseko. Tiek aizvērti visi ziņojumi par šo kontu. suspend: Novērs jebkādu mijiedarbību no šī konta vai uz to un dzēs tā saturu. Atgriežams 30 dienu laikā. Tiek aizvērti visi šī konta pārskati. @@ -45,8 +45,8 @@ lv: context: Viens vai vairāki konteksti, kur jāpiemēro filtrs current_password: Drošības nolūkos, lūdzu, ievadi pašreizējā konta paroli current_username: Lai apstiprinātu, lūdzu, ievadi pašreizējā konta paroli - digest: Tiek nosūtīts tikai pēc ilgstošas bezdarbības un tikai tad, ja savas prombūtnes laikā esi saņēmis jebkādas personīgas ziņas - email: Tev tiks nosūtīts apstiprinājuma e-pasts + digest: Tiek nosūtīts tikai pēc ilgstošas bezdarbības un tikai tad, ja savas prombūtnes laikā saņēmi jebkādas personīgas ziņas + email: Tev tiks nosūtīts apstiprinājuma e-pasta ziņojums header: WEBP, PNG, GIF vai JPG. Ne vairāk kā %{size}. Tiks samazināts līdz %{dimensions}px inbox_url: Nokopē URL no tā releja sākumlapas, kuru vēlies izmantot irreversible: Filtrētās ziņas neatgriezeniski pazudīs, pat ja filtrs vēlāk tiks noņemts @@ -125,7 +125,7 @@ lv: hint: Izvēles. Sniedz vairāk informācijas par noteikumu text: Jāapraksta nosacījums vai prasība šī servera lietotājiem. Jāmēģina to veidot īsu un vienkāršu sessions: - otp: 'Jāievada tālruņa lietotnes izveidots divpakāpju kods vai jāizmanto viens no saviem atkopes kodie:' + otp: 'Jāievada tālruņa lietotnes izveidots divpakāpju kods vai jāizmanto viens no saviem atkopes kodiem:' webauthn: Ja tā ir USB atslēga, noteikti ievieto to un, ja nepieciešams, pieskaries tai. settings: indexable: Tava profila lapa var tikt parādīta Google, Bing un citu meklēšanas dzinēju rezultātos. @@ -213,7 +213,7 @@ lv: max_uses: Maksimālais lietojumu skaits new_password: Jauna parole note: Par sevi - otp_attempt: Divfaktoru kods + otp_attempt: Divpakāpju kods password: Parole phrase: Atslēgvārds vai frāze setting_advanced_layout: Iespējot paplašināto tīmekļa saskarni diff --git a/config/locales/simple_form.nn.yml b/config/locales/simple_form.nn.yml index 1a33a4b91d..b742375f12 100644 --- a/config/locales/simple_form.nn.yml +++ b/config/locales/simple_form.nn.yml @@ -89,6 +89,7 @@ nn: favicon: WEBP, PNG, GIF eller JPG. Overstyrer det standarde Mastodon-favikonet med eit eigendefinert ikon. mascot: Overstyrer illustrasjonen i det avanserte webgrensesnittet. media_cache_retention_period: Mediafiler frå innlegg laga av eksterne brukarar blir bufra på serveren din. Når sett til ein positiv verdi, slettast media etter eit gitt antal dagar. Viss mediedata blir førespurt etter det er sletta, vil dei bli lasta ned på nytt viss kjelda sitt innhald framleis er tilgjengeleg. På grunn av restriksjonar på kor ofte lenkeførehandsvisningskort lastar tredjepart-nettstadar, rådast det til å setje denne verdien til minst 14 dagar, eller at førehandsvisningskort ikkje blir oppdatert på førespurnad før det tidspunktet. + min_age: Brukarane vil bli bedne om å stadfesta fødselsdatoen sin når dei registrerer seg peers_api_enabled: Ei liste over domenenamn denne tenaren har møtt på i allheimen. Det står ingenting om tenaren din samhandlar med ein annan tenar, berre om tenaren din veit om den andre. Dette blir brukt av tenester som samlar statistikk om føderering i det heile. profile_directory: Profilkatalogen viser alle brukarar som har valt å kunne bli oppdaga. require_invite_text: Når registrering krev manuell godkjenning, lyt du gjera tekstfeltet "Kvifor vil du bli med?" obligatorisk i staden for valfritt @@ -133,11 +134,13 @@ nn: name: Du kan berre endra bruken av store/små bokstavar, t. d. for å gjera det meir leseleg terms_of_service: changelog: Du kan bruka Markdown-syntaks for struktur. + effective_date: Ei rimeleg ventetid kan variera frå 10 til 30 dagar frå den dagen du varsla folka som bruker denne tenaren. text: Du kan bruka Markdown-syntaks for struktur. terms_of_service_generator: admin_email: Juridiske merknader kan vera motsegner, rettsavgjerder, orskurdar eller førespurnader om sletting. arbitration_address: Kan vere lik den fysiske adressa over, eller "N/A" viss du bruker epost. arbitration_website: Kan vere eit nettskjema eller "N/A" viss du bruker e-post. + choice_of_law: Jurisdiksjon dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view. dmca_email: Kan vere same e-post som brukast i "E-postadresse for juridiske meldingar" ovanfor. domain: Noko som identifiserer den nettenesta du tilbyr. @@ -145,6 +148,7 @@ nn: min_age: Skal ikkje vere under minstealder som krevst av lover i jurisdiksjonen din. user: chosen_languages: Når merka vil berre tuta på dei valde språka synast på offentlege tidsliner + date_of_birth: Me må syta for at du er minst %{age} for å bruka Masodon. Me lagrar ikkje dette. role: Rolla kontrollerer kva løyve brukaren har. user_role: color: Fargen som skal nyttast for denne rolla i heile brukargrensesnittet, som RGB i hex-format @@ -165,7 +169,7 @@ nn: value: Innhald indexable: Ta med offentlege innlegg i søkjeresultat show_collections: Vis dei du fylgjer og dei som fylgjer deg på profilen din - unlocked: Godta nye følgjare automatisk + unlocked: Godta nye fylgjarar automatisk account_alias: acct: Brukarnamnet på den gamle kontoen account_migration: @@ -271,6 +275,7 @@ nn: favicon: Favorittikon mascot: Eigendefinert maskot (eldre funksjon) media_cache_retention_period: Oppbevaringsperiode for mediebuffer + min_age: Minste aldersgrense peers_api_enabled: Legg ut ei liste over oppdaga tenarar i APIet profile_directory: Aktiver profilkatalog registrations_mode: Kven kan registrera seg @@ -336,17 +341,22 @@ nn: usable: Godta at innlegga kan bruka denne emneknaggen lokalt terms_of_service: changelog: Kva er endra? + effective_date: I kraft frå text: Bruksvilkår terms_of_service_generator: admin_email: Epostadresse for juridiske merknader arbitration_address: Fysisk adresse for skilsdomsvarsel arbitration_website: Nettstad for å senda inn skilsdomsvarsel + choice_of_law: Jurisdiksjon dmca_address: Fysisk adresse for opphavsrettsvarsel dmca_email: Epostadresse for opphavsrettsvarsel domain: Domene jurisdiction: Rettskrins min_age: Minstealder user: + date_of_birth_1i: Dag + date_of_birth_2i: Månad + date_of_birth_3i: År role: Rolle time_zone: Tidssone user_role: diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index 5fd28497af..5b18be5376 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -75,6 +75,7 @@ zh-CN: filters: action: 选择在嘟文命中过滤规则时要执行的操作 actions: + blur: 将媒体隐藏在警告之后,且不隐藏文字 hide: 选择在嘟文命中过滤规则时要执行的操作 warn: 显示带有过滤规则标题的警告,并隐藏过滤内容 form_admin_settings: diff --git a/config/locales/sq.yml b/config/locales/sq.yml index dd96ddb88d..01884c10e9 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -319,6 +319,7 @@ sq: create: Krijoni lajmërim title: Lajmërim i ri preview: + disclaimer: Ngaqë përdoruesit s’mund të zgjedhin lënien jashtë tyre, njoftimet me email do të kufizohen te njoftime të rëndësishme, të tilla si cenim të dhënash personale, ose njoftime mbylljesh shërbyesish. explanation_html: 'Email-i do të dërgohet te %{display_count} përdorues. Te email-i do të përfshihet teksti vijues:' title: Bëni paraparje të shënimit për njoftimin publish: Publikoje diff --git a/config/locales/tr.yml b/config/locales/tr.yml index d943462d0c..751e6ae22f 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -319,6 +319,7 @@ tr: create: Duyuru oluştur title: Yeni duyuru preview: + disclaimer: Kullanıcılar bu bildirimleri almayı iptal edemediği için, e-posta bildirimleri kişisel veri ihlali veya sunucu kapatma bildirimleri gibi önemli duyurularla sınırlandırılmalıdır. explanation_html: 'E-posta, %{display_count} kullanıcıya gönderilecektir. E-posta içerisinde aşağıdaki metin yer alacaktır:' title: Duyuru bildiriminin önizlemesine bak publish: Yayınla diff --git a/config/locales/uk.yml b/config/locales/uk.yml index 788c84fb27..0256773d32 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -325,6 +325,7 @@ uk: create: Створити оголошення title: Нове оголошення preview: + disclaimer: Оскільки користувачі не можуть відмовитися від них, сповіщення по електронній пошті повинні обмежуватися важливими оголошеннями, такими як порушення особистих даних або повідомлення про закриття серверу. explanation_html: 'Електронний лист буде надіслано %{display_count} користувачам. До електронного листа буде включено такий текст:' title: Попередній перегляд сповіщення publish: Опублікувати diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 25f423d37a..a255973a10 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -316,6 +316,7 @@ vi: create: Tạo thông báo title: Tạo thông báo mới preview: + disclaimer: Vì người dùng không thể chọn không nhận thông báo qua email nên thông báo qua email chỉ nên giới hạn ở những thông báo quan trọng như thông báo vi phạm dữ liệu cá nhân hoặc thông báo đóng máy chủ. explanation_html: 'Gửi email tới %{display_count} thành viên. Nội dung sau đây sẽ được đưa vào email:' title: Xem trước thông báo sẽ gửi publish: Đăng diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 5542975143..ec02a126a9 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -316,6 +316,7 @@ zh-CN: create: 创建公告 title: 新公告 preview: + disclaimer: 由于用户无法选择退出,电子邮件通知应仅限于重要公告,例如个人数据泄露或服务器关闭通知。 explanation_html: 此电子邮件将发送给 %{display_count} 用户。电子邮件将包含以下文本: title: 预览公告通知 publish: 发布 @@ -471,6 +472,31 @@ zh-CN: new: title: 导入域名列表 no_file: 没有选择文件 + fasp: + debug: + callbacks: + created_at: 创建于 + delete: 刪除 + ip: IP 地址 + request_body: 请求正文 + title: 调试回调 + providers: + active: 有效 + base_url: 基础 URL + callback: 回调 + delete: 刪除 + edit: 编辑提供商 + finish_registration: 完成注册 + name: 名称 + providers: 提供商 + public_key_fingerprint: 公钥指纹 + registrations: + confirm: 确认 + reject: 拒绝 + save: 保存 + sign_in: 登录 + status: 状态 + title: FASP follow_recommendations: description_html: "“关注推荐”可帮助新用户快速找到有趣的内容。 当用户与他人的互动不足以形成个性化的建议时,就会推荐关注这些账号。推荐会每日更新,基于选定语言的近期最高互动数和最多本站关注者数综合评估得出。" language: 选择语言 @@ -929,6 +955,7 @@ zh-CN: chance_to_review_html: "服务条款生成后不会自动发布。你可以审核生成的草稿,填写必要的信息后继续操作。" explanation_html: 此服务条款模板仅供参考,不构成法律意见。如有任何法律问题,请咨询法律顾问。 title: 设置服务条款 + going_live_on_html: 目前条款,自 %{date} 生效 history: 历史记录 live: 生效中 no_history: 尚无服务条款变更记录。 @@ -1901,6 +1928,7 @@ zh-CN: terms_of_service_changed: agreement: 继续使用你在 %{domain} 的账号即表示您同意这些条款。如果你不同意更新后的条款,你可以随时删除账号以终止与 %{domain} 的协议。 changelog: 本次更新的要点如下: + description: 你收到此邮件是因为我们更新了 %{domain} 的服务条款。这些更新将于 %{date} 生效。我们建议你在此查看变更后的服务条款: sign_off: "%{domain} 运营团队" subject: 服务条款变更 subtitle: "%{domain} 更新了服务条款" diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 029aa33214..fae08b3216 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -316,6 +316,7 @@ zh-TW: create: 新增公告 title: 新增公告 preview: + disclaimer: 由於使用者無法選擇退出,電子郵件通知應僅限於重要公告,例如個人資料洩露或伺服器關閉通知。 explanation_html: 此 email 將寄至 %{display_count} 名使用者。以下文字將被包含於 e-mail 中: title: 預覽公告通知 publish: 發布 diff --git a/config/routes/api.rb b/config/routes/api.rb index c7be8c8e45..8fb8f5d0af 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -190,6 +190,7 @@ namespace :api, format: false do resources :lists, only: :index resources :identity_proofs, only: :index resources :featured_tags, only: :index + resources :endorsements, only: :index end member do @@ -203,8 +204,10 @@ namespace :api, format: false do end scope module: :accounts do - resource :pin, only: :create - post :unpin, to: 'pins#destroy' + post :pin, to: 'endorsements#create' + post :endorse, to: 'endorsements#create' + post :unpin, to: 'endorsements#destroy' + post :unendorse, to: 'endorsements#destroy' resource :note, only: :create end end diff --git a/db/migrate/20250411094808_create_quotes.rb b/db/migrate/20250411094808_create_quotes.rb new file mode 100644 index 0000000000..8c830665ac --- /dev/null +++ b/db/migrate/20250411094808_create_quotes.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateQuotes < ActiveRecord::Migration[8.0] + def change + create_table :quotes do |t| + t.belongs_to :account, foreign_key: { on_delete: :cascade }, index: false, null: false + t.belongs_to :status, foreign_key: { on_delete: :cascade }, index: { unique: true }, null: false + t.belongs_to :quoted_status, foreign_key: { to_table: :statuses, on_delete: :nullify }, null: true + t.belongs_to :quoted_account, foreign_key: { to_table: :accounts, on_delete: :nullify }, null: true + t.integer :state, null: false, default: 0 + t.string :approval_uri, index: { where: 'approval_uri IS NOT NULL' } + t.string :activity_uri, index: { unique: true, where: 'activity_uri IS NOT NULL' } + + t.timestamps + end + + # Can be used in the future to e.g. bulk-reject quotes from blocked accounts + add_index :quotes, [:account_id, :quoted_account_id] + end +end diff --git a/db/migrate/20250411095859_add_quote_id_to_status_edit.rb b/db/migrate/20250411095859_add_quote_id_to_status_edit.rb new file mode 100644 index 0000000000..f5bb2f812a --- /dev/null +++ b/db/migrate/20250411095859_add_quote_id_to_status_edit.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddQuoteIdToStatusEdit < ActiveRecord::Migration[8.0] + def change + add_column :status_edits, :quote_id, :bigint, null: true + end +end diff --git a/db/schema.rb b/db/schema.rb index b09360ff43..6b1aa81bd0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do +ActiveRecord::Schema[8.0].define(version: 2025_04_11_095859) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -871,6 +871,24 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do t.string "url" end + create_table "quotes", force: :cascade do |t| + t.bigint "account_id", null: false + t.bigint "status_id", null: false + t.bigint "quoted_status_id" + t.bigint "quoted_account_id" + t.integer "state", default: 0, null: false + t.string "approval_uri" + t.string "activity_uri" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["account_id", "quoted_account_id"], name: "index_quotes_on_account_id_and_quoted_account_id" + t.index ["activity_uri"], name: "index_quotes_on_activity_uri", unique: true, where: "(activity_uri IS NOT NULL)" + t.index ["approval_uri"], name: "index_quotes_on_approval_uri", where: "(approval_uri IS NOT NULL)" + t.index ["quoted_account_id"], name: "index_quotes_on_quoted_account_id" + t.index ["quoted_status_id"], name: "index_quotes_on_quoted_status_id" + t.index ["status_id"], name: "index_quotes_on_status_id", unique: true + end + create_table "relationship_severance_events", force: :cascade do |t| t.integer "type", null: false t.string "target_name", null: false @@ -1007,6 +1025,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do t.text "media_descriptions", array: true t.string "poll_options", array: true t.boolean "sensitive" + t.bigint "quote_id" t.index ["account_id"], name: "index_status_edits_on_account_id" t.index ["status_id"], name: "index_status_edits_on_status_id" end @@ -1350,6 +1369,10 @@ ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do add_foreign_key "polls", "statuses", on_delete: :cascade add_foreign_key "preview_card_trends", "preview_cards", on_delete: :cascade add_foreign_key "preview_cards", "accounts", column: "author_account_id", on_delete: :nullify + add_foreign_key "quotes", "accounts", column: "quoted_account_id", on_delete: :nullify + add_foreign_key "quotes", "accounts", on_delete: :cascade + add_foreign_key "quotes", "statuses", column: "quoted_status_id", on_delete: :nullify + add_foreign_key "quotes", "statuses", on_delete: :cascade add_foreign_key "report_notes", "accounts", on_delete: :cascade add_foreign_key "report_notes", "reports", on_delete: :cascade add_foreign_key "reports", "accounts", column: "action_taken_by_account_id", name: "fk_bca45b75fd", on_delete: :nullify diff --git a/lib/mastodon/cli/search.rb b/lib/mastodon/cli/search.rb index 3a73c9c047..e291801f1b 100644 --- a/lib/mastodon/cli/search.rb +++ b/lib/mastodon/cli/search.rb @@ -20,6 +20,7 @@ module Mastodon::CLI option :import, type: :boolean, default: true, desc: 'Import data from the database to the index' option :clean, type: :boolean, default: true, desc: 'Remove outdated documents from the index' option :reset_chewy, type: :boolean, default: false, desc: "Reset Chewy's internal index" + option :only_mapping, type: :boolean, default: false, desc: 'Update the index specification without re-index' desc 'deploy', 'Create or upgrade Elasticsearch indices and populate them' long_desc <<~LONG_DESC If Elasticsearch is empty, this command will create the necessary indices @@ -52,6 +53,20 @@ module Mastodon::CLI Chewy::Stash::Specification.reset! if options[:reset_chewy] + if options[:only_mapping] + indices.select { |index| index.specification.changed? }.each do |index| + progress.title = "Updating mapping for #{index} " + index.update_mapping + index.specification.lock! + end + + progress.title = 'Done! ' + progress.finish + + say('Updated index mappings', :green, true) + return + end + # First, ensure all indices are created and have the correct # structure, so that live data can already be written indices.select { |index| index.specification.changed? }.each do |index| diff --git a/package.json b/package.json index cd4eb712d7..b0a5382aee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/mastodon", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.8.1", + "packageManager": "yarn@4.9.1", "engines": { "node": ">=18" }, diff --git a/spec/fabricators/quote_fabricator.rb b/spec/fabricators/quote_fabricator.rb new file mode 100644 index 0000000000..c420d2720c --- /dev/null +++ b/spec/fabricators/quote_fabricator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Fabricator(:quote) do + status { Fabricate.build(:status) } + quoted_status { Fabricate.build(:status) } + state :pending +end diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 623a21ab53..43cbc8a93b 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -7,7 +7,15 @@ RSpec.describe ActivityPub::Activity::Create do let(:json) do { - '@context': 'https://www.w3.org/ns/activitystreams', + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + quote: { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + }, + ], id: [ActivityPub::TagManager.instance.uri_for(sender), '#foo'].join, type: 'Create', actor: ActivityPub::TagManager.instance.uri_for(sender), @@ -879,6 +887,114 @@ RSpec.describe ActivityPub::Activity::Create do end end + context 'with an unverifiable quote of a known post', feature: :inbound_quotes do + let(:quoted_status) { Fabricate(:status) } + + let(:object_json) do + build_object( + type: 'Note', + content: 'woah what she said is amazing', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status) + ) + end + + it 'creates a status with an unverified quote' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + expect(status).to_not be_nil + expect(status.quote).to_not be_nil + expect(status.quote).to have_attributes( + state: 'pending', + approval_uri: nil + ) + end + end + + context 'with an unverifiable unknown post', feature: :inbound_quotes do + let(:unknown_post_uri) { 'https://unavailable.example.com/unavailable-post' } + + let(:object_json) do + build_object( + type: 'Note', + content: 'woah what she said is amazing', + quote: unknown_post_uri + ) + end + + before do + stub_request(:get, unknown_post_uri).to_return(status: 404) + end + + it 'creates a status with an unverified quote' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + expect(status).to_not be_nil + expect(status.quote).to_not be_nil + expect(status.quote).to have_attributes( + state: 'pending', + approval_uri: nil + ) + end + end + + context 'with a verifiable quote of a known post', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:approval_uri) { 'https://quoted.example.com/quote-approval' } + + let(:object_json) do + build_object( + type: 'Note', + content: 'woah what she said is amazing', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + quoteAuthorization: approval_uri + ) + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: 'QuoteAuthorization', + id: approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), + interactingObject: object_json[:id], + interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), + })) + end + + it 'creates a status with a verified quote' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + expect(status).to_not be_nil + expect(status.quote).to_not be_nil + expect(status.quote).to have_attributes( + state: 'accepted', + approval_uri: approval_uri + ) + end + end + context 'when a vote to a local poll' do let(:poll) { Fabricate(:poll, options: %w(Yellow Blue)) } let!(:local_status) { Fabricate(:status, poll: poll) } diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index 71977a96a2..849c7ada90 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -77,4 +77,61 @@ RSpec.describe ActivityPub::Activity::Delete do end end end + + context 'when the deleted object is an account' do + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Delete', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: ActivityPub::TagManager.instance.uri_for(sender), + signature: 'foo', + }.with_indifferent_access + end + + describe '#perform' do + subject { described_class.new(json, sender) } + + let(:service) { instance_double(DeleteAccountService, call: true) } + + before do + allow(DeleteAccountService).to receive(:new).and_return(service) + end + + it 'calls the account deletion service' do + subject.perform + + expect(service) + .to have_received(:call).with(sender, { reserve_username: false, skip_activitypub: true }) + end + end + end + + context 'when the deleted object is a quote authorization' do + let(:quoter) { Fabricate(:account, domain: 'b.example.com') } + let(:status) { Fabricate(:status, account: quoter) } + let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') } + let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) } + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Delete', + actor: ActivityPub::TagManager.instance.uri_for(sender), + object: quote.approval_uri, + signature: 'foo', + }.with_indifferent_access + end + + describe '#perform' do + subject { described_class.new(json, sender) } + + it 'revokes the authorization' do + expect { subject.perform } + .to change { quote.reload.state }.to('revoked') + end + end + end end diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index 958e2f62d7..a0a82e3923 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -40,10 +40,119 @@ RSpec.describe StatusCacheHydrator do end end + context 'when handling an unapproved quote' do + let(:quoted_status) { Fabricate(:status) } + + before do + Fabricate(:quote, status: status, quoted_status: quoted_status, state: :pending) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + + context 'when handling an approved quote' do + let(:quoted_status) { Fabricate(:status) } + + before do + Fabricate(:quote, status: status, quoted_status: quoted_status, state: :accepted) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + + context 'when the quoted post has been favourited' do + before do + FavouriteService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + + context 'when the quoted post has been reblogged' do + before do + ReblogService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + + context 'when the quoted post matches account filters' do + let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') } + + before do + account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) + end + + it 'renders the same attributes as a full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:quote]).to_not be_nil + end + end + end + context 'when handling a reblog' do let(:reblog) { Fabricate(:status) } let(:status) { Fabricate(:status, reblog: reblog) } + context 'when the reblog has an approved quote' do + let(:quoted_status) { Fabricate(:status) } + + before do + Fabricate(:quote, status: reblog, quoted_status: quoted_status, state: :accepted) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + + context 'when the quoted post has been favourited' do + before do + FavouriteService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + end + + context 'when the quoted post has been reblogged' do + before do + ReblogService.new.call(account, quoted_status) + end + + it 'renders the same attributes as full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + end + + context 'when the quoted post matches account filters' do + let(:quoted_status) { Fabricate(:status, text: 'this toot is about that banned word') } + + before do + account.custom_filters.create!(phrase: 'filter1', context: %w(home), action: :hide, keywords_attributes: [{ keyword: 'banned' }, { keyword: 'irrelevant' }]) + end + + it 'renders the same attributes as a full render' do + expect(subject).to eql(compare_to_hash) + expect(subject[:reblog][:quote]).to_not be_nil + end + end + end + context 'when it has been favourited' do before do FavouriteService.new.call(account, reblog) diff --git a/spec/requests/api/v1/accounts/pins_spec.rb b/spec/requests/api/v1/accounts/endorsements_spec.rb similarity index 57% rename from spec/requests/api/v1/accounts/pins_spec.rb rename to spec/requests/api/v1/accounts/endorsements_spec.rb index 8ebcb27d28..6e0996a1f1 100644 --- a/spec/requests/api/v1/accounts/pins_spec.rb +++ b/spec/requests/api/v1/accounts/endorsements_spec.rb @@ -13,8 +13,30 @@ RSpec.describe 'Accounts Pins API' do kevin.account.followers << user.account end - describe 'POST /api/v1/accounts/:account_id/pin' do - subject { post "/api/v1/accounts/#{kevin.account.id}/pin", headers: headers } + describe 'GET /api/v1/accounts/:account_id/endorsements' do + subject { get "/api/v1/accounts/#{user.account.id}/endorsements", headers: headers } + + let(:scopes) { 'read:accounts' } + + before do + user.account.endorsed_accounts << kevin.account + end + + it 'returns the expected accounts', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body) + .to contain_exactly( + hash_including(id: kevin.account_id.to_s) + ) + end + end + + describe 'POST /api/v1/accounts/:account_id/endorse' do + subject { post "/api/v1/accounts/#{kevin.account.id}/endorse", headers: headers } it 'creates account_pin', :aggregate_failures do expect do @@ -26,8 +48,8 @@ RSpec.describe 'Accounts Pins API' do end end - describe 'POST /api/v1/accounts/:account_id/unpin' do - subject { post "/api/v1/accounts/#{kevin.account.id}/unpin", headers: headers } + describe 'POST /api/v1/accounts/:account_id/unendorse' do + subject { post "/api/v1/accounts/#{kevin.account.id}/unendorse", headers: headers } before do Fabricate(:account_pin, account: user.account, target_account: kevin.account) diff --git a/spec/requests/api/v1/lists_spec.rb b/spec/requests/api/v1/lists_spec.rb index 20f27a7431..226632c5ac 100644 --- a/spec/requests/api/v1/lists_spec.rb +++ b/spec/requests/api/v1/lists_spec.rb @@ -132,9 +132,12 @@ RSpec.describe 'Lists' do it 'returns http unprocessable entity' do subject - expect(response).to have_http_status(422) + expect(response) + .to have_http_status(422) expect(response.content_type) .to start_with('application/json') + expect(response.parsed_body) + .to include(error: /Replies policy is not included/) end end end diff --git a/spec/requests/api/v2/filters_spec.rb b/spec/requests/api/v2/filters_spec.rb index 3b5c44cefa..304afc7bd8 100644 --- a/spec/requests/api/v2/filters_spec.rb +++ b/spec/requests/api/v2/filters_spec.rb @@ -115,6 +115,21 @@ RSpec.describe 'Filters' do .to start_with('application/json') end end + + context 'when the given filter_action value is invalid' do + let(:params) { { title: 'magic', filter_action: 'imaginary_value', keywords_attributes: [keyword: 'magic'] } } + + it 'returns http unprocessable entity' do + subject + + expect(response) + .to have_http_status(422) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body) + .to include(error: /Action is not included/) + end + end end describe 'GET /api/v2/filters/:id' do diff --git a/spec/serializers/rest/quote_serializer_spec.rb b/spec/serializers/rest/quote_serializer_spec.rb new file mode 100644 index 0000000000..999bd6d6b1 --- /dev/null +++ b/spec/serializers/rest/quote_serializer_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::QuoteSerializer do + subject do + serialized_record_json( + quote, + described_class, + options: { + scope: current_user, + scope_name: :current_user, + } + ) + end + + let(:current_user) { Fabricate(:user) } + let(:quote) { Fabricate(:quote) } + + context 'with a pending quote' do + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status: nil, + state: 'pending' + ) + end + end + + context 'with an accepted quote' do + let(:quote) { Fabricate(:quote, state: :accepted) } + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status: be_a(Hash), + state: 'accepted' + ) + end + end + + context 'with an accepted quote of a deleted post' do + let(:quote) { Fabricate(:quote, state: :accepted) } + + before do + quote.quoted_status.destroy! + quote.reload + end + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status: nil, + state: 'deleted' + ) + end + end + + context 'with an accepted quote of a blocked user' do + let(:quote) { Fabricate(:quote, state: :accepted) } + + before do + quote.quoted_account.block!(current_user.account) + end + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status: nil, + state: 'unauthorized' + ) + end + end + + context 'with a recursive accepted quote' do + let(:status) { Fabricate(:status) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: status, state: :accepted) } + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status: be_a(Hash), + state: 'accepted' + ) + end + end +end diff --git a/spec/serializers/rest/shallow_quote_serializer_spec.rb b/spec/serializers/rest/shallow_quote_serializer_spec.rb new file mode 100644 index 0000000000..32acd5f5d1 --- /dev/null +++ b/spec/serializers/rest/shallow_quote_serializer_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe REST::ShallowQuoteSerializer do + subject do + serialized_record_json( + quote, + described_class, + options: { + scope: current_user, + scope_name: :current_user, + } + ) + end + + let(:current_user) { Fabricate(:user) } + let(:quote) { Fabricate(:quote) } + + context 'with a pending quote' do + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status_id: nil, + state: 'pending' + ) + expect(subject.deep_symbolize_keys) + .to_not have_key(:quoted_status) + end + end + + context 'with an accepted quote' do + let(:quote) { Fabricate(:quote, state: :accepted) } + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status_id: be_a(String), + state: 'accepted' + ) + expect(subject.deep_symbolize_keys) + .to_not have_key(:quoted_status) + end + end + + context 'with an accepted quote of a deleted post' do + let(:quote) { Fabricate(:quote, state: :accepted) } + + before do + quote.quoted_status.destroy! + quote.reload + end + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status_id: nil, + state: 'deleted' + ) + end + end + + context 'with an accepted quote of a blocked user' do + let(:quote) { Fabricate(:quote, state: :accepted) } + + before do + quote.quoted_account.block!(current_user.account) + end + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status_id: nil, + state: 'unauthorized' + ) + end + end + + context 'with a recursive accepted quote' do + let(:status) { Fabricate(:status) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: status, state: :accepted) } + + it 'returns expected values' do + expect(subject.deep_symbolize_keys) + .to include( + quoted_status_id: be_a(String), + state: 'accepted' + ) + expect(subject.deep_symbolize_keys) + .to_not have_key(:quoted_status) + end + end +end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 28b7653833..ed04a19fb3 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe ActivityPub::ProcessStatusUpdateService do subject { described_class.new } - let!(:status) { Fabricate(:status, text: 'Hello world', account: Fabricate(:account, domain: 'example.com')) } + let!(:status) { Fabricate(:status, text: 'Hello world', uri: 'https://example.com/statuses/1234', account: Fabricate(:account, domain: 'example.com')) } let(:bogus_mention) { 'https://example.com/users/erroringuser' } let(:payload) do { @@ -435,6 +435,394 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end end + context 'when the status has an existing unverified quote and adds an approval link', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: nil) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + quoteAuthorization: approval_uri, + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: 'QuoteAuthorization', + id: approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), + interactingObject: ActivityPub::TagManager.instance.uri_for(status), + interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), + })) + end + + it 'updates the approval URI and verifies the quote' do + expect { subject.call(status, json, json) } + .to change(quote, :approval_uri).to(approval_uri) + .and change(quote, :state).to('accepted') + end + end + + context 'when the status has an existing verified quote and removes an approval link', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + } + end + + it 'removes the approval URI and unverifies the quote' do + expect { subject.call(status, json, json) } + .to change(quote, :approval_uri).to(nil) + .and change(quote, :state).to('pending') + end + end + + context 'when the status adds a verifiable quote', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + quoteAuthorization: approval_uri, + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: 'QuoteAuthorization', + id: approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), + interactingObject: ActivityPub::TagManager.instance.uri_for(status), + interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), + })) + end + + it 'updates the approval URI and verifies the quote' do + expect { subject.call(status, json, json) } + .to change(status, :quote).from(nil) + expect(status.quote.approval_uri).to eq approval_uri + expect(status.quote.state).to eq 'accepted' + end + end + + context 'when the status adds a unverifiable quote', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(quoted_status), + } + end + + it 'updates the approval URI but does not verify the quote' do + expect { subject.call(status, json, json) } + .to change(status, :quote).from(nil) + expect(status.quote.approval_uri).to be_nil + expect(status.quote.state).to eq 'pending' + end + end + + context 'when the status removes a verified quote', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + } + end + + it 'removes the quote' do + expect { subject.call(status, json, json) } + .to change { status.reload.quote }.to(nil) + + expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when the status removes an unverified quote', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: nil, state: :pending) } + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + } + end + + it 'removes the quote' do + expect { subject.call(status, json, json) } + .to change { status.reload.quote }.to(nil) + + expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when the status swaps a verified quote with an unverifiable quote', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:second_quoted_status) { Fabricate(:status, account: quoted_account) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(second_quoted_status), + quoteAuthorization: approval_uri, + } + end + + before do + stub_request(:get, approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: 'QuoteAuthorization', + id: approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account), + interactingObject: ActivityPub::TagManager.instance.uri_for(status), + interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status), + })) + end + + it 'updates the URI and unverifies the quote' do + expect { subject.call(status, json, json) } + .to change { status.quote.quoted_status }.from(quoted_status).to(second_quoted_status) + .and change { status.quote.state }.from('accepted') + + expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when the status swaps a verified quote with another verifiable quote', feature: :inbound_quotes do + let(:quoted_account) { Fabricate(:account, domain: 'quoted.example.com') } + let(:second_quoted_account) { Fabricate(:account, domain: 'second-quoted.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:second_quoted_status) { Fabricate(:status, account: second_quoted_account) } + let!(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) } + let(:approval_uri) { 'https://quoted.example.com/approvals/1' } + let(:second_approval_uri) { 'https://second-quoted.example.com/approvals/2' } + + let(:payload) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + '@id': 'https://w3id.org/fep/044f#quote', + '@type': '@id', + }, + { + '@id': 'https://w3id.org/fep/044f#quoteAuthorization', + '@type': '@id', + }, + ], + id: 'foo', + type: 'Note', + summary: 'Show more', + content: 'Hello universe', + updated: '2021-09-08T22:39:25Z', + quote: ActivityPub::TagManager.instance.uri_for(second_quoted_status), + quoteAuthorization: second_approval_uri, + } + end + + before do + stub_request(:get, second_approval_uri).to_return(headers: { 'Content-Type': 'application/activity+json' }, body: Oj.dump({ + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: 'QuoteAuthorization', + id: second_approval_uri, + attributedTo: ActivityPub::TagManager.instance.uri_for(second_quoted_status.account), + interactingObject: ActivityPub::TagManager.instance.uri_for(status), + interactionTarget: ActivityPub::TagManager.instance.uri_for(second_quoted_status), + })) + end + + it 'updates the URI and unverifies the quote' do + expect { subject.call(status, json, json) } + .to change { status.quote.quoted_status }.from(quoted_status).to(second_quoted_status) + .and change { status.quote.approval_uri }.from(approval_uri).to(second_approval_uri) + .and(not_change { status.quote.state }) + + expect { quote.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + def poll_option_json(name, votes) { type: 'Note', name: name, replies: { type: 'Collection', totalItems: votes } } end diff --git a/spec/services/activitypub/verify_quote_service_spec.rb b/spec/services/activitypub/verify_quote_service_spec.rb new file mode 100644 index 0000000000..0e5069a46b --- /dev/null +++ b/spec/services/activitypub/verify_quote_service_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::VerifyQuoteService do + subject { described_class.new } + + let(:account) { Fabricate(:account, domain: 'a.example.com') } + let(:quoted_account) { Fabricate(:account, domain: 'b.example.com') } + let(:quoted_status) { Fabricate(:status, account: quoted_account) } + let(:status) { Fabricate(:status, account: account) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri) } + + context 'with an unfetchable approval URI' do + let(:approval_uri) { 'https://b.example.com/approvals/1234' } + + before do + stub_request(:get, approval_uri) + .to_return(status: 404) + end + + context 'with an already-fetched post' do + it 'does not update the status' do + expect { subject.call(quote) } + .to change(quote, :state).to('rejected') + end + end + + context 'with an already-verified quote' do + let(:quote) { Fabricate(:quote, status: status, quoted_status: quoted_status, approval_uri: approval_uri, state: :accepted) } + + it 'rejects the quote' do + expect { subject.call(quote) } + .to change(quote, :state).to('revoked') + end + end + end + + context 'with an approval URI' do + let(:approval_uri) { 'https://b.example.com/approvals/1234' } + + let(:approval_type) { 'QuoteAuthorization' } + let(:approval_id) { approval_uri } + let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(quoted_account) } + let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(status) } + let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(quoted_status) } + + let(:json) do + { + '@context': [ + 'https://www.w3.org/ns/activitystreams', + { + QuoteAuthorization: 'https://w3id.org/fep/044f#QuoteAuthorization', + gts: 'https://gotosocial.org/ns#', + interactionPolicy: { + '@id': 'gts:interactionPolicy', + '@type': '@id', + }, + interactingObject: { + '@id': 'gts:interactingObject', + '@type': '@id', + }, + interactionTarget: { + '@id': 'gts:interactionTarget', + '@type': '@id', + }, + }, + ], + type: approval_type, + id: approval_id, + attributedTo: approval_attributed_to, + interactingObject: approval_interacting_object, + interactionTarget: approval_interaction_target, + }.with_indifferent_access + end + + before do + stub_request(:get, approval_uri) + .to_return(status: 200, body: Oj.dump(json), headers: { 'Content-Type': 'application/activity+json' }) + end + + context 'with a valid activity for already-fetched posts' do + it 'updates the status' do + expect { subject.call(quote) } + .to change(quote, :state).to('accepted') + + expect(a_request(:get, approval_uri)) + .to have_been_made.once + end + end + + context 'with a valid activity for a post that cannot be fetched but is inlined' do + let(:quoted_status) { nil } + + let(:approval_interaction_target) do + { + type: 'Note', + id: 'https://b.example.com/unknown-quoted', + to: 'https://www.w3.org/ns/activitystreams#Public', + attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_account), + content: 'previously unknown post', + } + end + + before do + stub_request(:get, 'https://b.example.com/unknown-quoted') + .to_return(status: 404) + end + + it 'updates the status' do + expect { subject.call(quote, fetchable_quoted_uri: 'https://b.example.com/unknown-quoted') } + .to change(quote, :state).to('accepted') + + expect(a_request(:get, approval_uri)) + .to have_been_made.once + + expect(quote.reload.quoted_status.content).to eq 'previously unknown post' + end + end + + context 'with a valid activity for a post that cannot be fetched and is inlined from an untrusted source' do + let(:quoted_status) { nil } + + let(:approval_interaction_target) do + { + type: 'Note', + id: 'https://example.com/unknown-quoted', + to: 'https://www.w3.org/ns/activitystreams#Public', + attributedTo: ActivityPub::TagManager.instance.uri_for(account), + content: 'previously unknown post', + } + end + + before do + stub_request(:get, 'https://example.com/unknown-quoted') + .to_return(status: 404) + end + + it 'does not update the status' do + expect { subject.call(quote, fetchable_quoted_uri: 'https://example.com/unknown-quoted') } + .to not_change(quote, :state) + .and not_change(quote, :quoted_status) + + expect(a_request(:get, approval_uri)) + .to have_been_made.once + end + end + + context 'with a valid activity for already-fetched posts, with a pre-fetched approval' do + it 'updates the status without fetching the activity' do + expect { subject.call(quote, prefetched_body: Oj.dump(json)) } + .to change(quote, :state).to('accepted') + + expect(a_request(:get, approval_uri)) + .to_not have_been_made + end + end + + context 'with an unverifiable approval' do + let(:approval_uri) { 'https://evil.com/approvals/1234' } + + it 'does not update the status' do + expect { subject.call(quote) } + .to_not change(quote, :state) + end + end + + context 'with an invalid approval document because of a mismatched ID' do + let(:approval_id) { 'https://evil.com/approvals/1234' } + + it 'does not accept the quote' do + # NOTE: maybe we want to skip that instead of rejecting it? + expect { subject.call(quote) } + .to change(quote, :state).to('rejected') + end + end + + context 'with an approval from the wrong account' do + let(:approval_attributed_to) { ActivityPub::TagManager.instance.uri_for(Fabricate(:account, domain: 'b.example.com')) } + + it 'does not update the status' do + expect { subject.call(quote) } + .to_not change(quote, :state) + end + end + + context 'with an approval for the wrong quoted post' do + let(:approval_interaction_target) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: quoted_account)) } + + it 'does not update the status' do + expect { subject.call(quote) } + .to_not change(quote, :state) + end + end + + context 'with an approval for the wrong quote post' do + let(:approval_interacting_object) { ActivityPub::TagManager.instance.uri_for(Fabricate(:status, account: account)) } + + it 'does not update the status' do + expect { subject.call(quote) } + .to_not change(quote, :state) + end + end + + context 'with an approval of the wrong type' do + let(:approval_type) { 'ReplyAuthorization' } + + it 'does not update the status' do + expect { subject.call(quote) } + .to_not change(quote, :state) + end + end + end + + context 'with fast-track authorizations' do + let(:approval_uri) { nil } + + context 'without any fast-track condition' do + it 'does not update the status' do + expect { subject.call(quote) } + .to_not change(quote, :state) + end + end + + context 'when the account and the quoted account are the same' do + let(:quoted_account) { account } + + it 'updates the status' do + expect { subject.call(quote) } + .to change(quote, :state).to('accepted') + end + end + + context 'when the account is mentioned by the quoted post' do + before do + quoted_status.mentions << Mention.new(account: account) + end + + it 'updates the status' do + expect { subject.call(quote) } + .to change(quote, :state).to('accepted') + end + end + end +end diff --git a/spec/workers/activitypub/quote_refresh_worker_spec.rb b/spec/workers/activitypub/quote_refresh_worker_spec.rb new file mode 100644 index 0000000000..bcdcc0b746 --- /dev/null +++ b/spec/workers/activitypub/quote_refresh_worker_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::QuoteRefreshWorker do + let(:worker) { described_class.new } + let(:service) { instance_double(ActivityPub::VerifyQuoteService, call: true) } + + describe '#perform' do + before { stub_service } + + let(:account) { Fabricate(:account, domain: 'example.com') } + let(:status) { Fabricate(:status, account: account) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: nil, updated_at: updated_at) } + + context 'when dealing with an old quote' do + let(:updated_at) { (Quote::BACKGROUND_REFRESH_INTERVAL * 2).ago } + + it 'sends the status to the service and bumps the updated date' do + expect { worker.perform(quote.id) } + .to(change { quote.reload.updated_at }) + + expect(service).to have_received(:call).with(quote) + end + end + + context 'when dealing with a recent quote' do + let(:updated_at) { Time.now.utc } + + it 'does not call the service and does not touch the quote' do + expect { worker.perform(quote.id) } + .to_not(change { quote.reload.updated_at }) + + expect(service).to_not have_received(:call).with(quote) + end + end + end + + def stub_service + allow(ActivityPub::VerifyQuoteService) + .to receive(:new) + .and_return(service) + end +end diff --git a/spec/workers/activitypub/refetch_and_verify_quote_worker_spec.rb b/spec/workers/activitypub/refetch_and_verify_quote_worker_spec.rb new file mode 100644 index 0000000000..a925709885 --- /dev/null +++ b/spec/workers/activitypub/refetch_and_verify_quote_worker_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::RefetchAndVerifyQuoteWorker do + let(:worker) { described_class.new } + let(:service) { instance_double(ActivityPub::VerifyQuoteService, call: true) } + + describe '#perform' do + before { stub_service } + + let(:account) { Fabricate(:account, domain: 'example.com') } + let(:status) { Fabricate(:status, account: account) } + let(:quote) { Fabricate(:quote, status: status, quoted_status: nil) } + let(:url) { 'https://example.com/quoted-status' } + + it 'sends the status to the service' do + worker.perform(quote.id, url) + + expect(service).to have_received(:call).with(quote, fetchable_quoted_uri: url, request_id: anything) + end + + it 'returns nil for non-existent record' do + result = worker.perform(123_123_123, url) + + expect(result).to be(true) + end + end + + def stub_service + allow(ActivityPub::VerifyQuoteService) + .to receive(:new) + .and_return(service) + end +end diff --git a/streaming/package.json b/streaming/package.json index f376b89c3a..fa33b575db 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -1,7 +1,7 @@ { "name": "@mastodon/streaming", "license": "AGPL-3.0-or-later", - "packageManager": "yarn@4.8.1", + "packageManager": "yarn@4.9.1", "engines": { "node": ">=18" }, diff --git a/yarn.lock b/yarn.lock index 1568040692..79ffcc5b9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7274,10 +7274,10 @@ __metadata: languageName: node linkType: hard -"decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3": - version: 10.4.3 - resolution: "decimal.js@npm:10.4.3" - checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee +"decimal.js@npm:^10.4.2, decimal.js@npm:^10.4.3, decimal.js@npm:^10.5.0": + version: 10.5.0 + resolution: "decimal.js@npm:10.5.0" + checksum: 10c0/785c35279df32762143914668df35948920b6c1c259b933e0519a69b7003fc0a5ed2a766b1e1dda02574450c566b21738a45f15e274b47c2ac02072c0d1f3ac3 languageName: node linkType: hard @@ -7678,9 +7678,9 @@ __metadata: linkType: hard "dotenv@npm:^16.0.3": - version: 16.4.7 - resolution: "dotenv@npm:16.4.7" - checksum: 10c0/be9f597e36a8daf834452daa1f4cc30e5375a5968f98f46d89b16b983c567398a330580c88395069a77473943c06b877d1ca25b4afafcdd6d4adb549e8293462 + version: 16.5.0 + resolution: "dotenv@npm:16.5.0" + checksum: 10c0/5bc94c919fbd955bf0ba44d33922a1e93d1078e64a1db5c30faeded1d996e7a83c55332cb8ea4fae5a9ca4d0be44cbceb95c5811e70f9f095298df09d1997dd9 languageName: node linkType: hard @@ -8996,7 +8996,7 @@ __metadata: languageName: node linkType: hard -"form-data@npm:^4.0.0, form-data@npm:^4.0.1": +"form-data@npm:^4.0.0": version: 4.0.1 resolution: "form-data@npm:4.0.1" dependencies: @@ -10092,8 +10092,8 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.6.0 - resolution: "ioredis@npm:5.6.0" + version: 5.6.1 + resolution: "ioredis@npm:5.6.1" dependencies: "@ioredis/commands": "npm:^1.1.1" cluster-key-slot: "npm:^1.1.0" @@ -10104,7 +10104,7 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 10c0/a885e5146640fc448706871290ef424ffa39af561f7ee3cf1590085209a509f85e99082bdaaf3cd32fa66758aea3fc2055d1109648ddca96fac4944bf2092c30 + checksum: 10c0/26ae49cf448e807e454a9bdea5a9dfdcf669e2fdbf2df341900a0fb693c5662fea7e39db3227ce8972d1bda0ba7da9b7410e5163b12d8878a579548d847220ac languageName: node linkType: hard @@ -11373,13 +11373,12 @@ __metadata: linkType: hard "jsdom@npm:^26.0.0": - version: 26.0.0 - resolution: "jsdom@npm:26.0.0" + version: 26.1.0 + resolution: "jsdom@npm:26.1.0" dependencies: cssstyle: "npm:^4.2.1" data-urls: "npm:^5.0.0" - decimal.js: "npm:^10.4.3" - form-data: "npm:^4.0.1" + decimal.js: "npm:^10.5.0" html-encoding-sniffer: "npm:^4.0.0" http-proxy-agent: "npm:^7.0.2" https-proxy-agent: "npm:^7.0.6" @@ -11389,12 +11388,12 @@ __metadata: rrweb-cssom: "npm:^0.8.0" saxes: "npm:^6.0.0" symbol-tree: "npm:^3.2.4" - tough-cookie: "npm:^5.0.0" + tough-cookie: "npm:^5.1.1" w3c-xmlserializer: "npm:^5.0.0" webidl-conversions: "npm:^7.0.0" whatwg-encoding: "npm:^3.1.1" whatwg-mimetype: "npm:^4.0.0" - whatwg-url: "npm:^14.1.0" + whatwg-url: "npm:^14.1.1" ws: "npm:^8.18.0" xml-name-validator: "npm:^5.0.0" peerDependencies: @@ -11402,7 +11401,7 @@ __metadata: peerDependenciesMeta: canvas: optional: true - checksum: 10c0/e48725ba4027edcfc9bca5799eaec72c6561ecffe3675a8ff87fe9c3541ca4ff9f82b4eff5b3d9c527302da0d859b2f60e9364347a5d42b77f5c76c436c569dc + checksum: 10c0/5b14a5bc32ce077a06fb42d1ab95b1191afa5cbbce8859e3b96831c5143becbbcbf0511d4d4934e922d2901443ced2cdc3b734c1cf30b5f73b3e067ce457d0f4 languageName: node linkType: hard @@ -12001,9 +12000,9 @@ __metadata: linkType: hard "marky@npm:^1.2.5": - version: 1.2.5 - resolution: "marky@npm:1.2.5" - checksum: 10c0/ca8a011f287dab1ac3291df720fc32b366c4cd767347b63722966650405ce71ec6566f71d1e22e1768bf6461a7fd689b9038e7df0fcfb62eacf3a5a6dcac249e + version: 1.3.0 + resolution: "marky@npm:1.3.0" + checksum: 10c0/6619cdb132fdc4f7cd3e2bed6eebf81a38e50ff4b426bbfb354db68731e4adfebf35ebfd7c8e5a6e846cbf9b872588c4f76db25782caee8c1529ec9d483bf98b languageName: node linkType: hard @@ -17446,12 +17445,12 @@ __metadata: languageName: node linkType: hard -"tough-cookie@npm:^5.0.0": - version: 5.0.0 - resolution: "tough-cookie@npm:5.0.0" +"tough-cookie@npm:^5.1.1": + version: 5.1.2 + resolution: "tough-cookie@npm:5.1.2" dependencies: tldts: "npm:^6.1.32" - checksum: 10c0/4a69c885bf6f45c5a64e60262af99e8c0d58a33bd3d0ce5da62121eeb9c00996d0128a72df8fc4614cbde59cc8b70aa3e21e4c3c98c2bbde137d7aba7fa00124 + checksum: 10c0/5f95023a47de0f30a902bba951664b359725597d8adeabc66a0b93a931c3af801e1e697dae4b8c21a012056c0ea88bd2bf4dfe66b2adcf8e2f42cd9796fe0626 languageName: node linkType: hard @@ -17480,12 +17479,12 @@ __metadata: languageName: node linkType: hard -"tr46@npm:^5.0.0": - version: 5.0.0 - resolution: "tr46@npm:5.0.0" +"tr46@npm:^5.1.0": + version: 5.1.0 + resolution: "tr46@npm:5.1.0" dependencies: punycode: "npm:^2.3.1" - checksum: 10c0/1521b6e7bbc8adc825c4561480f9fe48eb2276c81335eed9fa610aa4c44a48a3221f78b10e5f18b875769eb3413e30efbf209ed556a17a42aa8d690df44b7bee + checksum: 10c0/d761f7144e0cb296187674ef245c74f761e334d7cf25ca73ef60e4c72c097c75051031c093fa1a2fee04b904977b316716a7915854bcae8fb1a371746513cbe8 languageName: node linkType: hard @@ -18520,13 +18519,13 @@ __metadata: languageName: node linkType: hard -"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.0": - version: 14.1.0 - resolution: "whatwg-url@npm:14.1.0" +"whatwg-url@npm:^14.0.0, whatwg-url@npm:^14.1.1": + version: 14.2.0 + resolution: "whatwg-url@npm:14.2.0" dependencies: - tr46: "npm:^5.0.0" + tr46: "npm:^5.1.0" webidl-conversions: "npm:^7.0.0" - checksum: 10c0/f00104f1c67ce086ba8ffedab529cbbd9aefd8c0a6555320026de7aeff31f91c38680f95818b140a7c9cc657cde3781e567835dda552ddb1e2b8faaba0ac3cb6 + checksum: 10c0/f746fc2f4c906607d09537de1227b13f9494c171141e5427ed7d2c0dd0b6a48b43d8e71abaae57d368d0c06b673fd8ec63550b32ad5ed64990c7b0266c2b4272 languageName: node linkType: hard