Merge commit 'e1dbbf6c9db6dbf3f356dae4c59fa05299372370' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-03-25 19:59:59 +01:00
32 changed files with 329 additions and 155 deletions

View File

@@ -21,12 +21,13 @@ services:
ES_HOST: es
ES_PORT: '9200'
LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000
LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000}
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
ports:
- '127.0.0.1:3000:3000'
- '127.0.0.1:3035:3035'
- '127.0.0.1:4000:4000'
- '3000:3000'
- '3035:3035'
- '4000:4000'
networks:
- external_network
- internal_network

View File

@@ -417,10 +417,10 @@ GEM
redis (>= 3.0.5)
matrix (0.4.2)
memory_profiler (1.1.0)
mime-types (3.6.1)
mime-types (3.6.2)
logger
mime-types-data (~> 3.2015)
mime-types-data (3.2025.0304)
mime-types-data (3.2025.0318)
mini_mime (1.1.5)
mini_portile2 (2.8.8)
minitest (5.25.5)
@@ -440,7 +440,7 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.5)
nokogiri (1.18.6)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
oj (3.16.10)
@@ -789,7 +789,7 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
securerandom (0.4.1)
selenium-webdriver (4.29.1)
selenium-webdriver (4.30.1)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)

View File

@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Api::V1::Accounts::IdentityProofsController < Api::BaseController
include DeprecationConcern
deprecate_api '2022-03-30'
before_action :require_user!
before_action :set_account

View File

@@ -1,6 +1,10 @@
# frozen_string_literal: true
class Api::V1::FiltersController < Api::BaseController
include DeprecationConcern
deprecate_api '2022-11-14'
before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
before_action :require_user!

View File

@@ -1,15 +1,9 @@
# frozen_string_literal: true
class Api::V1::InstancesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale
class Api::V1::InstancesController < Api::V2::InstancesController
include DeprecationConcern
vary_by ''
# Override `current_user` to avoid reading session cookies unless in limited federation mode
def current_user
super if limited_federation_mode?
end
deprecate_api '2022-11-14'
def show
cache_even_if_authenticated!

View File

@@ -2,6 +2,9 @@
class Api::V1::SuggestionsController < Api::BaseController
include Authorization
include DeprecationConcern
deprecate_api '2021-05-16'
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index

View File

@@ -1,12 +1,16 @@
# frozen_string_literal: true
class Api::V1::Trends::TagsController < Api::BaseController
include DeprecationConcern
before_action :set_tags
after_action :insert_pagination_headers
DEFAULT_TAGS_LIMIT = (ENV['MAX_TRENDING_TAGS'] || 10).to_i
deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' }
def index
cache_if_unauthenticated!
render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id)

View File

@@ -1,6 +1,16 @@
# frozen_string_literal: true
class Api::V2::InstancesController < Api::V1::InstancesController
class Api::V2::InstancesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
skip_around_action :set_locale
vary_by ''
# Override `current_user` to avoid reading session cookies unless in limited federation mode
def current_user
super if limited_federation_mode?
end
def show
cache_even_if_authenticated!
render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module DeprecationConcern
extend ActiveSupport::Concern
class_methods do
def deprecate_api(date, sunset: nil, **kwargs)
deprecation_timestamp = "@#{date.to_datetime.to_i}"
sunset = sunset&.to_date&.httpdate
before_action(**kwargs) do
response.headers['Deprecation'] = deprecation_timestamp
response.headers['Sunset'] = sunset if sunset
end
end
end
end

View File

@@ -1,4 +1,9 @@
import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios';
import type {
AxiosError,
AxiosResponse,
Method,
RawAxiosRequestHeaders,
} from 'axios';
import axios from 'axios';
import LinkHeader from 'http-link-header';
@@ -41,7 +46,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
// eslint-disable-next-line import/no-default-export
export default function api(withAuthorization = true) {
return axios.create({
const instance = axios.create({
transitional: {
clarifyTimeoutError: true,
},
@@ -60,6 +65,22 @@ export default function api(withAuthorization = true) {
},
],
});
instance.interceptors.response.use(
(response: AxiosResponse) => {
if (response.headers.deprecation) {
console.warn(
`Deprecated request: ${response.config.method} ${response.config.url}`,
);
}
return response;
},
(error: AxiosError) => {
return Promise.reject(error);
},
);
return instance;
}
type RequestParamsOrData = Record<string, unknown>;

View File

@@ -99,6 +99,7 @@ class Bookmarks extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='bookmarks'
/>
<Helmet>

View File

@@ -99,6 +99,7 @@ class Favourites extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='favourites'
/>
<Helmet>

View File

@@ -15,6 +15,7 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
import { AnimatedNumber } from 'mastodon/components/animated_number';
import { ContentWarning } from 'mastodon/components/content_warning';
import EditedTimestamp from 'mastodon/components/edited_timestamp';
import { FilterWarning } from 'mastodon/components/filter_warning';
import type { StatusLike } from 'mastodon/components/hashtag_bar';
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
import { Icon } from 'mastodon/components/icon';
@@ -70,6 +71,7 @@ export const DetailedStatus: React.FC<{
}) => {
const properStatus = status?.get('reblog') ?? status;
const [height, setHeight] = useState(0);
const [showDespiteFilter, setShowDespiteFilter] = useState(false);
const nodeRef = useRef<HTMLDivElement>();
const handleOpenVideo = useCallback(
@@ -82,6 +84,10 @@ export const DetailedStatus: React.FC<{
[onOpenVideo, status],
);
const handleFilterToggle = useCallback(() => {
setShowDespiteFilter(!showDespiteFilter);
}, [showDespiteFilter, setShowDespiteFilter]);
const handleExpandedToggle = useCallback(() => {
if (onToggleHidden) onToggleHidden(status);
}, [onToggleHidden, status]);
@@ -292,8 +298,12 @@ export const DetailedStatus: React.FC<{
const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
status as StatusLike,
);
const matchedFilters = status.get('matched_filters');
const expanded =
!status.get('hidden') || status.get('spoiler_text').length === 0;
(!matchedFilters || showDespiteFilter) &&
(!status.get('hidden') || status.get('spoiler_text').length === 0);
return (
<div style={outerStyle}>
@@ -334,7 +344,16 @@ export const DetailedStatus: React.FC<{
)}
</Link>
{status.get('spoiler_text').length > 0 && (
{matchedFilters && (
<FilterWarning
title={matchedFilters.join(', ')}
expanded={showDespiteFilter}
onClick={handleFilterToggle}
/>
)}
{status.get('spoiler_text').length > 0 &&
(!matchedFilters || showDespiteFilter) && (
<ContentWarning
text={
status.getIn(['translation', 'spoilerHtml']) ||

View File

@@ -138,7 +138,7 @@ const makeMapStateToProps = () => {
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId });
const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
let ancestorsIds = ImmutableList();
let descendantsIds = ImmutableList();

View File

@@ -110,7 +110,7 @@
"annual_report.summary.most_used_hashtag.most_used_hashtag": "nejpoužívanější hashtag",
"annual_report.summary.most_used_hashtag.none": "Žádné",
"annual_report.summary.new_posts.new_posts": "nové příspěvky",
"annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do vrcholu</topLabel><percentage></percentage><bottomLabel>{domain} uživatelů.</bottomLabel>",
"annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do horních</topLabel><percentage></percentage><bottomLabel> uživatelů domény {domain}.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka, zůstane mezi námi ;).",
"annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!",
"attachments_list.unprocessed": "(nezpracováno)",

View File

@@ -6,20 +6,20 @@
"account.badges.group": "ჯგუფი",
"account.block": "დაბლოკე @{name}",
"account.block_domain": "დაიმალოს ყველაფერი დომენიდან {domain}",
"account.blocked": "დაბლოკა",
"account.blocked": "დაბლოკილია",
"account.cancel_follow_request": "Withdraw follow request",
"account.domain_blocked": "დომენი დამალულია",
"account.edit_profile": "პროფილის ცვლილება",
"account.endorse": "გამორჩევა პროფილზე",
"account.featured_tags.last_status_never": "პოსტები არ არის",
"account.featured_tags.last_status_never": "პოსტების გარეშე",
"account.follow": "გაყოლა",
"account.followers": "მიმდევრები",
"account.hide_reblogs": "დაიმალოს ბუსტები @{name}-სგან",
"account.media": "მედია",
"account.mention": "ასახელეთ @{name}",
"account.mute": "გააჩუმე @{name}",
"account.muted": "გაჩუმებული",
"account.posts": "ტუტები",
"account.muted": "დადუმებული",
"account.posts": "პოსტები",
"account.posts_with_replies": "ტუტები და პასუხები",
"account.report": "დაარეპორტე @{name}",
"account.requested": "დამტკიცების მოლოდინში. დააწკაპუნეთ რომ უარყოთ დადევნების მოთხონვა",
@@ -42,7 +42,7 @@
"column.community": "ლოკალური თაიმლაინი",
"column.domain_blocks": "დამალული დომენები",
"column.follow_requests": "დადევნების მოთხოვნები",
"column.home": "სახლი",
"column.home": "საწყისი",
"column.lists": "სიები",
"column.mutes": "გაჩუმებული მომხმარებლები",
"column.notifications": "შეტყობინებები",
@@ -52,9 +52,9 @@
"column_header.hide_settings": "პარამეტრების დამალვა",
"column_header.moveLeft_settings": "სვეტის მარცხნივ გადატანა",
"column_header.moveRight_settings": "სვეტის მარჯვნივ გადატანა",
"column_header.pin": "აპინვა",
"column_header.pin": "მიმაგრება",
"column_header.show_settings": "პარამეტრების ჩვენება",
"column_header.unpin": "პინის მოხსნა",
"column_header.unpin": "მოხსნა",
"column_subheading.settings": "პარამეტრები",
"community.column_settings.media_only": "მხოლოდ მედია",
"compose_form.direct_message_warning_learn_more": "გაიგე მეტი",
@@ -66,21 +66,21 @@
"compose_form.publish_form": "Publish",
"compose_form.spoiler.marked": "გაფრთხილების უკან ტექსტი დამალულია",
"compose_form.spoiler.unmarked": "ტექსტი არაა დამალული",
"confirmation_modal.cancel": "უარყოფა",
"confirmation_modal.cancel": "გაუქმება",
"confirmations.block.confirm": "ბლოკი",
"confirmations.delete.confirm": "გაუქმება",
"confirmations.delete.confirm": "წაშლა",
"confirmations.delete.message": "დარწმუნებული ხართ, გსურთ გააუქმოთ ეს სტატუსი?",
"confirmations.delete_list.confirm": "გაუქმება",
"confirmations.delete_list.confirm": "წაშლა",
"confirmations.delete_list.message": "დარწმუნებული ხართ, გსურთ სამუდამოდ გააუქმოთ ეს სია?",
"confirmations.mute.confirm": "გაჩუმება",
"confirmations.mute.confirm": "დადუმება",
"confirmations.redraft.confirm": "გაუქმება და გადანაწილება",
"confirmations.unfollow.confirm": "ნუღარ მიჰყვები",
"confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?",
"embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.",
"embed.preview": "ესაა თუ როგორც გამოჩნდება:",
"emoji_button.activity": "აქტივობა",
"emoji_button.custom": "პერსონალიზირებლი",
"emoji_button.flags": "დროშები",
"emoji_button.custom": "მომხმარებლი",
"emoji_button.flags": "ალმები",
"emoji_button.food": "საჭმელი და სასლმელი",
"emoji_button.label": "ემოჯის ჩასმა",
"emoji_button.nature": "ბუმება",
@@ -119,7 +119,7 @@
"keyboard_shortcuts.federated": "to open federated timeline",
"keyboard_shortcuts.heading": "კლავიატურის სწრაფი ბმულები",
"keyboard_shortcuts.home": "to open home timeline",
"keyboard_shortcuts.hotkey": "ცხელი კლავიში",
"keyboard_shortcuts.hotkey": "მალსახმობი ღილაკი",
"keyboard_shortcuts.legend": "ამ ლეგენდის გამოსაჩენად",
"keyboard_shortcuts.local": "to open local timeline",
"keyboard_shortcuts.mention": "ავტორის დასახელებლად",
@@ -180,20 +180,20 @@
"relative_time.just_now": "ახლა",
"relative_time.minutes": "{number}წთ",
"relative_time.seconds": "{number}წმ",
"reply_indicator.cancel": "უარყოფა",
"reply_indicator.cancel": "გაუქმება",
"report.forward": "ფორვარდი {target}-ს",
"report.forward_hint": "ანგარიში სხვა სერვერიდანაა. გავაგზავნოთ რეპორტის ანონიმური ასლიც?",
"report.placeholder": "დამატებითი კომენტარები",
"report.submit": "დასრულება",
"report.submit": "გადაცემა",
"report.target": "არეპორტებთ {target}",
"report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
"search.placeholder": "ძებნა",
"search_results.hashtags": "ჰეშტეგები",
"search_results.statuses": "ტუტები",
"sign_in_banner.sign_in": "Sign in",
"search_results.statuses": "პოსტები",
"sign_in_banner.sign_in": "შესვლა",
"status.admin_status": "Open this status in the moderation interface",
"status.block": "დაბლოკე @{name}",
"status.cancel_reblog_private": "ბუსტის მოშორება",
"status.cancel_reblog_private": "ბუსტის მოხსნა",
"status.cannot_reblog": "ეს პოსტი ვერ დაიბუსტება",
"status.copy": "Copy link to status",
"status.delete": "წაშლა",
@@ -222,7 +222,7 @@
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
"status.unmute_conversation": "საუბარზე გაჩუმების მოშორება",
"status.unpin": "პროფილიდან პინის მოშორება",
"tabs_bar.home": "სახლი",
"tabs_bar.home": "საწყისი",
"tabs_bar.notifications": "შეტყობინებები",
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}",
"ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.",

View File

@@ -173,7 +173,7 @@
"compose.published.open": "Atvērt",
"compose.saved.body": "Ziņa saglabāta.",
"compose_form.direct_message_warning_learn_more": "Uzzināt vairāk",
"compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jutīgu informāciju caur Mastodon!",
"compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jūtīgu informāciju caur Mastodon!",
"compose_form.hashtag_warning": "Šis ieraksts netiks uzrādīts nevienā tēmturī, jo tas nav redzams visiem. Tikai visiem redzamos ierakstus var meklēt pēc tēmtura.",
"compose_form.lock_disclaimer": "Tavs konts nav {locked}. Ikviens var Tev sekot, lai redzētu tikai sekotājiem paredzētos ierakstus.",
"compose_form.lock_disclaimer.lock": "slēgts",
@@ -474,9 +474,9 @@
"notification.moderation_warning": "Ir saņemts satura pārraudzības brīdinājums",
"notification.moderation_warning.action_delete_statuses": "Daži no Taviem ierakstiem tika noņemti.",
"notification.moderation_warning.action_disable": "Tavs konts tika atspējots.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jutīgi.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jūtīgi.",
"notification.moderation_warning.action_none": "Konts ir saņēmis satura pārraudzības brīdinājumu.",
"notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jutīgi.",
"notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jūtīgi.",
"notification.moderation_warning.action_silence": "Tavs konts tika ierobežots.",
"notification.moderation_warning.action_suspend": "Tava konta darbība tika apturēta.",
"notification.own_poll": "Tava aptauja ir noslēgusies",
@@ -702,7 +702,7 @@
"status.reply": "Atbildēt",
"status.replyAll": "Atbildēt uz tematu",
"status.report": "Ziņot par @{name}",
"status.sensitive_warning": "Sensivs saturs",
"status.sensitive_warning": "gs saturs",
"status.share": "Kopīgot",
"status.show_less_all": "Rādīt mazāk visiem",
"status.show_more_all": "Rādīt vairāk visiem",

View File

@@ -15,9 +15,10 @@ export const makeGetStatus = () => {
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
getFilters,
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
],
(statusBase, statusReblog, accountBase, accountReblog, filters) => {
(statusBase, statusReblog, accountBase, accountReblog, filters, warnInsteadOfHide) => {
if (!statusBase || statusBase.get('isLoading')) {
return null;
}
@@ -31,7 +32,7 @@ export const makeGetStatus = () => {
let filtered = false;
if ((accountReblog || accountBase).get('id') !== me && filters) {
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
return null;
}
filterResults = filterResults.filter(result => filters.has(result.get('filter')));

View File

@@ -6,6 +6,11 @@ export const toServerSideType = (columnType: string) => {
case 'thread':
case 'account':
return columnType;
case 'detailed':
return 'thread';
case 'bookmarks':
case 'favourites':
return 'home';
default:
if (columnType.includes('list:')) {
return 'home';

View File

@@ -57,6 +57,9 @@ class ActivityPub::SynchronizeFollowersService < BaseService
collection = fetch_collection(collection['first']) if collection['first'].present?
return unless collection.is_a?(Hash)
# Abort if we'd have to paginate through more than one page of followers
return if collection['next'].present?
case collection['type']
when 'Collection', 'CollectionPage'
as_array(collection['items'])

View File

@@ -49,6 +49,10 @@ lv:
attributes:
reblog:
taken: ziņai jau pastāv
terms_of_service:
attributes:
effective_date:
too_soon: ir pārāk agri, jābūt vēlāk kā %{date}
user:
attributes:
date_of_birth:

View File

@@ -2,7 +2,7 @@
lv:
devise:
confirmations:
confirmed: Tava e-pasta adrese ir veiksmīgi apstiprināta.
confirmed: Tava e-pasta adrese tika sekmīgi apstiprināta.
send_instructions: Pēc dažām minūtēm saņemsi e-pasta ziņojum ar norādēm, kā apstiprināt savu e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
send_paranoid_instructions: Ja Tava e-pasta adrese ir mūsu datubāzē, pēc dažām minūtēm saņemsi e-pasta ziņojumu ar norādēm, kā apstiprināt savu e-pasta adresi. Lūgums pārbaudīt mēstuļu mapi, ja nesaņēmi šo e-pasta ziņojumu.
failure:

View File

@@ -150,13 +150,13 @@ lv:
title: OAuth nepieciešama autorizācija
scopes:
admin:read: lasīt visus datus uz servera
admin:read:accounts: lasīt sensivu informāciju no visiem kontiem
admin:read:canonical_email_blocks: lasīt sensivu informāciju par visiem kanoniskajiem e-pasta blokiem
admin:read:domain_allows: lasīt visu domēnu sensitīvo informāciju, ko atļauj
admin:read:domain_blocks: lasīt sensivu informāciju par visiem domēna blokiem
admin:read:email_domain_blocks: lasīt sensivu informāciju par visiem e-pasta domēna blokiem
admin:read:ip_blocks: lasīt sensivu informāciju par visiem IP blokiem
admin:read:reports: lasīt sensivu informāciju no visiem pārskatiem un kontiem, par kuriem ziņots
admin:read:accounts: lasīt gu informāciju no visiem kontiem
admin:read:canonical_email_blocks: lasīt gu informāciju par visiem kanoniskajiem e-pasta blokiem
admin:read:domain_allows: lasīt jūtīgu informāciju par visiem atļautajiem domēniem
admin:read:domain_blocks: lasīt gu informāciju par visiem domēna blokiem
admin:read:email_domain_blocks: lasīt gu informāciju par visiem e-pasta domēna blokiem
admin:read:ip_blocks: lasīt gu informāciju par visiem IP blokiem
admin:read:reports: lasīt gu informāciju no visiem pārskatiem un kontiem, par kuriem ziņots
admin:write: modificēt visus datus uz servera
admin:write:accounts: veikt satura pārraudzības darbības kontos
admin:write:canonical_email_blocks: veikt satura pārraudzības darbības kanoniskajos e-pasta blokos

View File

@@ -144,8 +144,8 @@ lv:
security_measures:
only_password: Tikai parole
password_and_2fa: Parole un 2FA
sensitive: Sensitīvs
sensitized: Atzīmēts kā sensitīvs
sensitive: Uzspiest atzīmēšanu kā jūtīgu
sensitized: Atzīmēts kā jūtīgu
shared_inbox_url: Koplietotās iesūtnes URL
show:
created_reports: Sastādītie ziņojumi
@@ -163,7 +163,7 @@ lv:
unblock_email: Atbloķēt e-pasta adresi
unblocked_email_msg: Veiksmīgi atbloķēta %{username} e-pasta adrese
unconfirmed_email: Neapstiprināts e-pasts
undo_sensitized: Atcelt sensitivizēšanu
undo_sensitized: Atcelt uzspiestu atzīmēšanu kā jūtīgu
undo_silenced: Atsaukt ierobežojumu
undo_suspension: Atsaukt apturēšanu
unsilenced_msg: Veiksmīgi atsaukts %{username} konta ierobežojums
@@ -225,12 +225,12 @@ lv:
resend_user: Atkārtoti nosūtīt Apstiprinājuma Pastu
reset_password_user: Atiestatīt Paroli
resolve_report: Atrisināt Ziņojumu
sensitive_account: Piespiedu sensitīvizēt kontu
sensitive_account: Uzspiesti atzimēt kontu kā jūtīgu
silence_account: Ierobežot Kontu
suspend_account: Apturēt Kontu
unassigned_report: Atcelt Pārskata Piešķiršanu
unblock_email_account: Atbloķēt e-pasta adresi
unsensitive_account: Atsaukt Konta Piespiedu Sensitivizēšanu
unsensitive_account: Atsaukt uzspiestu konta atzīmēšanu kā jūtīgu
unsilence_account: Atcelt Konta Ierobežošanu
unsuspend_account: Atcelt konta apturēšanu
update_announcement: Atjaunināt Paziņojumu
@@ -621,7 +621,7 @@ lv:
add_to_report: Pievienot varāk paziņošanai
are_you_sure: Vai esi pārliecināts?
assign_to_self: Piešķirt man
assigned: Piešķirtais moderators
assigned: Piešķirtais satura pārraudzītājs
by_target_domain: Ziņotā konta domēns
cancel: Atcelt
category: Kategorija

View File

@@ -88,6 +88,7 @@ zh-CN:
favicon: WEBP、PNG、GIF 或 JPG。使用自定义图标覆盖 Mastodon 的默认图标。
mascot: 覆盖高级网页界面中的绘图形象。
media_cache_retention_period: 来自外站用户嘟文的媒体文件将被缓存到你的实例上。当该值被设为正值时,缓存的媒体文件将在指定天数后被清除。如果媒体文件在被清除后重新被请求,且源站内容仍然可用,它将被重新下载。由于链接预览卡拉取第三方站点的频率受到限制,建议将此值设置为至少 14 天,如果小于该值,链接预览卡将不会按需更新。
min_age: 用户注册时必须确认出生日期
peers_api_enabled: 本站在联邦宇宙中遇到的站点列表。 此处不包含关于您是否与给定站点联合的数据,只是您的实例知道它。 这由收集一般意义上的联合统计信息的服务使用。
profile_directory: 个人资料目录会列出所有选择可被发现的用户。
require_invite_text: 当注册需要手动批准时,将“你为什么想要加入?”设为必填项
@@ -142,6 +143,7 @@ zh-CN:
min_age: 不应低于您所在地法律管辖权要求的最低年龄。
user:
chosen_languages: 仅选中语言的嘟文会出现在公共时间线上(全不选则显示所有语言的嘟文)
date_of_birth: 我们必须确认%{age}岁以上的用户才能使用Mastodon。我们不会存储该信息。
role: 角色用于控制用户拥有的权限。
user_role:
color: 在界面各处用于标记该角色的颜色,以十六进制 RGB 格式表示

View File

@@ -317,6 +317,7 @@ zh-CN:
title: 新公告
preview:
explanation_html: 此电子邮件将发送给 <strong>%{display_count} 用户</strong>。电子邮件将包含以下文本:
title: 预览公告通知
publish: 发布
published_msg: 公告已发布!
scheduled_for: 定时在 %{time}
@@ -1864,6 +1865,10 @@ zh-CN:
recovery_instructions_html: 如果你的手机无法使用,你可以使用下列任意一个恢复代码来重新获得对账号的访问权。<strong>请妥善保管好你的恢复代码</strong>(例如,你可以将它们打印出来,然后和其他重要的文件放在一起)。
webauthn: 安全密钥
user_mailer:
announcement_published:
description: "%{domain}管理员发布了一则公告:"
subject: 服务公告
title: "%{domain}服务公告"
appeal_approved:
action: 账号设置
explanation: 你于 %{appeal_date} 对 %{strike_date} 在你账号上做出的处罚提出的申诉已被批准,你的账号已回到正常状态。

View File

@@ -15,6 +15,8 @@ RSpec.describe 'API V1 Trends Tags' do
.and not_have_http_link_header
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to be_nil
end
end
@@ -31,6 +33,8 @@ RSpec.describe 'API V1 Trends Tags' do
.and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next')
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to be_nil
end
def prepare_trends

View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'deprecated API V1 Trends Tags' do
describe 'GET /api/v1/trends' do
context 'when trends are disabled' do
before { Setting.trends = false }
it 'returns http success' do
get '/api/v1/trends'
expect(response)
.to have_http_status(200)
.and not_have_http_link_header
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to start_with '@'
end
end
context 'when trends are enabled' do
before { Setting.trends = true }
it 'returns http success' do
prepare_trends
stub_const('Api::V1::Trends::TagsController::DEFAULT_TAGS_LIMIT', 2)
get '/api/v1/trends'
expect(response)
.to have_http_status(200)
.and have_http_link_header(api_v1_trends_tags_url(offset: 2)).for(rel: 'next')
expect(response.content_type)
.to start_with('application/json')
expect(response.headers['Deprecation'])
.to start_with '@'
end
def prepare_trends
Fabricate.times(3, :tag, trendable: true).each do |tag|
2.times { |i| Trends.tags.add(tag, i) }
end
Trends::Tags.new(threshold: 1).refresh
end
end
end
end

View File

@@ -158,14 +158,13 @@ RSpec.describe 'Notifications' do
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:notification_groups]).to contain_exactly(
a_hash_including(
type: 'favourite',
sample_account_ids: have_attributes(size: 5),
page_min_id: notification_ids.first.to_s,
page_max_id: notification_ids.last.to_s
)
)
expect(response.parsed_body[:notification_groups].size)
.to eq(1)
expect(response.parsed_body.dig(:notification_groups, 0))
.to include(type: 'favourite')
.and(include(sample_account_ids: have_attributes(size: 5)))
.and(include(page_max_id: notification_ids.last.to_s))
.and(include(page_min_id: notification_ids.first.to_s))
end
end
@@ -180,14 +179,13 @@ RSpec.describe 'Notifications' do
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:notification_groups]).to contain_exactly(
a_hash_including(
type: 'favourite',
sample_account_ids: have_attributes(size: 5),
page_min_id: notification_ids.first.to_s,
page_max_id: notification_ids.last.to_s
)
)
expect(response.parsed_body[:notification_groups].size)
.to eq(1)
expect(response.parsed_body.dig(:notification_groups, 0))
.to include(type: 'favourite')
.and(include(sample_account_ids: have_attributes(size: 5)))
.and(include(page_max_id: notification_ids.last.to_s))
.and(include(page_min_id: notification_ids.first.to_s))
end
end
end

View File

@@ -2,15 +2,15 @@
require 'rails_helper'
RSpec.describe IntentsController do
render_views
RSpec.describe 'Intents' do
let(:user) { Fabricate(:user) }
before { sign_in user, scope: :user }
describe 'GET #show' do
subject { get :show, params: { uri: uri } }
describe 'GET /intent' do
subject { response }
before { get intent_path(uri: uri) }
context 'when schema is web+mastodon' do
context 'when host is follow' do

View File

@@ -27,14 +27,14 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
}.with_indifferent_access
end
shared_examples 'synchronizes followers' do
before do
alice.follow!(actor)
bob.follow!(actor)
mallory.request_follow!(actor)
end
allow(ActivityPub::DeliveryWorker).to receive(:perform_async)
shared_examples 'synchronizes followers' do
before do
subject.call(actor, collection_uri)
end
@@ -46,7 +46,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
expect(mallory)
.to be_following(actor) # Convert follow request to follow when accepted
expect(ActivityPub::DeliveryWorker)
.to have_received(:perform_async).with(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
.to have_enqueued_sidekiq_job(anything, eve.id, actor.inbox_url) # Send Undo Follow to actor
end
end
@@ -76,7 +76,7 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
it_behaves_like 'synchronizes followers'
end
context 'when the endpoint is a paginated Collection of actor URIs' do
context 'when the endpoint is a single-page paginated Collection of actor URIs' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
@@ -96,5 +96,30 @@ RSpec.describe ActivityPub::SynchronizeFollowersService do
it_behaves_like 'synchronizes followers'
end
context 'when the endpoint is a paginated Collection of actor URIs with a next page' do
let(:payload) do
{
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'Collection',
id: collection_uri,
first: {
type: 'CollectionPage',
partOf: collection_uri,
items: items,
next: "#{collection_uri}/page2",
},
}.with_indifferent_access
end
before do
stub_request(:get, collection_uri).to_return(status: 200, body: Oj.dump(payload), headers: { 'Content-Type': 'application/activity+json' })
end
it 'does not change followers' do
expect { subject.call(actor, collection_uri) }
.to_not(change { actor.followers.reload.reorder(id: :asc).pluck(:id) })
end
end
end
end

112
yarn.lock
View File

@@ -96,16 +96,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/generator@npm:^7.26.10, @babel/generator@npm:^7.7.2":
version: 7.26.10
resolution: "@babel/generator@npm:7.26.10"
"@babel/generator@npm:^7.26.10, @babel/generator@npm:^7.27.0, @babel/generator@npm:^7.7.2":
version: 7.27.0
resolution: "@babel/generator@npm:7.27.0"
dependencies:
"@babel/parser": "npm:^7.26.10"
"@babel/types": "npm:^7.26.10"
"@babel/parser": "npm:^7.27.0"
"@babel/types": "npm:^7.27.0"
"@jridgewell/gen-mapping": "npm:^0.3.5"
"@jridgewell/trace-mapping": "npm:^0.3.25"
jsesc: "npm:^3.0.2"
checksum: 10c0/88b3b3ea80592fc89349c4e1a145e1386e4042866d2507298adf452bf972f68d13bf699a845e6ab8c028bd52c2247013eb1221b86e1db5c9779faacba9c4b10e
checksum: 10c0/7cb10693d2b365c278f109a745dc08856cae139d262748b77b70ce1d97da84627f79648cab6940d847392c0e5d180441669ed958b3aee98d9c7d274b37c553bd
languageName: node
linkType: hard
@@ -141,20 +141,20 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-create-class-features-plugin@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-create-class-features-plugin@npm:7.25.9"
"@babel/helper-create-class-features-plugin@npm:^7.25.9, @babel/helper-create-class-features-plugin@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/helper-create-class-features-plugin@npm:7.27.0"
dependencies:
"@babel/helper-annotate-as-pure": "npm:^7.25.9"
"@babel/helper-member-expression-to-functions": "npm:^7.25.9"
"@babel/helper-optimise-call-expression": "npm:^7.25.9"
"@babel/helper-replace-supers": "npm:^7.25.9"
"@babel/helper-replace-supers": "npm:^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9"
"@babel/traverse": "npm:^7.25.9"
"@babel/traverse": "npm:^7.27.0"
semver: "npm:^6.3.1"
peerDependencies:
"@babel/core": ^7.0.0
checksum: 10c0/b2bdd39f38056a76b9ba00ec5b209dd84f5c5ebd998d0f4033cf0e73d5f2c357fbb49d1ce52db77a2709fb29ee22321f84a5734dc9914849bdfee9ad12ce8caf
checksum: 10c0/c4945903136d934050e070f69a4d72ec425f1f70634e0ddf14ad36695f935125a6df559f8d5b94cc1ed49abd4ce9c5be8ef3ba033fa8d09c5dd78d1a9b97d8cc
languageName: node
linkType: hard
@@ -248,16 +248,16 @@ __metadata:
languageName: node
linkType: hard
"@babel/helper-replace-supers@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-replace-supers@npm:7.25.9"
"@babel/helper-replace-supers@npm:^7.25.9, @babel/helper-replace-supers@npm:^7.26.5":
version: 7.26.5
resolution: "@babel/helper-replace-supers@npm:7.26.5"
dependencies:
"@babel/helper-member-expression-to-functions": "npm:^7.25.9"
"@babel/helper-optimise-call-expression": "npm:^7.25.9"
"@babel/traverse": "npm:^7.25.9"
"@babel/traverse": "npm:^7.26.5"
peerDependencies:
"@babel/core": ^7.0.0
checksum: 10c0/0b40d7d2925bd3ba4223b3519e2e4d2456d471ad69aa458f1c1d1783c80b522c61f8237d3a52afc9e47c7174129bbba650df06393a6787d5722f2ec7f223c3f4
checksum: 10c0/b19b1245caf835207aaaaac3a494f03a16069ae55e76a2e1350b5acd560e6a820026997a8160e8ebab82ae873e8208759aa008eb8422a67a775df41f0a4633d4
languageName: node
linkType: hard
@@ -313,14 +313,14 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.26.9":
version: 7.26.10
resolution: "@babel/parser@npm:7.26.10"
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.26.10, @babel/parser@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/parser@npm:7.27.0"
dependencies:
"@babel/types": "npm:^7.26.10"
"@babel/types": "npm:^7.27.0"
bin:
parser: ./bin/babel-parser.js
checksum: 10c0/c47f5c0f63cd12a663e9dc94a635f9efbb5059d98086a92286d7764357c66bceba18ccbe79333e01e9be3bfb8caba34b3aaebfd8e62c3d5921c8cf907267be75
checksum: 10c0/ba2ed3f41735826546a3ef2a7634a8d10351df221891906e59b29b0a0cd748f9b0e7a6f07576858a9de8e77785aad925c8389ddef146de04ea2842047c9d2859
languageName: node
linkType: hard
@@ -851,7 +851,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-modules-commonjs@npm:^7.25.9, @babel/plugin-transform-modules-commonjs@npm:^7.26.3":
"@babel/plugin-transform-modules-commonjs@npm:^7.26.3":
version: 7.26.3
resolution: "@babel/plugin-transform-modules-commonjs@npm:7.26.3"
dependencies:
@@ -1208,18 +1208,18 @@ __metadata:
languageName: node
linkType: hard
"@babel/plugin-transform-typescript@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-typescript@npm:7.25.9"
"@babel/plugin-transform-typescript@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/plugin-transform-typescript@npm:7.27.0"
dependencies:
"@babel/helper-annotate-as-pure": "npm:^7.25.9"
"@babel/helper-create-class-features-plugin": "npm:^7.25.9"
"@babel/helper-plugin-utils": "npm:^7.25.9"
"@babel/helper-create-class-features-plugin": "npm:^7.27.0"
"@babel/helper-plugin-utils": "npm:^7.26.5"
"@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9"
"@babel/plugin-syntax-typescript": "npm:^7.25.9"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10c0/c607ddb45f7e33cfcb928aad05cb1b18b1ecb564d2329d8f8e427f75192511aa821dee42d26871f1bdffbd883853e150ba81436664646c6e6b13063e65ce1475
checksum: 10c0/028e75dd6195495dc2d105ca8ded19d62aef90a215d597451cee57c35325960a87963913aa9a21b8ade190c638b588422292ea7e23b21565baf53c469254dbd4
languageName: node
linkType: hard
@@ -1379,17 +1379,17 @@ __metadata:
linkType: hard
"@babel/preset-typescript@npm:^7.21.5":
version: 7.26.0
resolution: "@babel/preset-typescript@npm:7.26.0"
version: 7.27.0
resolution: "@babel/preset-typescript@npm:7.27.0"
dependencies:
"@babel/helper-plugin-utils": "npm:^7.25.9"
"@babel/helper-plugin-utils": "npm:^7.26.5"
"@babel/helper-validator-option": "npm:^7.25.9"
"@babel/plugin-syntax-jsx": "npm:^7.25.9"
"@babel/plugin-transform-modules-commonjs": "npm:^7.25.9"
"@babel/plugin-transform-typescript": "npm:^7.25.9"
"@babel/plugin-transform-modules-commonjs": "npm:^7.26.3"
"@babel/plugin-transform-typescript": "npm:^7.27.0"
peerDependencies:
"@babel/core": ^7.0.0-0
checksum: 10c0/20d86bc45d2bbfde2f84fc7d7b38746fa6481d4bde6643039ad4b1ff0b804c6d210ee43e6830effd8571f2ff43fa7ffd27369f42f2b3a2518bb92dc86c780c61
checksum: 10c0/986b20edab3c18727d911a6e1a14095c1271afc6cc625b02f42b371f06c1e041e5d7c1baf2afe8b0029b60788a06f02fd6844dedfe54183b148ab9a7429438a9
languageName: node
linkType: hard
@@ -1403,47 +1403,47 @@ __metadata:
linkType: hard
"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.22.3, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2":
version: 7.26.10
resolution: "@babel/runtime@npm:7.26.10"
version: 7.27.0
resolution: "@babel/runtime@npm:7.27.0"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: 10c0/6dc6d88c7908f505c4f7770fb4677dfa61f68f659b943c2be1f2a99cb6680343462867abf2d49822adc435932919b36c77ac60125793e719ea8745f2073d3745
checksum: 10c0/35091ea9de48bd7fd26fb177693d64f4d195eb58ab2b142b893b7f3fa0f1d7c677604d36499ae0621a3703f35ba0c6a8f6c572cc8f7dc0317213841e493cf663
languageName: node
linkType: hard
"@babel/template@npm:^7.25.9, @babel/template@npm:^7.26.9, @babel/template@npm:^7.3.3":
version: 7.26.9
resolution: "@babel/template@npm:7.26.9"
"@babel/template@npm:^7.25.9, @babel/template@npm:^7.26.9, @babel/template@npm:^7.27.0, @babel/template@npm:^7.3.3":
version: 7.27.0
resolution: "@babel/template@npm:7.27.0"
dependencies:
"@babel/code-frame": "npm:^7.26.2"
"@babel/parser": "npm:^7.26.9"
"@babel/types": "npm:^7.26.9"
checksum: 10c0/019b1c4129cc01ad63e17529089c2c559c74709d225f595eee017af227fee11ae8a97a6ab19ae6768b8aa22d8d75dcb60a00b28f52e9fa78140672d928bc1ae9
"@babel/parser": "npm:^7.27.0"
"@babel/types": "npm:^7.27.0"
checksum: 10c0/13af543756127edb5f62bf121f9b093c09a2b6fe108373887ccffc701465cfbcb17e07cf48aa7f440415b263f6ec006e9415c79dfc2e8e6010b069435f81f340
languageName: node
linkType: hard
"@babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.26.8":
version: 7.26.10
resolution: "@babel/traverse@npm:7.26.10"
"@babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.26.5, @babel/traverse@npm:^7.26.8, @babel/traverse@npm:^7.27.0":
version: 7.27.0
resolution: "@babel/traverse@npm:7.27.0"
dependencies:
"@babel/code-frame": "npm:^7.26.2"
"@babel/generator": "npm:^7.26.10"
"@babel/parser": "npm:^7.26.10"
"@babel/template": "npm:^7.26.9"
"@babel/types": "npm:^7.26.10"
"@babel/generator": "npm:^7.27.0"
"@babel/parser": "npm:^7.27.0"
"@babel/template": "npm:^7.27.0"
"@babel/types": "npm:^7.27.0"
debug: "npm:^4.3.1"
globals: "npm:^11.1.0"
checksum: 10c0/4e86bb4e3c30a6162bb91df86329df79d96566c3e2d9ccba04f108c30473a3a4fd360d9990531493d90f6a12004f10f616bf9b9229ca30c816b708615e9de2ac
checksum: 10c0/c7af29781960dacaae51762e8bc6c4b13d6ab4b17312990fbca9fc38e19c4ad7fecaae24b1cf52fb844e8e6cdc76c70ad597f90e496bcb3cc0a1d66b41a0aa5b
languageName: node
linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.26.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
version: 7.26.10
resolution: "@babel/types@npm:7.26.10"
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.0.0-beta.49, @babel/types@npm:^7.12.6, @babel/types@npm:^7.20.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.9, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4":
version: 7.27.0
resolution: "@babel/types@npm:7.27.0"
dependencies:
"@babel/helper-string-parser": "npm:^7.25.9"
"@babel/helper-validator-identifier": "npm:^7.25.9"
checksum: 10c0/7a7f83f568bfc3dfabfaf9ae3a97ab5c061726c0afa7dcd94226d4f84a81559da368ed79671e3a8039d16f12476cf110381a377ebdea07587925f69628200dac
checksum: 10c0/6f1592eabe243c89a608717b07b72969be9d9d2fce1dee21426238757ea1fa60fdfc09b29de9e48d8104311afc6e6fb1702565a9cc1e09bc1e76f2b2ddb0f6e1
languageName: node
linkType: hard