Merge commit '9c2d5b534f' into upstream-20250314

This commit is contained in:
KMY 2025-03-14 08:35:27 +09:00
commit 6548462ecb
84 changed files with 1719 additions and 418 deletions

View file

@ -14,7 +14,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
@account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true)
current_user.update(user_params) if user_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
rescue ActiveRecord::RecordInvalid => e
render json: ValidationErrorFormatter.new(e).as_json, status: 422

View file

@ -7,7 +7,7 @@ class Api::V1::Profile::AvatarsController < Api::BaseController
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { avatar: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

View file

@ -7,7 +7,7 @@ class Api::V1::Profile::HeadersController < Api::BaseController
def destroy
@account = current_account
UpdateAccountService.new.call(@account, { header: nil }, raise_error: true)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
end

View file

@ -67,6 +67,8 @@ class Api::V1::StatusesController < Api::BaseController
statuses = [@status] + @context.ancestors + @context.descendants + @context.references
render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
ActivityPub::FetchAllRepliesWorker.perform_async(@status.id) if !current_account.nil? && @status.should_fetch_replies?
end
def create

View file

@ -8,7 +8,7 @@ module Settings
def destroy
if valid_picture?
if UpdateAccountService.new.call(@account, { @picture => nil, "#{@picture}_remote_url" => '' })
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg'), status: 303
else
redirect_to settings_profile_path

View file

@ -8,7 +8,7 @@ class Settings::PrivacyController < Settings::BaseController
def update
if UpdateAccountService.new.call(@account, account_params.except(:settings))
current_user.update!(settings_attributes: account_params[:settings])
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_privacy_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show

View file

@ -9,7 +9,7 @@ class Settings::ProfilesController < Settings::BaseController
def update
if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_profile_path, notice: I18n.t('generic.changes_saved_msg')
else
@account.build_fields

View file

@ -8,7 +8,7 @@ class Settings::VerificationsController < Settings::BaseController
def update
if UpdateAccountService.new.call(@account, account_params)
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
ActivityPub::UpdateDistributionWorker.perform_in(ActivityPub::UpdateDistributionWorker::DEBOUNCE_DELAY, @account.id)
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
else
render :show

View file

@ -163,24 +163,49 @@ module JsonLdHelper
end
end
def fetch_resource(uri, id_is_known, on_behalf_of = nil, request_options: {})
# Fetch the resource given by uri.
# @param uri [String]
# @param id_is_known [Boolean]
# @param on_behalf_of [nil, Account]
# @param raise_on_error [Symbol<:all, :temporary, :none>] See {#fetch_resource_without_id_validation} for possible values
def fetch_resource(uri, id_is_known, on_behalf_of = nil, raise_on_error: :none, request_options: {})
unless id_is_known
json = fetch_resource_without_id_validation(uri, on_behalf_of)
json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error)
return if !json.is_a?(Hash) || unsupported_uri_scheme?(json['id'])
uri = json['id']
end
json = fetch_resource_without_id_validation(uri, on_behalf_of, request_options: request_options)
json = fetch_resource_without_id_validation(uri, on_behalf_of, raise_on_error: raise_on_error, request_options: request_options)
json.present? && json['id'] == uri ? json : nil
end
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_temporary_error = false, request_options: {})
# Fetch the resource given by uri
#
# If an error is raised, it contains the response and can be captured for handling like
#
# begin
# fetch_resource_without_id_validation(uri, nil, true)
# rescue Mastodon::UnexpectedResponseError => e
# e.response
# end
#
# @param uri [String]
# @param on_behalf_of [nil, Account]
# @param raise_on_error [Symbol<:all, :temporary, :none>]
# - +:all+ - raise if response code is not in the 2xx range
# - +:temporary+ - raise if the response code is not an "unsalvageable error" like a 404
# (see {#response_error_unsalvageable} )
# - +:none+ - do not raise, return +nil+
def fetch_resource_without_id_validation(uri, on_behalf_of = nil, raise_on_error: :none, request_options: {})
on_behalf_of ||= Account.representative
build_request(uri, on_behalf_of, options: request_options).perform do |response|
raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || !raise_on_temporary_error
raise Mastodon::UnexpectedResponseError, response if !response_successful?(response) && (
raise_on_error == :all ||
(!response_error_unsalvageable?(response) && raise_on_error == :temporary)
)
body_to_json(response.body_with_limit) if response.code == 200 && valid_activitypub_content_type?(response)
end

View file

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -30,30 +30,31 @@ export const ActionBar = () => {
const dispatch = useDispatch();
const intl = useIntl();
const handleLogoutClick = useCallback(() => {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
}, [dispatch]);
const menu = useMemo(() => {
const handleLogoutClick = () => {
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
};
let menu = [];
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
menu.push({ text: intl.formatMessage(messages.reaction_deck), to: '/reaction_deck' });
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
menu.push({ text: intl.formatMessage(messages.emoji_reactions), to: '/emoji_reactions' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick });
return ([
{ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' },
{ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' },
{ text: intl.formatMessage(messages.pins), to: '/pinned' },
null,
{ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' },
{ text: intl.formatMessage(messages.favourites), to: '/favourites' },
{ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' },
{ text: intl.formatMessage(messages.emoji_reactions), to: '/emoji_reactions' },
{ text: intl.formatMessage(messages.lists), to: '/lists' },
{ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' },
null,
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
{ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' },
{ text: intl.formatMessage(messages.filters), href: '/filters' },
null,
{ text: intl.formatMessage(messages.logout), action: handleLogoutClick },
]);
}, [intl, dispatch]);
return (
<DropdownMenuContainer

View file

@ -562,6 +562,7 @@
"notification.favourite": "Ffafriodd {name} eich postiad",
"notification.favourite.name_and_others_with_link": "Ffafriodd {name} a <a>{count, plural, one {# arall} other {# arall}}</a> eich postiad",
"notification.favourite_pm": "Mae {name} wedi ffefrynnu eich cyfeiriad preifat",
"notification.favourite_pm.name_and_others_with_link": "Mae {name} a <a>{count, plural, one {# arall} other {# arall}}</a> wedi hoffi'ch crybwylliad preifat",
"notification.follow": "Dilynodd {name} chi",
"notification.follow.name_and_others": "Mae {name} a <a>{count, plural, zero {}one {# arall} two {# arall} few {# arall} many {# others} other {# arall}}</a> nawr yn eich dilyn chi",
"notification.follow_request": "Mae {name} wedi gwneud cais i'ch dilyn",
@ -696,6 +697,7 @@
"poll_button.remove_poll": "Tynnu pleidlais",
"privacy.change": "Addasu preifatrwdd y post",
"privacy.direct.long": "Pawb sydd â sôn amdanyn nhw yn y postiad",
"privacy.direct.short": "Crybwylliad preifat",
"privacy.private.long": "Eich dilynwyr yn unig",
"privacy.private.short": "Dilynwyr",
"privacy.public.long": "Unrhyw ar ac oddi ar Mastodon",
@ -870,7 +872,9 @@
"subscribed_languages.target": "Newid ieithoedd tanysgrifio {target}",
"tabs_bar.home": "Cartref",
"tabs_bar.notifications": "Hysbysiadau",
"terms_of_service.effective_as_of": "Yn effeithiol ers {date}",
"terms_of_service.title": "Telerau Gwasanaeth",
"terms_of_service.upcoming_changes_on": "Newidiadau i ddod ar {date}",
"time_remaining.days": "{number, plural, one {# diwrnod} other {# diwrnod}} ar ôl",
"time_remaining.hours": "{number, plural, one {# awr} other {# awr}} ar ôl",
"time_remaining.minutes": "{number, plural, one {# munud} other {# munud}} ar ôl",

View file

@ -153,7 +153,7 @@
"column.domain_blocks": "Blokerede domæner",
"column.edit_list": "Redigér liste",
"column.favourites": "Favoritter",
"column.firehose": "Realtids-strømme",
"column.firehose": "Aktuelt",
"column.follow_requests": "Følgeanmodninger",
"column.home": "Hjem",
"column.list_members": "Håndtér listemedlemmer",

View file

@ -874,7 +874,7 @@
"tabs_bar.notifications": "Notificações",
"terms_of_service.effective_as_of": "Válido a partir de {date}",
"terms_of_service.title": "Termos do serviço",
"terms_of_service.upcoming_changes_on": "Alterações em {date}",
"terms_of_service.upcoming_changes_on": "Próximas aterações em {date}",
"time_remaining.days": "{number, plural, one {# dia restante} other {# dias restantes}}",
"time_remaining.hours": "{number, plural, one {# hora restante} other {# horas restantes}}",
"time_remaining.minutes": "{number, plural, one {# minuto restante} other {# minutos restantes}}",

View file

@ -1,10 +1,10 @@
{
"about.blocks": "Moderirani strežniki",
"about.contact": "Stik:",
"about.disclaimer": "Mastodon je prosto, odprto-kodno programje in blagovna znamka Mastodon gGmbH.",
"about.disclaimer": "Mastodon je prosto, odprtokodno programje in blagovna znamka podjetja Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Razlog ni na voljo",
"about.domain_blocks.preamble": "Mastodon vam splošno omogoča ogled vsebin in interakcijo z uporabniki iz vseh drugih strežnikov v fediverzumu. To so izjeme, opravljene na tem strežniku.",
"about.domain_blocks.silenced.explanation": "V splošnem ne boste videli profilov in vsebin s tega strežnika, če jih eksplicino ne poiščete ali nanje naročite s sledenjem.",
"about.domain_blocks.preamble": "Mastodon vam na splošno omogoča ogled vsebin in interakcijo z uporabniki z vseh drugih strežnikov v fediverzumu. Tu so navedene izjeme, ki jih postavlja ta strežnik.",
"about.domain_blocks.silenced.explanation": "V splošnem ne boste videli profilov in vsebin s tega strežnika, razen če jih izrecno poiščete ali jim začnete slediti.",
"about.domain_blocks.silenced.title": "Omejeno",
"about.domain_blocks.suspended.explanation": "Nobeni podatki s tega strežnika ne bodo obdelani, shranjeni ali izmenjani, zaradi česar je nemogoča kakršna koli interakcija ali komunikacija z uporabniki s tega strežnika.",
"about.domain_blocks.suspended.title": "Suspendiran",
@ -29,11 +29,11 @@
"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 {name}",
"account.featured_tags.title": "Izpostavljeni ključniki osebe {name}",
"account.follow": "Sledi",
"account.follow_back": "Sledi nazaj",
"account.followers": "Sledilci",
"account.followers.empty": "Nihče ne sledi temu uporabniku.",
"account.followers.empty": "Nihče še ne sledi temu uporabniku.",
"account.followers_counter": "{count, plural, one {{counter} sledilec} two {{counter} sledilca} few {{counter} sledilci} other {{counter} sledilcev}}",
"account.following": "Sledim",
"account.following_counter": "{count, plural, one {{counter} sleden} two {{counter} sledena} few {{counter} sledeni} other {{counter} sledenih}}",
@ -45,9 +45,9 @@
"account.languages": "Spremeni naročene jezike",
"account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}",
"account.locked_info": "Stanje zasebnosti računa je nastavljeno na zaklenjeno. Lastnik ročno pregleda, kdo ga lahko spremlja.",
"account.media": "Mediji",
"account.media": "Predstavnosti",
"account.mention": "Omeni @{name}",
"account.moved_to": "{name} nakazuje, da ima zdaj nov račun:",
"account.moved_to": "{name} sporoča, da ima zdaj nov račun:",
"account.mute": "Utišaj @{name}",
"account.mute_notifications_short": "Utišaj obvestila",
"account.mute_short": "Utišaj",
@ -68,14 +68,14 @@
"account.unblock_short": "Odblokiraj",
"account.unendorse": "Ne vključi v profil",
"account.unfollow": "Ne sledi več",
"account.unmute": "Odtišaj @{name}",
"account.unmute": "Povrni glas @{name}",
"account.unmute_notifications_short": "Izklopi utišanje obvestil",
"account.unmute_short": "Odtišaj",
"account_note.placeholder": "Kliknite za dodajanje opombe",
"account.unmute_short": "Povrni glas",
"account_note.placeholder": "Kliknite, da dodate opombo",
"admin.dashboard.daily_retention": "Mera ohranjanja uporabnikov po dnevih od registracije",
"admin.dashboard.monthly_retention": "Mera ohranjanja uporabnikov po mesecih od registracije",
"admin.dashboard.retention.average": "Povprečje",
"admin.dashboard.retention.cohort": "Mesec prijave",
"admin.dashboard.retention.cohort": "Mesec registracije",
"admin.dashboard.retention.cohort_size": "Novi uporabniki",
"admin.impact_report.instance_accounts": "Profili računov, ki bi jih s tem izbrisali",
"admin.impact_report.instance_followers": "Sledilci, ki bi jih izgubili naši uporabniki",
@ -87,44 +87,61 @@
"alert.unexpected.title": "Ojoj!",
"alt_text_badge.title": "Nadomestno besedilo",
"alt_text_modal.add_alt_text": "Dodaj nadomestno besedilo",
"alt_text_modal.add_text_from_image": "Dodaj besedilo iz slike",
"alt_text_modal.cancel": "Prekliči",
"alt_text_modal.change_thumbnail": "Spremeni sličico",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Podaj opis za ljudi s težavami s sluhom ...",
"alt_text_modal.describe_for_people_with_visual_impairments": "Podaj opis za slabovidne ...",
"alt_text_modal.done": "Opravljeno",
"announcement.announcement": "Obvestilo",
"announcement.announcement": "Oznanilo",
"annual_report.summary.archetype.booster": "Lovec na trende",
"annual_report.summary.archetype.lurker": "Tiholazec",
"annual_report.summary.archetype.oracle": "Orakelj",
"annual_report.summary.archetype.pollster": "Anketar",
"annual_report.summary.archetype.replier": "Priljudnež",
"annual_report.summary.followers.followers": "sledilcev",
"annual_report.summary.followers.total": "",
"annual_report.summary.followers.total": "skupaj {count}",
"annual_report.summary.here_it_is": "Tule je povzetek vašega leta {year}:",
"annual_report.summary.highlighted_post.by_favourites": "- najpriljubljenejša objava",
"annual_report.summary.highlighted_post.by_reblogs": "- največkrat izpostavljena objava",
"annual_report.summary.highlighted_post.by_replies": "- objava z največ odgovori",
"annual_report.summary.highlighted_post.possessive": "{name}",
"annual_report.summary.most_used_app.most_used_app": "najpogosteje uporabljena aplikacija",
"annual_report.summary.most_used_hashtag.most_used_hashtag": "največkrat uporabljen ključnik",
"annual_report.summary.most_used_hashtag.none": "Brez",
"annual_report.summary.new_posts.new_posts": "nove objave",
"annual_report.summary.percentile.text": "<topLabel>S tem ste se uvrstili med zgornjih </topLabel><percentage></percentage><bottomLabel> uporabnikov domene {domain}.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "Živi duši ne bomo povedali.",
"annual_report.summary.thanks": "Hvala, ker ste del Mastodona!",
"attachments_list.unprocessed": "(neobdelano)",
"audio.hide": "Skrij zvok",
"block_modal.remote_users_caveat": "Od strežnika {domain} bomo zahtevali, da spoštuje vašo odločitev. Izpolnjevanje zahteve ni zagotovljeno, ker nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.",
"block_modal.remote_users_caveat": "Strežnik {domain} bomo pozvali, naj spoštuje vašo odločitev. Kljub temu pa ni gotovo, da bo strežnik prošnjo upošteval, saj nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.",
"block_modal.show_less": "Pokaži manj",
"block_modal.show_more": "Pokaži več",
"block_modal.they_cant_mention": "Ne morejo vas omenjati ali vam slediti.",
"block_modal.they_cant_see_posts": "Ne vidijo vaših objav, vi pa ne njihovih.",
"block_modal.they_will_know": "Ne morejo videti, da so blokirani.",
"block_modal.title": "Blokiraj uporabnika?",
"block_modal.you_wont_see_mentions": "Objav, ki jih omenjajo, ne boste videli.",
"boost_modal.combo": "Če želite preskočiti to, lahko pritisnete {combo}",
"boost_modal.reblog": "Izpostavi objavo?",
"block_modal.they_cant_mention": "Ne more vas omenjati ali vam slediti.",
"block_modal.they_cant_see_posts": "Ne vidi vaših objav, vi pa ne njegovih.",
"block_modal.they_will_know": "Ne more videti, da je blokiran.",
"block_modal.title": "Blokiram uporabnika?",
"block_modal.you_wont_see_mentions": "Objav, ki ga omenjajo, ne boste videli.",
"boost_modal.combo": "Če želite naslednjič to preskočiti, lahko pritisnete {combo}",
"boost_modal.reblog": "Izpostavim objavo?",
"boost_modal.undo_reblog": "Ali želite preklicati izpostavitev objave?",
"bundle_column_error.copy_stacktrace": "Kopiraj poročilo o napaki",
"bundle_column_error.error.body": "Zahtevane strani ni mogoče upodobiti. Vzrok težave je morda hrošč v naši kodi ali pa nezdružljivost z brskalnikom.",
"bundle_column_error.error.title": "Oh, ne!",
"bundle_column_error.network.body": "Pri poskusu nalaganja te strani je prišlo do napake. Vzrok je lahko začasna težava z vašo internetno povezavo ali s tem strežnikom.",
"bundle_column_error.network.title": "Napaka v omrežju",
"bundle_column_error.network.title": "Omrežna napaka",
"bundle_column_error.retry": "Poskusi znova",
"bundle_column_error.return": "Nazaj domov",
"bundle_column_error.routing.body": "Zahtevane strani ni mogoče najti. Ali ste prepričani, da je naslov URL v naslovni vrstici pravilen?",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "Zapri",
"bundle_modal_error.message": "Med nalaganjem prikaza je prišlo do napake.",
"bundle_modal_error.retry": "Poskusi znova",
"closed_registrations.other_server_instructions": "Ker je Mastodon decentraliziran, lahko ustvarite račun na drugem strežniku in ste še vedno v interakciji s tem.",
"closed_registrations_modal.description": "Odpiranje računa na {domain} trenutno ni možno, upoštevajte pa, da ne potrebujete računa prav na {domain}, da bi uporabljali Mastodon.",
"closed_registrations_modal.description": "Odpiranje računa na domeni {domain} trenutno ni možno, upoštevajte pa, da ne potrebujete računa prav na domeni {domain}, da bi uporabljali Mastodon.",
"closed_registrations_modal.find_another_server": "Najdi drug strežnik",
"closed_registrations_modal.preamble": "Mastodon je decentraliziran, kar pomeni, da ni pomembno, kje ustvarite svoj račun; od koder koli je omogočeno sledenje in interakcija z vsemi s tega strežnika. Strežnik lahko gostite tudi sami!",
"closed_registrations_modal.preamble": "Mastodon je decentraliziran, kar pomeni, da ni pomembno, kje ustvarite svoj račun; od koder koli je mogoče slediti in komunicirati z vsemi s tega strežnika. Strežnik lahko gostite tudi sami!",
"closed_registrations_modal.title": "Registracija v Mastodon",
"column.about": "O programu",
"column.blocks": "Blokirani uporabniki",
@ -137,7 +154,7 @@
"column.edit_list": "Uredi seznam",
"column.favourites": "Priljubljeni",
"column.firehose": "Viri v živo",
"column.follow_requests": "Sledi prošnjam",
"column.follow_requests": "Prošnje za sledenje",
"column.home": "Domov",
"column.list_members": "Upravljaj člane seznama",
"column.lists": "Seznami",
@ -155,25 +172,25 @@
"column_search.cancel": "Prekliči",
"column_subheading.settings": "Nastavitve",
"community.column_settings.local_only": "Samo krajevno",
"community.column_settings.media_only": "Samo mediji",
"community.column_settings.media_only": "Samo predstavnosti",
"community.column_settings.remote_only": "Samo oddaljeno",
"compose.language.change": "Spremeni jezik",
"compose.language.search": "Poišči jezik ...",
"compose.language.search": "Poišči jezike ...",
"compose.published.body": "Objavljeno.",
"compose.published.open": "Odpri",
"compose.saved.body": "Objava shranjena.",
"compose_form.direct_message_warning_learn_more": "Izvej več",
"compose_form.encryption_warning": "Objave na Mastodonu niso šifrirane od kraja do kraja. Prek Mastodona ne delite nobenih občutljivih informacij.",
"compose_form.direct_message_warning_learn_more": "Več o tem",
"compose_form.encryption_warning": "Objave na Mastodonu niso šifrirane od konca do konca. Prek Mastodona ne delite nobenih občutljivih informacij.",
"compose_form.hashtag_warning": "Ta objava ne bo navedena pod nobenim ključnikom, ker ni javna. Samo javne objave lahko iščete s ključniki.",
"compose_form.lock_disclaimer": "Vaš račun ni {locked}. Vsakdo vam lahko sledi in si ogleda objave, ki so namenjene samo sledilcem.",
"compose_form.lock_disclaimer.lock": "zaklenjen",
"compose_form.placeholder": "O čem razmišljate?",
"compose_form.poll.duration": "Trajanje ankete",
"compose_form.poll.multiple": "Več možnosti",
"compose_form.poll.multiple": "Izbira več možnosti",
"compose_form.poll.option_placeholder": "Možnost {number}",
"compose_form.poll.single": "Ena izbira",
"compose_form.poll.switch_to_multiple": "Spremenite anketo, da omogočite več izbir",
"compose_form.poll.switch_to_single": "Spremenite anketo, da omogočite eno izbiro",
"compose_form.poll.single": "Izbira ene možnosti",
"compose_form.poll.switch_to_multiple": "Spremenite anketo, da omogočite izbiro več možnosti",
"compose_form.poll.switch_to_single": "Spremenite anketo, da omogočite izbiro ene možnosti",
"compose_form.poll.type": "Slog",
"compose_form.publish": "Objavi",
"compose_form.publish_form": "Objavi",
@ -191,17 +208,24 @@
"confirmations.delete_list.message": "Ali ste prepričani, da želite trajno izbrisati ta seznam?",
"confirmations.delete_list.title": "Želite izbrisati seznam?",
"confirmations.discard_edit_media.confirm": "Opusti",
"confirmations.discard_edit_media.message": "Imate ne shranjene spremembe za medijski opis ali predogled; jih želite kljub temu opustiti?",
"confirmations.discard_edit_media.message": "Spremenjenega opisa predstavnosti ali predogleda niste shranili. Želite spremembe kljub temu opustiti?",
"confirmations.edit.confirm": "Uredi",
"confirmations.edit.message": "Urejanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?",
"confirmations.edit.title": "Želite prepisati objavo?",
"confirmations.follow_to_list.confirm": "Sledi in dodaj na seznam",
"confirmations.follow_to_list.message": "Osebi {name} morate slediti, preden jo lahko dodate na seznam.",
"confirmations.follow_to_list.title": "Naj sledim uporabniku?",
"confirmations.logout.confirm": "Odjava",
"confirmations.logout.message": "Ali ste prepričani, da se želite odjaviti?",
"confirmations.logout.title": "Se želite odjaviti?",
"confirmations.mute.confirm": "Utišanje",
"confirmations.missing_alt_text.confirm": "Dodaj nadomestno besedilo",
"confirmations.missing_alt_text.message": "Vaša objava vsebuje predstavnosti brez nadomestnega besedila. Če jih dodatno opišete, bodo dostopne večji množici ljudi.",
"confirmations.missing_alt_text.secondary": "Vseeno objavi",
"confirmations.missing_alt_text.title": "Dodam nadomestno besedilo?",
"confirmations.mute.confirm": "Utišaj",
"confirmations.redraft.confirm": "Izbriši in preoblikuj",
"confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati ta status in ga preoblikovati? Vzljubi in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.",
"confirmations.redraft.title": "Želite izbrisati in predelati objavo?",
"confirmations.redraft.message": "Ali ste prepričani, da želite izbrisati to objavo in jo preoblikovati? Izkazi priljubljenosti in izpostavitve bodo izgubljeni, odgovori na izvirno objavo pa bodo osiroteli.",
"confirmations.redraft.title": "Želite izbrisati in preoblikovati objavo?",
"confirmations.reply.confirm": "Odgovori",
"confirmations.reply.message": "Odgovarjanje bo prepisalo sporočilo, ki ga trenutno sestavljate. Ali ste prepričani, da želite nadaljevati?",
"confirmations.reply.title": "Želite prepisati objavo?",
@ -217,7 +241,7 @@
"conversation.with": "Z {names}",
"copy_icon_button.copied": "Kopirano v odložišče",
"copypaste.copied": "Kopirano",
"copypaste.copy_to_clipboard": "Kopiraj na odložišče",
"copypaste.copy_to_clipboard": "Kopiraj v odložišče",
"directory.federated": "Iz znanega fediverzuma",
"directory.local": "Samo iz {domain}",
"directory.new_arrivals": "Novi prišleki",
@ -226,28 +250,34 @@
"disabled_account_banner.text": "Vaš račun {disabledAccount} je trenutno onemogočen.",
"dismissable_banner.community_timeline": "To so najnovejše javne objave oseb, katerih računi gostujejo na {domain}.",
"dismissable_banner.dismiss": "Opusti",
"dismissable_banner.explore_links": "Danes po fediverzumu najbolj odmevajo te novice. Višje na seznamu so novejše vesti bolj raznolikih objaviteljev.",
"dismissable_banner.explore_statuses": "Danes so po fediverzumu pozornost pritegnile te objave. Višje na seznamu so novejše, bolj izpostavljene in bolj priljubljene objave.",
"dismissable_banner.explore_tags": "Danes se po fediverzumu najpogosteje uporabljajo ti ključniki. Višje na seznamu so ključniki, ki jih uporablja več različnih ljudi.",
"dismissable_banner.public_timeline": "To so najnovejše javne objave ljudi s fediverzuma, ki jim sledijo ljudje na domeni {domain}.",
"domain_block_modal.block": "Blokiraj strežnik",
"domain_block_modal.block_account_instead": "Namesto tega blokiraj @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Osebe s tega strežnika se lahko odzivajo na vaše stare objave.",
"domain_block_modal.they_cant_follow": "Nihče s tega strežnika vam ne more slediti.",
"domain_block_modal.they_wont_know": "Ne bodo vedeli, da so blokirani.",
"domain_block_modal.title": "Blokiraj domeno?",
"domain_block_modal.you_will_lose_num_followers": "Izgubili boste {followersCount, plural, one {{followersCountDisplay} sledilca} two {{followersCountDisplay} sledilca} few {{followersCountDisplay} sledilce} other {{followersCountDisplay} sledilcev}} in {followingCount, plural, one {{followingCountDisplay} osebo, ki ji sledite} two {{followingCountDisplay} osebi, ki jima sledite} few {{followingCountDisplay} osebe, ki jim sledite} other {{followingCountDisplay} oseb, ki jim sledite}}.",
"domain_block_modal.you_will_lose_relationships": "Izgubili boste vse sledilce in ljudi, ki jim sledite na tem strežniku.",
"domain_block_modal.you_wont_see_posts": "Objav ali obvestil uporabnikov s tega strežnika ne boste videli.",
"domain_pill.activitypub_lets_connect": "Omogoča vam povezovanje in interakcijo z ljudmi, ki niso samo na Mastodonu, ampak tudi na drugih družabnih platformah.",
"domain_pill.activitypub_like_language": "Protokol ActivityPub je kot jezik, s katerim se Mastodon pogovarja z drugimi družabnimi omrežji.",
"domain_pill.activitypub_lets_connect": "Omogoča vam povezovanje in interakcijo z ljudmi, ki niso samo na Mastodonu, ampak tudi na drugih družbenih platformah.",
"domain_pill.activitypub_like_language": "Protokol ActivityPub je kot jezik, v katerem se Mastodon pogovarja z drugimi družabnimi omrežji.",
"domain_pill.server": "Strežnik",
"domain_pill.their_handle": "Njihova ročica:",
"domain_pill.their_server": "Njihovo digitalno domovanje, kjer bivajo vse njihove objave.",
"domain_pill.their_username": "Njihov edinstveni identifikator na njihovem strežniku. Uporabnike z istim uporabniškim imenom lahko najdete na različnih strežnikih.",
"domain_pill.their_handle": "Njegova/njena ročica:",
"domain_pill.their_server": "Njegovo/njeno digitalno domovanje, kjer bivajo vse njegove/njene objave.",
"domain_pill.their_username": "Njegov/njen edinstveni identifikator na njegovem/njenem strežniku. Uporabnike z istim uporabniškim imenom lahko najdete na različnih strežnikih.",
"domain_pill.username": "Uporabniško ime",
"domain_pill.whats_in_a_handle": "Kaj je v ročici?",
"domain_pill.who_they_are": "Ker ročice povedo, kdo je kdo in kje so, ste lahko z osebami v interakciji prek družabnega spleta <button>platform, ki jih poganja ActivityPub</button>.",
"domain_pill.who_you_are": "Ker ročice povedo, kdo ste in kje ste, ste lahko z osebami v interakciji prek družabnega spleta <button>platform, ki jih poganja ActivityPub</button>.",
"domain_pill.who_they_are": "Ker ročice povedo, kdo je kdo in kje je, lahko komunicirate z ljudmi po vsem spletu družbenih <button>platform, ki jih poganja ActivityPub</button>.",
"domain_pill.who_you_are": "Ker ročice povedo, kdo ste in kje ste, lahko komunicirate z ljudmi po vsem spletu družbenih <button>platform, ki jih poganja ActivityPub</button>.",
"domain_pill.your_handle": "Vaša ročica:",
"domain_pill.your_server": "Vaše digitalno domovanje, kjer bivajo vse vaše objave. Vam ta ni všeč? Prenesite ga med strežniki kadar koli in z njim tudi svoje sledilce.",
"domain_pill.your_server": "Vaše digitalno domovanje, kjer bivajo vse vaše objave. Vam ni všeč? Kadar koli ga prenesite med strežniki in z njim tudi svoje sledilce.",
"domain_pill.your_username": "Vaš edinstveni identifikator na tem strežniku. Uporabnike z istim uporabniškim imenom je možno najti na različnih strežnikih.",
"embed.instructions": "Vstavite to objavo na svojo spletno stran tako, da kopirate spodnjo kodo.",
"embed.preview": "Tako bo izgledalo:",
"embed.preview": "Takole bo videti:",
"emoji_button.activity": "Dejavnost",
"emoji_button.clear": "Počisti",
"emoji_button.custom": "Po meri",
@ -263,32 +293,32 @@
"emoji_button.search_results": "Rezultati iskanja",
"emoji_button.symbols": "Simboli",
"emoji_button.travel": "Potovanja in kraji",
"empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo dal na voljo",
"empty_column.account_hides_collections": "Ta uporabnik se je odločil, da te informacije ne bo delil",
"empty_column.account_suspended": "Račun je suspendiran",
"empty_column.account_timeline": "Tukaj ni objav!",
"empty_column.account_unavailable": "Profil ni na voljo",
"empty_column.blocks": "Niste še blokirali nobenega uporabnika.",
"empty_column.bookmarked_statuses": "Zaenkrat še nimate zaznamovanih objav. Ko objavo zaznamujete, se pojavi tukaj.",
"empty_column.community": "Krajevna časovnica je prazna. Napišite nekaj javnega, da se bo snežna kepa zakotalila!",
"empty_column.community": "Krajevna časovnica je prazna. Napišite nekaj javnega, da se začne polniti!",
"empty_column.direct": "Nimate še nobenih zasebnih omemb. Ko jih boste poslali ali prejeli, se bodo prikazale tukaj.",
"empty_column.domain_blocks": "Zaenkrat ni blokiranih domen.",
"empty_column.explore_statuses": "Trenutno ni nič v trendu. Preverite znova kasneje!",
"empty_column.favourited_statuses": "Nimate priljubljenih objav. Ko boste vzljubili kakšno, bo prikazana tukaj.",
"empty_column.favourites": "Nihče še ni vzljubil te objave. Ko jo bo nekdo, se bo pojavila tukaj.",
"empty_column.explore_statuses": "Trenutno ni novih trendov. Preverite znova kasneje!",
"empty_column.favourited_statuses": "Nimate priljubljenih objav. Ko boste kakšno dodali med priljubljene, bo prikazana tukaj.",
"empty_column.favourites": "Nihče še ni vzljubil te objave. Ko jo bo nekdo, bo naveden tukaj.",
"empty_column.follow_requests": "Nimate prošenj za sledenje. Ko boste prejeli kakšno, se bo prikazala tukaj.",
"empty_column.followed_tags": "Zaenkrat ne sledite še nobenemu ključniku. Ko boste, se bodo pojavili tukaj.",
"empty_column.hashtag": "V tem ključniku še ni nič.",
"empty_column.home": "Vaša domača časovnica je prazna! Sledite več osebam, da jo zapolnite. {suggestions}",
"empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama objavili nove statuse, se bodo pojavili tukaj.",
"empty_column.followed_tags": "Zaenkrat ne sledite še nobenemu ključniku. Ko boste, se bo pojavil tukaj.",
"empty_column.hashtag": "V tem ključniku ni še nič.",
"empty_column.home": "Vaša domača časovnica je prazna! Sledite več osebam, da jo zapolnite.",
"empty_column.list": "Na tem seznamu ni ničesar. Ko bodo člani tega seznama kaj objavili, se bodo te objave pojavile tukaj.",
"empty_column.mutes": "Niste utišali še nobenega uporabnika.",
"empty_column.notification_requests": "Vse prebrano! Tu ni ničesar več. Ko prejmete nova obvestila, se bodo pojavila tu glede na vaše nastavitve.",
"empty_column.notifications": "Nimate še nobenih obvestil. Povežite se z drugimi, da začnete pogovor.",
"empty_column.public": "Tukaj ni ničesar! Da ga napolnite, napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov",
"empty_column.public": "Tukaj ni ničesar! Napišite nekaj javnega ali pa ročno sledite uporabnikom iz drugih strežnikov, da se bo napolnilo",
"error.unexpected_crash.explanation": "Zaradi hrošča v naši kodi ali težave z združljivostjo brskalnika te strani ni mogoče ustrezno prikazati.",
"error.unexpected_crash.explanation_addons": "Te strani ni mogoče ustrezno prikazati. To napako najverjetneje povzroča dodatek briskalnika ali samodejna orodja za prevajanje.",
"error.unexpected_crash.explanation_addons": "Te strani ni mogoče ustrezno prikazati. To napako najverjetneje povzroča dodatek brskalnika ali samodejna orodja za prevajanje.",
"error.unexpected_crash.next_steps": "Poskusite osvežiti stran. Če to ne pomaga, boste morda še vedno lahko uporabljali Mastodon prek drugega brskalnika ali z domorodno aplikacijo.",
"error.unexpected_crash.next_steps_addons": "Poskusite jih onemogočiti in osvežiti stran. Če to ne pomaga, boste morda še vedno lahko uporabljali Mastodon prek drugega brskalnika ali z domorodno aplikacijo.",
"errors.unexpected_crash.copy_stacktrace": "Kopiraj sledenje skladu na odložišče",
"errors.unexpected_crash.copy_stacktrace": "Kopiraj sled sklada na odložišče",
"errors.unexpected_crash.report_issue": "Prijavi težavo",
"explore.suggested_follows": "Ljudje",
"explore.title": "Razišči",
@ -297,7 +327,7 @@
"explore.trending_tags": "Ključniki",
"filter_modal.added.context_mismatch_explanation": "Ta kategorija filtra ne velja za kontekst, v katerem ste dostopali do te objave. Če želite, da je objava filtrirana tudi v tem kontekstu, morate urediti filter.",
"filter_modal.added.context_mismatch_title": "Neujemanje konteksta!",
"filter_modal.added.expired_explanation": "Ta kategorija filtra je pretekla, morali boste spremeniti datum veljavnosti, da bo veljal še naprej.",
"filter_modal.added.expired_explanation": "Ta kategorija filtra je pretekla. Morali boste spremeniti datum veljavnosti, da bo veljal še naprej.",
"filter_modal.added.expired_title": "Filter je pretekel!",
"filter_modal.added.review_and_configure": "Če želite pregledati in nadalje prilagoditi kategorijo filtra, obiščite {settings_link}.",
"filter_modal.added.review_and_configure_title": "Nastavitve filtra",
@ -312,6 +342,7 @@
"filter_modal.select_filter.title": "Filtriraj to objavo",
"filter_modal.title.status": "Filtrirajte objavo",
"filter_warning.matches_filter": "Se ujema s filtrom »<span>{title}</span>«",
"filtered_notifications_banner.pending_requests": "Od {count, plural, =0 {nikogar, ki bi ga poznali} one {nekoga, ki ga morda poznate} two {dveh ljudi, ki ju morda poznate} other {ljudi, ki jih morda poznate}}",
"filtered_notifications_banner.title": "Filtrirana obvestila",
"firehose.all": "Vse",
"firehose.local": "Ta strežnik",
@ -360,9 +391,12 @@
"hashtag.counter_by_uses_today": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objav} other {{counter} objav}}",
"hashtag.follow": "Sledi ključniku",
"hashtag.unfollow": "Nehaj slediti ključniku",
"hashtags.and_other": "…in še {count, plural, other {#}}",
"hashtags.and_other": "… in še {count, plural, other {#}}",
"hints.profiles.followers_may_be_missing": "Sledilci za ta profil morda manjkajo.",
"hints.profiles.follows_may_be_missing": "Osebe, ki jim ta profil sledi, morda manjkajo.",
"hints.profiles.posts_may_be_missing": "Nekatere objave s tega profila morda manjkajo.",
"hints.profiles.see_more_followers": "Pokaži več sledilcev na {domain}",
"hints.profiles.see_more_follows": "Pokaži več sledenih ljudi na zbirališču {domain}",
"hints.profiles.see_more_posts": "Pokaži več objav na {domain}",
"hints.threads.replies_may_be_missing": "Odgovori z drugih strežnikov morda manjkajo.",
"hints.threads.see_more": "Pokaži več odgovorov na {domain}",
@ -373,9 +407,25 @@
"home.pending_critical_update.link": "Glejte posodobitve",
"home.pending_critical_update.title": "Na voljo je kritična varnostna posodobbitev!",
"home.show_announcements": "Pokaži obvestila",
"ignore_notifications_modal.disclaimer": "Mastodon ne more obveščati uporabnikov, da ste prezrli njihova obvestila. Tudi če jih prezrete, jih lahko uporabniki še vedno pošiljajo.",
"ignore_notifications_modal.filter_instead": "Raje filtriraj",
"ignore_notifications_modal.filter_to_act_users": "Še vedno boste lahko sprejeli, zavrnili ali prijavili uporabnike",
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtriranje pomaga pri izogibanju morebitni zmedi",
"ignore_notifications_modal.filter_to_review_separately": "Filtrirana obvestila lahko pregledate ločeno",
"ignore_notifications_modal.ignore": "Prezri obvestila",
"ignore_notifications_modal.limited_accounts_title": "Naj prezrem obvestila moderiranih računov?",
"ignore_notifications_modal.new_accounts_title": "Naj prezrem obvestila novih računov?",
"ignore_notifications_modal.not_followers_title": "Naj prezrem obvestila ljudi, ki vam ne sledijo?",
"ignore_notifications_modal.not_following_title": "Naj prezrem obvestila ljudi, ki jim ne sledite?",
"ignore_notifications_modal.private_mentions_title": "Naj prezrem obvestila od nezaželenih zasebnih omemb?",
"info_button.label": "Pomoč",
"info_button.what_is_alt_text": "<h1>Kaj je nadomestno besedilo?</h1> <p>Z nadomestnim besedilom dodatno opišemo sliko in tako pomagamo slabovidnim, ljudem s slabo internetno povezavo in tistim, ki jim manjka kontekst.</p> <p>Vaša objava bo veliko bolj dostopna in razumljiva, če boste napisali jasno, jedrnato in nepristransko nadomestno besedilo.</p> <ul> <li>Izpostavite pomembne elemente.</li> <li>Povzemite besedilo v slikah.</li> <li>Pišite v celih stavkih.</li> <li>Zajemite bistvo, ne dolgovezite.</li> <li>Opišite težnje in ključna odkritja, ki ste jih razbrali iz zapletenih grafik (npr. diagramov ali zemljevidov).</li> </ul>",
"interaction_modal.action.favourite": "Med priljubljene lahko dodate, ko se vpišete v svoj račun.",
"interaction_modal.action.follow": "Sledite lahko šele, ko se vpišete v svoj račun.",
"interaction_modal.action.reblog": "Izpostavite lahko šele, ko se vpišete v svoj račun.",
"interaction_modal.action.reply": "Odgovorite lahko šele, ko se vpišete v svoj račun.",
"interaction_modal.action.vote": "Glasujete lahko šele, ko se vpišete v svoj račun.",
"interaction_modal.go": "Naprej",
"interaction_modal.no_account_yet": "Še nimate računa?",
"interaction_modal.on_another_server": "Na drugem strežniku",
"interaction_modal.on_this_server": "Na tem strežniku",
@ -383,6 +433,8 @@
"interaction_modal.title.follow": "Sledi {name}",
"interaction_modal.title.reblog": "Izpostavi objavo {name}",
"interaction_modal.title.reply": "Odgovori na objavo {name}",
"interaction_modal.title.vote": "Izpolni anketo uporabnika/ce {name}",
"interaction_modal.username_prompt": "Npr. {example}",
"intervals.full.days": "{number, plural, one {# dan} two {# dni} few {# dni} other {# dni}}",
"intervals.full.hours": "{number, plural, one {# ura} two {# uri} few {# ure} other {# ur}}",
"intervals.full.minutes": "{number, plural, one {# minuta} two {# minuti} few {# minute} other {# minut}}",
@ -407,7 +459,7 @@
"keyboard_shortcuts.muted": "Odpri seznam utišanih uporabnikov",
"keyboard_shortcuts.my_profile": "Odprite svoj profil",
"keyboard_shortcuts.notifications": "Odpri stolpec z obvestili",
"keyboard_shortcuts.open_media": "Odpri medij",
"keyboard_shortcuts.open_media": "Odpri predstavnost",
"keyboard_shortcuts.pinned": "Odpri seznam pripetih objav",
"keyboard_shortcuts.profile": "Odpri avtorjev profil",
"keyboard_shortcuts.reply": "Odgovori na objavo",
@ -416,26 +468,35 @@
"keyboard_shortcuts.spoilers": "Pokaži/skrij polje CW",
"keyboard_shortcuts.start": "Odpri stolpec \"začni\"",
"keyboard_shortcuts.toggle_hidden": "Pokaži/skrij besedilo za CW",
"keyboard_shortcuts.toggle_sensitivity": "Pokaži/skrij medije",
"keyboard_shortcuts.toggle_sensitivity": "Pokaži/skrij predstavnosti",
"keyboard_shortcuts.toot": "Začni povsem novo objavo",
"keyboard_shortcuts.translate": "za prevod objave",
"keyboard_shortcuts.unfocus": "Odstrani pozornost z območja za sestavljanje besedila/iskanje",
"keyboard_shortcuts.up": "Premakni navzgor po seznamu",
"lightbox.close": "Zapri",
"lightbox.next": "Naslednji",
"lightbox.previous": "Prejšnji",
"lightbox.zoom_in": "Približaj na dejansko velikost",
"lightbox.zoom_out": "Čez cel prikaz",
"limited_account_hint.action": "Vseeno pokaži profil",
"limited_account_hint.title": "Profil so moderatorji strežnika {domain} skrili.",
"link_preview.author": "Avtor_ica {name}",
"link_preview.author": "Avtor/ica {name}",
"link_preview.more_from_author": "Več od {name}",
"link_preview.shares": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}",
"lists.add_member": "Dodaj",
"lists.add_to_list": "Dodaj na seznam",
"lists.add_to_lists": "Dodaj {name} na sezname",
"lists.create": "Ustvari",
"lists.create_a_list_to_organize": "Uredite si domači vir z novim seznamom",
"lists.create_list": "Ustvari seznam",
"lists.delete": "Izbriši seznam",
"lists.done": "Opravljeno",
"lists.edit": "Uredi seznam",
"lists.exclusive": "Skrij člane v domovanju",
"lists.exclusive_hint": "Objave vseh, ki so na tem seznamu, se ne pokažejo v vašem domačem viru. Tako se izognete podvojenim objavam.",
"lists.find_users_to_add": "Poišči člane za dodajanje",
"lists.list_members": "Člani seznama",
"lists.list_members_count": "{count, plural, one {# član} two {# člana} few {# člani} other {# članov}}",
"lists.list_name": "Ime seznama",
"lists.new_list_name": "Novo ime seznama",
"lists.no_lists_yet": "Ni seznamov.",
@ -446,6 +507,8 @@
"lists.replies_policy.list": "Članom seznama",
"lists.replies_policy.none": "Nikomur",
"lists.save": "Shrani",
"lists.search": "Iskanje",
"lists.show_replies_to": "Vključi odgovore, katerih pošiljatelji so člani seznama in prejemniki",
"load_pending": "{count, plural, one {# nov element} two {# nova elementa} few {# novi elementi} other {# novih elementov}}",
"loading_indicator.label": "Nalaganje …",
"media_gallery.hide": "Skrij",
@ -493,9 +556,17 @@
"notification.admin.report_statuses": "{name} je prijavil/a {target} zaradi {category}",
"notification.admin.report_statuses_other": "{name} je prijavil/a {target}",
"notification.admin.sign_up": "{name} se je vpisal/a",
"notification.admin.sign_up.name_and_others": "Prijavili so se {name} in {count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}}",
"notification.annual_report.message": "Čaka vas vaš #Wrapstodon {year}! Razkrijte svoje letošnje nepozabne trenutke na Mastodonu!",
"notification.annual_report.view": "Pokaži #Wrapstodon",
"notification.favourite": "{name} je vzljubil/a vašo objavo",
"notification.favourite.name_and_others_with_link": "{name} in <a>{count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}}</a> je dodalo vašo objavo med priljubljene",
"notification.favourite_pm": "{name} je dodalo vašo zasebno omembo med priljubljene",
"notification.favourite_pm.name_and_others_with_link": "{name} in <a>{count, plural, one {# druga oseba} two {# drugi osebi} few {# druge osebe} other {# drugih oseb}}</a> je dodalo vašo zasebno omembo med priljubljene",
"notification.follow": "{name} vam sledi",
"notification.follow.name_and_others": "{name} in {count, plural, one {<a># druga oseba</a> sta ti sledila} two {<a># drugi osebi</a> so ti sledili} few {<a># druge osebe</a> so ti sledili} other {<a># drugih oseb</a> ti je sledilo}}",
"notification.follow_request": "{name} vam želi slediti",
"notification.follow_request.name_and_others": "{name} in {count, plural, one {# druga oseba bi ti rada sledila} two {# drugi osebi bi ti radi sledili} few {# druge osebe bi ti radi sledili} other {# drugih oseb bi ti radi sledili}}",
"notification.label.mention": "Omemba",
"notification.label.private_mention": "Zasebna omemba",
"notification.label.private_reply": "Zasebni odgovor",
@ -514,6 +585,7 @@
"notification.own_poll": "Vaša anketa je zaključena",
"notification.poll": "Anketa, v kateri ste sodelovali, je zaključena",
"notification.reblog": "{name} je izpostavila/a vašo objavo",
"notification.reblog.name_and_others_with_link": "{name} in <a>{count, plural, one {# druga oseba</a> sta izpostavila tvojo objavo} two {# drugi osebi</a> so izpostavili tvojo objavo} few {# druge osebe</a> so izpostavili tvojo objavo} other {# drugih oseb</a> so izpostavili tvojo objavo}}",
"notification.relationships_severance_event": "Povezave z {name} prekinjene",
"notification.relationships_severance_event.account_suspension": "Skrbnik na {from} je suspendiral račun {target}, kar pomeni, da od računa ne morete več prejemati posodobitev ali imeti z njim interakcij.",
"notification.relationships_severance_event.domain_block": "Skrbnik na {from} je blokiral domeno {target}, vključno z vašimi sledilci ({followersCount}) in {followingCount, plural, one {# računom, ki mu sledite} two {# računoma, ki jima sledite} few {# računi, ki jim sledite} other {# računi, ki jim sledite}}.",
@ -522,12 +594,21 @@
"notification.status": "{name} je pravkar objavil/a",
"notification.update": "{name} je uredil(a) objavo",
"notification_requests.accept": "Sprejmi",
"notification_requests.accept_multiple": "{count, plural, one {Sprejmi # prošnjo …} two {Sprejmi # prošnji …} few {Sprejmi # prošnje …} other {Sprejmi # prošenj …}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Sprejmi prošnjo} two {Sprejmi prošnji} other {Sprejmi prošnje}}",
"notification_requests.confirm_accept_multiple.message": "Sprejeti nameravate {count, plural, one {eno prošnjo za obvestila} two {dve prošnji za obvestila} few {# prošnje za obvestila} other {# prošenj za obvestila}}. Ali ste prepričani?",
"notification_requests.confirm_accept_multiple.title": "Ali želite sprejeti zahteve za obvestila?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Zavrni prošnjo} two {Zavrni prošnji} other {Zavrni prošnje}}",
"notification_requests.confirm_dismiss_multiple.message": "Zavrniti nameravate {count, plural, one {eno prošnjo za obvestila} two {dve prošnji za obvestila} few {# prošnje za obvestila} other {# prošenj za obvestila}}. Do {count, plural, one {nje} two {njiju} other {njih}} ne boste več mogli dostopati. Ali ste prepričani?",
"notification_requests.confirm_dismiss_multiple.title": "Želite opustiti zahteve za obvestila?",
"notification_requests.dismiss": "Zavrni",
"notification_requests.dismiss_multiple": "{count, plural, one {Zavrni # prošnjo …} two {Zavrni # prošnji …} few {Zavrni # prošnje …} other {Zavrni # prošenj …}}",
"notification_requests.edit_selection": "Uredi",
"notification_requests.exit_selection": "Opravljeno",
"notification_requests.explainer_for_limited_account": "Obvestila za ta račun so bila filtrirana, ker je ta račun omejil moderator.",
"notification_requests.explainer_for_limited_remote_account": "Obvestila za ta račun so bila filtrirana, ker je račun ali njegov strežnik omejil moderator.",
"notification_requests.maximize": "Maksimiraj",
"notification_requests.minimize_banner": "Zloži pasico filtriranih obvestil",
"notification_requests.notifications_from": "Obvestila od {name}",
"notification_requests.title": "Filtrirana obvestila",
"notification_requests.view": "Pokaži obvestila",
@ -542,6 +623,7 @@
"notifications.column_settings.filter_bar.category": "Vrstica za hitro filtriranje",
"notifications.column_settings.follow": "Novi sledilci:",
"notifications.column_settings.follow_request": "Nove prošnje za sledenje:",
"notifications.column_settings.group": "Združi",
"notifications.column_settings.mention": "Omembe:",
"notifications.column_settings.poll": "Rezultati ankete:",
"notifications.column_settings.push": "Potisna obvestila",
@ -568,6 +650,9 @@
"notifications.policy.accept": "Sprejmi",
"notifications.policy.accept_hint": "Pokaži med obvestili",
"notifications.policy.drop": "Prezri",
"notifications.policy.drop_hint": "Pošlji v pozabo, od koder se nikdar nič ne vrne",
"notifications.policy.filter": "Filtriraj",
"notifications.policy.filter_hint": "Pošlji med filtrirana prejeta obvestila",
"notifications.policy.filter_limited_accounts_hint": "Omejeno s strani moderatorjev strežnika",
"notifications.policy.filter_limited_accounts_title": "Moderirani računi",
"notifications.policy.filter_new_accounts.hint": "Ustvarjen v {days, plural, one {zadnjem # dnevu} two {zadnjih # dnevih} few {zadnjih # dnevih} other {zadnjih # dnevih}}",
@ -585,12 +670,14 @@
"onboarding.follows.back": "Nazaj",
"onboarding.follows.done": "Opravljeno",
"onboarding.follows.empty": "Žal trenutno ni mogoče prikazati nobenih rezultatov. Lahko poskusite z iskanjem ali brskanjem po strani za raziskovanje, da poiščete osebe, ki jim želite slediti, ali poskusite znova pozneje.",
"onboarding.follows.search": "Išči",
"onboarding.follows.title": "Vaš prvi korak je, da sledite ljudem",
"onboarding.profile.discoverable": "Naj bo moj profil mogoče najti",
"onboarding.profile.discoverable_hint": "Ko se odločite za razkrivanje na Mastodonu, se lahko vaše objave pojavijo v rezultatih iskanja in trendih, vaš profil pa se lahko predlaga ljudem, ki imajo podobne interese kot vi.",
"onboarding.profile.display_name": "Pojavno ime",
"onboarding.profile.display_name_hint": "Vaše polno ime ali lažno ime ...",
"onboarding.profile.note": "Biografija",
"onboarding.profile.note_hint": "Druge osebe lahko @omenite ali #ključite ...",
"onboarding.profile.note_hint": "Lahko @omenite druge osebe ali dodate #ključnike ...",
"onboarding.profile.save_and_continue": "Shrani in nadaljuj",
"onboarding.profile.title": "Nastavitev profila",
"onboarding.profile.upload_avatar": "Naloži sliko profila",
@ -610,6 +697,7 @@
"poll_button.remove_poll": "Odstrani anketo",
"privacy.change": "Spremeni zasebnost objave",
"privacy.direct.long": "Vsem omenjenim v objavi",
"privacy.direct.short": "Zasebna omemba",
"privacy.private.long": "Samo vašim sledilcem",
"privacy.private.short": "Sledilcem",
"privacy.public.long": "Vsem, ki so ali niso na Mastodonu",
@ -621,6 +709,8 @@
"privacy_policy.title": "Pravilnik o zasebnosti",
"recommended": "Priporočeno",
"refresh": "Osveži",
"regeneration_indicator.please_stand_by": "Prosimo, počakajte.",
"regeneration_indicator.preparing_your_home_feed": "Pripravljamo vaš domači vir …",
"relative_time.days": "{number} d",
"relative_time.full.days": "{number, plural, one {pred # dnem} two {pred # dnevoma} few {pred # dnevi} other {pred # dnevi}}",
"relative_time.full.hours": "{number, plural, one {pred # uro} two {pred # urama} few {pred # urami} other {pred # urami}}",
@ -656,7 +746,7 @@
"report.reasons.dislike": "Ni mi všeč",
"report.reasons.dislike_description": "To ni tisto, kar želim videti",
"report.reasons.legal": "To ni legalno",
"report.reasons.legal_description": "Ste mnenja, da krši zakonodajo vaše države ali države strežnika",
"report.reasons.legal_description": "Sem mnenja, da krši zakonodajo moje države ali države strežnika",
"report.reasons.other": "Gre za nekaj drugega",
"report.reasons.other_description": "Težava ne sodi v druge kategorije",
"report.reasons.spam": "To je neželena vsebina",
@ -668,10 +758,10 @@
"report.statuses.subtitle": "Izberite vse, kar ustreza",
"report.statuses.title": "Ali so kakšne objave, ki dokazujejo trditve iz te prijave?",
"report.submit": "Pošlji",
"report.target": "Prijavi {target}",
"report.target": "Prijavljate {target}",
"report.thanks.take_action": "Tukaj so vaše možnosti za nadzor tistega, kar vidite na Mastodonu:",
"report.thanks.take_action_actionable": "Medtem, ko to pregledujemo, lahko proti @{name} ukrepate:",
"report.thanks.title": "Ali ne želite tega videti?",
"report.thanks.title": "Ali ne želite videti tega?",
"report.thanks.title_actionable": "Hvala za prijavo, bomo preverili.",
"report.unfollow": "Ne sledi več @{name}",
"report.unfollow_explanation": "Temu računu sledite. Da ne boste več videli njegovih objav v svojem domačem viru, mu prenehajte slediti.",
@ -695,7 +785,7 @@
"search.search_or_paste": "Iščite ali prilepite URL",
"search_popout.full_text_search_disabled_message": "Ni dostopno na {domain}.",
"search_popout.full_text_search_logged_out_message": "Na voljo le, če ste prijavljeni.",
"search_popout.language_code": "Koda ISO jezika",
"search_popout.language_code": "Jezikovna koda ISO",
"search_popout.options": "Možnosti iskanja",
"search_popout.quick_actions": "Hitra dejanja",
"search_popout.recent": "Nedavna iskanja",
@ -705,8 +795,10 @@
"search_results.all": "Vse",
"search_results.hashtags": "Ključniki",
"search_results.no_results": "Ni rezultatov.",
"search_results.no_search_yet": "Pobrskajte med objavami, profili in ključniki.",
"search_results.see_all": "Poglej vse",
"search_results.statuses": "Objave",
"search_results.title": "Zadetki za \"{q}\"",
"server_banner.about_active_users": "Osebe, ki so uporabljale ta strežnik zadnjih 30 dni (dejavni uporabniki meseca)",
"server_banner.active_users": "dejavnih uporabnikov",
"server_banner.administered_by": "Upravlja:",
@ -724,6 +816,7 @@
"status.bookmark": "Dodaj med zaznamke",
"status.cancel_reblog_private": "Prekliči izpostavitev",
"status.cannot_reblog": "Te objave ni mogoče izpostaviti",
"status.continued_thread": "Nadaljevanje niti",
"status.copy": "Kopiraj povezavo do objave",
"status.delete": "Izbriši",
"status.detailed_status": "Podroben pogled pogovora",
@ -733,7 +826,7 @@
"status.edited": "Zadnje urejanje {date}",
"status.edited_x_times": "Urejeno {count, plural, one {#-krat} two {#-krat} few {#-krat} other {#-krat}}",
"status.embed": "Pridobite kodo za vgradnjo",
"status.favourite": "Priljubljen_a",
"status.favourite": "Priljubljen/a",
"status.favourites": "{count, plural, one {priljubitev} two {priljubitvi} few {priljubitve} other {priljubitev}}",
"status.filter": "Filtriraj to objavo",
"status.history.created": "{name}: ustvarjeno {date}",
@ -741,7 +834,7 @@
"status.load_more": "Naloži več",
"status.media.open": "Kliknite za odpiranje",
"status.media.show": "Kliknite za prikaz",
"status.media_hidden": "Mediji so skriti",
"status.media_hidden": "Predstavnosti so skrite",
"status.mention": "Omeni @{name}",
"status.more": "Več",
"status.mute": "Utišaj @{name}",
@ -758,6 +851,7 @@
"status.redraft": "Izbriši in preoblikuj",
"status.remove_bookmark": "Odstrani zaznamek",
"status.remove_favourite": "Odstrani iz priljubljenih",
"status.replied_in_thread": "Odgovor iz niti",
"status.replied_to": "Odgovoril/a {name}",
"status.reply": "Odgovori",
"status.replyAll": "Odgovori na nit",
@ -767,7 +861,7 @@
"status.show_less_all": "Prikaži manj za vse",
"status.show_more_all": "Pokaži več za vse",
"status.show_original": "Pokaži izvirnik",
"status.title.with_attachments": "{user} je objavil_a {attachmentCount, plural, one {{attachmentCount} priponko} two {{attachmentCount} priponki} few {{attachmentCount} priponke} other {{attachmentCount} priponk}}",
"status.title.with_attachments": "{user} je objavil/a {attachmentCount, plural, one {{attachmentCount} priponko} two {{attachmentCount} priponki} few {{attachmentCount} priponke} other {{attachmentCount} priponk}}",
"status.translate": "Prevedi",
"status.translated_from_with": "Prevedeno iz {lang} s pomočjo {provider}",
"status.uncached_media_warning": "Predogled ni na voljo",
@ -778,7 +872,9 @@
"subscribed_languages.target": "Spremeni naročene jezike za {target}",
"tabs_bar.home": "Domov",
"tabs_bar.notifications": "Obvestila",
"terms_of_service.effective_as_of": "Veljavno od {date}",
"terms_of_service.title": "Pogoji uporabe",
"terms_of_service.upcoming_changes_on": "Spremembe začnejo veljati {date}",
"time_remaining.days": "{number, plural, one {preostaja # dan} two {preostajata # dneva} few {preostajajo # dnevi} other {preostaja # dni}}",
"time_remaining.hours": "{number, plural, one {# ura} other {# ur}} je ostalo",
"time_remaining.minutes": "{number, plural, one {# minuta} other {# minut}} je ostalo",
@ -794,6 +890,11 @@
"upload_button.label": "Dodajte slike, video ali zvočno datoteko",
"upload_error.limit": "Omejitev prenosa datoteke je presežena.",
"upload_error.poll": "Prenos datoteke z anketami ni dovoljen.",
"upload_form.drag_and_drop.instructions": "Predstavnostno priponko lahko poberete tako, da pritisnete preslednico ali vnašalko. S puščicami na tipkovnici premikate priponko v posamezno smer. Priponko lahko odložite na novem položaju s ponovnim pritiskom na preslednico ali vnašalko ali pa dejanje prekličete s tipko ubežnica.",
"upload_form.drag_and_drop.on_drag_cancel": "Premikanje priponke je preklicano. Predstavnostna priponka {item} je padla nazaj na prejšnje mesto.",
"upload_form.drag_and_drop.on_drag_end": "Predstavnostna priponka {item} je padla nazaj.",
"upload_form.drag_and_drop.on_drag_over": "Priponka {item} je bila premaknjena.",
"upload_form.drag_and_drop.on_drag_start": "Pobrana priponka {item}.",
"upload_form.edit": "Uredi",
"upload_progress.label": "Pošiljanje ...",
"upload_progress.processing": "Obdelovanje …",

View file

@ -19,6 +19,7 @@
"account.block_short": "Блокла",
"account.blocked": "Блокланган",
"account.cancel_follow_request": "Киләсе сорау",
"account.copy": "Профиль сылтамасын күчереп ал",
"account.disable_notifications": "@{name} язулары өчен белдерүләр сүндерү",
"account.domain_blocked": "Домен блокланган",
"account.edit_profile": "Профильне үзгәртү",
@ -43,6 +44,8 @@
"account.mention": "@{name} искәртү",
"account.moved_to": "{name} аларның яңа счеты хәзер күрсәтте:",
"account.mute": "@{name} кулланучыга әһәмият бирмәү",
"account.mute_notifications_short": "Искәртүләрне сүндер",
"account.mute_short": "Тавышсыз",
"account.muted": "Әһәмият бирмәнгән",
"account.open_original_page": "Чыганак битен ачу",
"account.posts": "Язма",
@ -58,6 +61,7 @@
"account.unendorse": "Профильдә тәкъдим итмәү",
"account.unfollow": "Язылуны туктату",
"account.unmute": "Kабызыгыз @{name}",
"account.unmute_notifications_short": "Искәртүләрне кабыз",
"account.unmute_short": "Kабызыгыз",
"account_note.placeholder": "Click to add a note",
"admin.dashboard.daily_retention": "Теркәлгәннән соң икенче көнне кулланучыларны тоту коэффициенты",

View file

@ -874,6 +874,7 @@
"tabs_bar.notifications": "Сповіщення",
"terms_of_service.effective_as_of": "Ефективний на {date}",
"terms_of_service.title": "Умови використання",
"terms_of_service.upcoming_changes_on": "Майбутні зміни {date}",
"time_remaining.days": "{number, plural, one {# день} few {# дні} other {# днів}}",
"time_remaining.hours": "{number, plural, one {# година} few {# години} other {# годин}}",
"time_remaining.minutes": "{number, plural, one {# хвилина} few {# хвилини} other {# хвилин}}",

View file

@ -435,7 +435,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
collection = @object['replies']
return if collection.blank?
replies = ActivityPub::FetchRepliesService.new.call(status, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
replies = ActivityPub::FetchRepliesService.new.call(status.account.uri, collection, allow_synchronous_requests: false, request_id: @options[:request_id])
return unless replies.nil?
uri = value_or_id(collection)

View file

@ -0,0 +1,43 @@
# frozen_string_literal: true
module Status::FetchRepliesConcern
extend ActiveSupport::Concern
# enable/disable fetching all replies
FETCH_REPLIES_ENABLED = ENV['FETCH_REPLIES_ENABLED'] == 'true'
# debounce fetching all replies to minimize DoS
FETCH_REPLIES_COOLDOWN_MINUTES = (ENV['FETCH_REPLIES_COOLDOWN_MINUTES'] || 15).to_i.minutes
FETCH_REPLIES_INITIAL_WAIT_MINUTES = (ENV['FETCH_REPLIES_INITIAL_WAIT_MINUTES'] || 5).to_i.minutes
included do
scope :created_recently, -> { where(created_at: FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago..) }
scope :not_created_recently, -> { where(created_at: ..FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago) }
scope :fetched_recently, -> { where(fetched_replies_at: FETCH_REPLIES_COOLDOWN_MINUTES.ago..) }
scope :not_fetched_recently, -> { where(fetched_replies_at: [nil, ..FETCH_REPLIES_COOLDOWN_MINUTES.ago]) }
scope :should_not_fetch_replies, -> { local.or(created_recently.or(fetched_recently)) }
scope :should_fetch_replies, -> { remote.not_created_recently.not_fetched_recently }
# statuses for which we won't receive update or deletion actions,
# and should update when fetching replies
# Status from an account which either
# a) has only remote followers
# b) has local follows that were created after the last update time, or
# c) has no known followers
scope :unsubscribed, lambda {
remote.merge(
Status.left_outer_joins(account: :followers).where.not(followers_accounts: { domain: nil })
.or(where.not('follows.created_at < statuses.updated_at'))
.or(where(follows: { id: nil }))
)
}
end
def should_fetch_replies?
# we aren't brand new, and we haven't fetched replies since the debounce window
FETCH_REPLIES_ENABLED && !local? && created_at <= FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago && (
fetched_replies_at.nil? || fetched_replies_at <= FETCH_REPLIES_COOLDOWN_MINUTES.ago
)
end
end

View file

@ -31,6 +31,7 @@
# markdown :boolean default(FALSE)
# limited_scope :integer
# quote_of_id :bigint(8)
# fetched_replies_at :datetime
#
require 'ostruct'
@ -41,6 +42,7 @@ class Status < ApplicationRecord
include Paginable
include RateLimitable
include Status::DomainBlockConcern
include Status::FetchRepliesConcern
include Status::SafeReblogInsert
include Status::SearchConcern
include Status::SnapshotConcern

View file

@ -0,0 +1,54 @@
# frozen_string_literal: true
class ActivityPub::FetchAllRepliesService < ActivityPub::FetchRepliesService
include JsonLdHelper
# Limit of replies to fetch per status
MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_SINGLE'] || 500).to_i
def call(status_uri, collection_or_uri, max_pages: 1, request_id: nil)
@status_uri = status_uri
super
end
private
def filter_replies(items)
# Find all statuses that we *shouldn't* update the replies for, and use that as a filter.
# We don't assume that we have the statuses before they're created,
# hence the negative filter -
# "keep all these uris except the ones we already have"
# instead of
# "keep all these uris that match some conditions on existing Status objects"
#
# Typically we assume the number of replies we *shouldn't* fetch is smaller than the
# replies we *should* fetch, so we also minimize the number of uris we should load here.
uris = items.map { |item| value_or_id(item) }
# Expand collection to get replies in the DB that were
# - not included in the collection,
# - that we have locally
# - but we have no local followers and thus don't get updates/deletes for
parent_id = Status.where(uri: @status_uri).pick(:id)
unless parent_id.nil?
unsubscribed_replies = Status
.where.not(uri: uris)
.where(in_reply_to_id: parent_id)
.unsubscribed
.pluck(:uri)
uris.concat(unsubscribed_replies)
end
dont_update = Status.where(uri: uris).should_not_fetch_replies.pluck(:uri)
# touch all statuses that already exist and that we're about to update
Status.where(uri: uris).should_fetch_replies.touch_all(:fetched_replies_at)
# Reject all statuses that we already have in the db
uris = (uris - dont_update).take(MAX_REPLIES)
Rails.logger.debug { "FetchAllRepliesService - #{@collection_or_uri}: Fetching filtered statuses: #{uris}" }
uris
end
end

View file

@ -33,7 +33,7 @@ class ActivityPub::FetchFeaturedCollectionService < BaseService
return collection_or_uri if collection_or_uri.is_a?(Hash)
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
fetch_resource_without_id_validation(collection_or_uri, local_follower, true)
fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
end
def process_items(items)

View file

@ -45,7 +45,7 @@ class ActivityPub::FetchFeaturedTagsCollectionService < BaseService
return collection_or_uri if collection_or_uri.is_a?(Hash)
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
fetch_resource_without_id_validation(collection_or_uri, local_follower, true)
fetch_resource_without_id_validation(collection_or_uri, local_follower, raise_on_error: :temporary)
end
def process_items(items)

View file

@ -13,7 +13,7 @@ class ActivityPub::FetchRemoteStatusService < BaseService
@request_id = request_id || "#{Time.now.utc.to_i}-status-#{uri}"
@json = if prefetched_body.nil?
fetch_resource(uri, true, on_behalf_of)
fetch_status(uri, true, on_behalf_of)
else
body_to_json(prefetched_body, compare_id: uri)
end
@ -80,4 +80,20 @@ class ActivityPub::FetchRemoteStatusService < BaseService
def expected_object_type?
equals_or_includes_any?(@json['type'], ActivityPub::Activity::Create::SUPPORTED_TYPES + ActivityPub::Activity::Create::CONVERTED_TYPES)
end
def fetch_status(uri, id_is_known, on_behalf_of = nil)
begin
fetch_resource(uri, id_is_known, on_behalf_of, raise_on_error: :all)
rescue Mastodon::UnexpectedResponseError => e
return unless e.response.code == 404
# If this is a 404 from a public status from a remote account, delete it
existing_status = Status.remote.find_by(uri: uri)
if existing_status&.distributable?
Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" }
Tombstone.find_or_create_by(uri: uri, account: existing_status.account)
RemoveStatusService.new.call(existing_status, redraft: false)
end
end
end
end

View file

@ -3,39 +3,59 @@
class ActivityPub::FetchRepliesService < BaseService
include JsonLdHelper
def call(parent_status, collection_or_uri, allow_synchronous_requests: true, request_id: nil)
@account = parent_status.account
# Limit of fetched replies
MAX_REPLIES = 5
def call(reference_uri, collection_or_uri, max_pages: 1, allow_synchronous_requests: true, request_id: nil)
@reference_uri = reference_uri
@allow_synchronous_requests = allow_synchronous_requests
@items = collection_items(collection_or_uri)
@items, n_pages = collection_items(collection_or_uri, max_pages: max_pages)
return if @items.nil?
FetchReplyWorker.push_bulk(filtered_replies) { |reply_uri| [reply_uri, { 'request_id' => request_id }] }
@items = filter_replies(@items)
FetchReplyWorker.push_bulk(@items) { |reply_uri| [reply_uri, { 'request_id' => request_id }] }
@items
[@items, n_pages]
end
private
def collection_items(collection_or_uri)
def collection_items(collection_or_uri, max_pages: 1)
collection = fetch_collection(collection_or_uri)
return unless collection.is_a?(Hash)
collection = fetch_collection(collection['first']) if collection['first'].present?
return unless collection.is_a?(Hash)
items = []
n_pages = 1
while collection.is_a?(Hash)
items.concat(as_array(collection_page_items(collection)))
break if items.size >= MAX_REPLIES
break if n_pages >= max_pages
collection = collection['next'].present? ? fetch_collection(collection['next']) : nil
n_pages += 1
end
[items, n_pages]
end
def collection_page_items(collection)
case collection['type']
when 'Collection', 'CollectionPage'
as_array(collection['items'])
collection['items']
when 'OrderedCollection', 'OrderedCollectionPage'
as_array(collection['orderedItems'])
collection['orderedItems']
end
end
def fetch_collection(collection_or_uri)
return collection_or_uri if collection_or_uri.is_a?(Hash)
return unless @allow_synchronous_requests
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
return if non_matching_uri_hosts?(@reference_uri, collection_or_uri)
# NOTE: For backward compatibility reasons, Mastodon signs outgoing
# queries incorrectly by default.
@ -45,19 +65,19 @@ class ActivityPub::FetchRepliesService < BaseService
#
# Therefore, retry with correct signatures if this fails.
begin
fetch_resource_without_id_validation(collection_or_uri, nil, true)
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
rescue Mastodon::UnexpectedResponseError => e
raise unless e.response && e.response.code == 401 && Addressable::URI.parse(collection_or_uri).query.present?
fetch_resource_without_id_validation(collection_or_uri, nil, true, request_options: { omit_query_string: false })
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary, request_options: { omit_query_string: false })
end
end
def filtered_replies
def filter_replies(items)
# Only fetch replies to the same server as the original status to avoid
# amplification attacks.
# Also limit to 5 fetched replies to limit potential for DoS.
@items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@account.uri, uri) }.take(5)
items.map { |item| value_or_id(item) }.reject { |uri| non_matching_uri_hosts?(@reference_uri, uri) }.take(MAX_REPLIES)
end
end

View file

@ -69,6 +69,6 @@ class ActivityPub::SynchronizeFollowersService < BaseService
return collection_or_uri if collection_or_uri.is_a?(Hash)
return if non_matching_uri_hosts?(@account.uri, collection_or_uri)
fetch_resource_without_id_validation(collection_or_uri, nil, true)
fetch_resource_without_id_validation(collection_or_uri, nil, raise_on_error: :temporary)
end
end

View file

@ -0,0 +1,68 @@
# frozen_string_literal: true
# Fetch all replies to a status, querying recursively through
# ActivityPub replies collections, fetching any statuses that
# we either don't already have or we haven't checked for new replies
# in the Status::FETCH_REPLIES_COOLDOWN_MINUTES interval
class ActivityPub::FetchAllRepliesWorker
include Sidekiq::Worker
include ExponentialBackoff
include JsonLdHelper
sidekiq_options queue: 'pull', retry: 3
# Global max replies to fetch per request (all replies, recursively)
MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_GLOBAL'] || 1000).to_i
MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i
def perform(root_status_id, options = {})
@root_status = Status.remote.find_by(id: root_status_id)
return unless @root_status&.should_fetch_replies?
@root_status.touch(:fetched_replies_at)
Rails.logger.debug { "FetchAllRepliesWorker - #{@root_status.uri}: Fetching all replies for status: #{@root_status}" }
uris_to_fetch, n_pages = get_replies(@root_status.uri, MAX_PAGES, options)
return if uris_to_fetch.nil?
fetched_uris = uris_to_fetch.clone.to_set
until uris_to_fetch.empty? || fetched_uris.length >= MAX_REPLIES || n_pages >= MAX_PAGES
next_reply = uris_to_fetch.pop
next if next_reply.nil?
new_reply_uris, new_n_pages = get_replies(next_reply, MAX_PAGES - n_pages, options)
next if new_reply_uris.nil?
new_reply_uris = new_reply_uris.reject { |uri| fetched_uris.include?(uri) }
uris_to_fetch.concat(new_reply_uris)
fetched_uris = fetched_uris.merge(new_reply_uris)
n_pages += new_n_pages
end
Rails.logger.debug { "FetchAllRepliesWorker - #{@root_status.uri}: fetched #{fetched_uris.length} replies" }
# Workers shouldn't be returning anything, but this is used in tests
fetched_uris
end
private
def get_replies(status_uri, max_pages, options = {})
replies_collection_or_uri = get_replies_uri(status_uri)
return if replies_collection_or_uri.nil?
ActivityPub::FetchAllRepliesService.new.call(status_uri, replies_collection_or_uri, max_pages: max_pages, **options.deep_symbolize_keys)
end
def get_replies_uri(parent_status_uri)
fetch_resource(parent_status_uri, true)&.fetch('replies', nil)
rescue => e
Rails.logger.info { "FetchAllRepliesWorker - #{@root_status.uri}: Caught exception while resolving replies URI #{parent_status_uri}: #{e} - #{e.message}" }
# Raise if we can't get the collection for top-level status to trigger retry
raise e if parent_status_uri == @root_status.uri
nil
end
end

View file

@ -7,7 +7,7 @@ class ActivityPub::FetchRepliesWorker
sidekiq_options queue: 'pull', retry: 3
def perform(parent_status_id, replies_uri, options = {})
ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id), replies_uri, **options.deep_symbolize_keys)
ActivityPub::FetchRepliesService.new.call(Status.find(parent_status_id).account.uri, replies_uri, **options.deep_symbolize_keys)
rescue ActiveRecord::RecordNotFound
true
end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ActivityPub::UpdateDistributionWorker < ActivityPub::RawDistributionWorker
DEBOUNCE_DELAY = 5.seconds
sidekiq_options queue: 'push', lock: :until_executed, lock_ttl: 1.day.to_i
# Distribute an profile update to servers that might have a copy