Merge remote-tracking branch 'parent/main' into kbtopic-remove-quote

This commit is contained in:
KMY 2025-04-26 08:30:17 +09:00
commit 80542ea172
76 changed files with 658 additions and 390 deletions

View file

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

View file

@ -79,7 +79,7 @@ gem 'rails-i18n', '~> 8.0'
gem 'redcarpet', '~> 3.6'
gem 'redis', '~> 4.5', require: ['redis', 'redis/connection/hiredis']
gem 'redis-namespace', '~> 1.10'
gem 'rqrcode', '~> 2.2'
gem 'rqrcode', '~> 3.0'
gem 'ruby-progressbar', '~> 1.13'
gem 'sanitize', '~> 7.0'
gem 'scenic', '~> 1.7'

View file

@ -160,7 +160,7 @@ GEM
cocoon (1.2.15)
color_diff (0.1)
concurrent-ruby (1.3.5)
connection_pool (2.5.1)
connection_pool (2.5.2)
cose (1.3.1)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 1.0)
@ -711,10 +711,10 @@ GEM
rotp (6.3.0)
rouge (4.5.1)
rpam2 (4.0.2)
rqrcode (2.2.0)
rqrcode (3.0.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rqrcode_core (~> 2.0)
rqrcode_core (2.0.0)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
@ -743,7 +743,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.2)
rubocop (1.75.2)
rubocop (1.75.3)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@ -773,7 +773,7 @@ GEM
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.38.0, < 2.0)
rubocop-rspec (3.5.0)
rubocop-rspec (3.6.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
@ -1043,7 +1043,7 @@ DEPENDENCIES
redcarpet (~> 3.6)
redis (~> 4.5)
redis-namespace (~> 1.10)
rqrcode (~> 2.2)
rqrcode (~> 3.0)
rspec-github (~> 3.0)
rspec-rails (~> 7.0)
rspec-sidekiq (~> 5.0)

View file

@ -72,6 +72,13 @@ class Api::BaseController < ApplicationController
end
end
# Redefine `require_functional!` to properly output JSON instead of HTML redirects
def require_functional!
return if current_user.functional?
require_user!
end
def render_empty
render json: {}, status: 200
end

View file

@ -18,7 +18,7 @@ class Api::V1::FeaturedTagsController < Api::BaseController
end
def destroy
RemoveFeaturedTagWorker.perform_async(current_account.id, @featured_tag.id)
RemoveFeaturedTagService.new.call(current_account, @featured_tag)
render_empty
end

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true
class Api::V1::TagsController < Api::BaseController
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, except: :show
before_action -> { doorkeeper_authorize! :follow, :write, :'write:follows' }, only: [:follow, :unfollow]
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, only: [:feature, :unfeature]
before_action :require_user!, except: :show
before_action :set_or_create_tag
@ -23,6 +24,16 @@ class Api::V1::TagsController < Api::BaseController
render json: @tag, serializer: REST::TagSerializer
end
def feature
CreateFeaturedTagService.new.call(current_account, @tag)
render json: @tag, serializer: REST::TagSerializer
end
def unfeature
RemoveFeaturedTagService.new.call(current_account, @tag)
render json: @tag, serializer: REST::TagSerializer
end
private
def set_or_create_tag

View file

@ -72,10 +72,24 @@ class ApplicationController < ActionController::Base
def require_functional!
return if current_user.functional?
if current_user.confirmed?
redirect_to edit_user_registration_path
else
redirect_to auth_setup_path
respond_to do |format|
format.any do
if current_user.confirmed?
redirect_to edit_user_registration_path
else
redirect_to auth_setup_path
end
end
format.json do
if !current_user.confirmed?
render json: { error: 'Your login is missing a confirmed e-mail address' }, status: 403
elsif !current_user.approved?
render json: { error: 'Your login is currently pending approval' }, status: 403
elsif !current_user.functional?
render json: { error: 'Your login is currently disabled' }, status: 403
end
end
end
end

View file

@ -16,7 +16,7 @@ module Localized
def requested_locale
requested_locale_name = available_locale_or_nil(params[:lang])
requested_locale_name ||= available_locale_or_nil(current_user.locale) if respond_to?(:user_signed_in?) && user_signed_in?
requested_locale_name ||= http_accept_language if ENV['DEFAULT_LOCALE'].blank?
requested_locale_name ||= http_accept_language unless ENV['FORCE_DEFAULT_LOCALE'] == 'true'
requested_locale_name
end

View file

@ -12,7 +12,7 @@ class Settings::FeaturedTagsController < Settings::BaseController
end
def create
@featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name], force: false)
@featured_tag = CreateFeaturedTagService.new.call(current_account, featured_tag_params[:name], raise_error: false)
if @featured_tag.valid?
redirect_to settings_featured_tags_path

View file

@ -4,9 +4,12 @@ import axios from 'axios';
import ready from '../mastodon/ready';
async function checkConfirmation() {
const response = await axios.get('/api/v1/emails/check_confirmation');
const response = await axios.get('/api/v1/emails/check_confirmation', {
headers: { Accept: 'application/json' },
withCredentials: true,
});
if (response.data) {
if (response.status === 200 && response.data === true) {
window.location.href = '/start';
}
}

View file

@ -1,2 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="79" height="79" viewBox="0 0 79 75"><symbol id="logo-symbol-icon"><path d="M63 45.3v-20c0-4.1-1-7.3-3.2-9.7-2.1-2.4-5-3.7-8.5-3.7-4.1 0-7.2 1.6-9.3 4.7l-2 3.3-2-3.3c-2-3.1-5.1-4.7-9.2-4.7-3.5 0-6.4 1.3-8.6 3.7-2.1 2.4-3.1 5.6-3.1 9.7v20h8V25.9c0-4.1 1.7-6.2 5.2-6.2 3.8 0 5.8 2.5 5.8 7.4V37.7H44V27.1c0-4.9 1.9-7.4 5.8-7.4 3.5 0 5.2 2.1 5.2 6.2V45.3h8ZM74.7 16.6c.6 6 .1 15.7.1 17.3 0 .5-.1 4.8-.1 5.3-.7 11.5-8 16-15.6 17.5-.1 0-.2 0-.3 0-4.9 1-10 1.2-14.9 1.4-1.2 0-2.4 0-3.6 0-4.8 0-9.7-.6-14.4-1.7-.1 0-.1 0-.1 0s-.1 0-.1 0 0 .1 0 .1 0 0 0 0c.1 1.6.4 3.1 1 4.5.6 1.7 2.9 5.7 11.4 5.7 5 0 9.9-.6 14.8-1.7 0 0 0 0 0 0 .1 0 .1 0 .1 0 0 .1 0 .1 0 .1.1 0 .1 0 .1.1v5.6s0 .1-.1.1c0 0 0 0 0 .1-1.6 1.1-3.7 1.7-5.6 2.3-.8.3-1.6.5-2.4.7-7.5 1.7-15.4 1.3-22.7-1.2-6.8-2.4-13.8-8.2-15.5-15.2-.9-3.8-1.6-7.6-1.9-11.5-.6-5.8-.6-11.7-.8-17.5C3.9 24.5 4 20 4.9 16 6.7 7.9 14.1 2.2 22.3 1c1.4-.2 4.1-1 16.5-1h.1C51.4 0 56.7.8 58.1 1c8.4 1.2 15.5 7.5 16.6 15.6Z" fill="currentColor"/></symbol><use xlink:href="#logo-symbol-icon"/></svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before After
Before After

View file

@ -412,14 +412,7 @@ class Status extends ImmutablePureComponent {
let visibilityName = status.get('limited_scope') || status.get('visibility_ex') || status.get('visibility');
if (featured) {
prepend = (
<div className='status__prepend'>
<div className='status__prepend__icon'><Icon id='thumb-tack' icon={PushPinIcon} /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = (

View file

@ -292,7 +292,7 @@
"column.local": "Local",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
"column.pins": "Pinned posts",
"column.pins": "Featured posts",
"column.public": "Federated timeline",
"column.reaction_deck": "Reaction deck",
"column_back_button.label": "Back",
@ -623,7 +623,7 @@
"keyboard_shortcuts.my_profile": "Open your profile",
"keyboard_shortcuts.notifications": "Open notifications column",
"keyboard_shortcuts.open_media": "Open media",
"keyboard_shortcuts.pinned": "Open pinned posts list",
"keyboard_shortcuts.pinned": "Open featured posts list",
"keyboard_shortcuts.profile": "Open author's profile",
"keyboard_shortcuts.reply": "Reply to post",
"keyboard_shortcuts.requests": "Open follow requests list",
@ -718,7 +718,7 @@
"navigation_bar.mutes": "Muted users",
"navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
"navigation_bar.personal": "Personal",
"navigation_bar.pins": "Pinned posts",
"navigation_bar.pins": "Featured posts",
"navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.reaction_deck": "Reaction deck",
@ -1069,8 +1069,7 @@
"status.mute": "Mute @{name}",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this post",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.pin": "Feature on profile",
"status.read_more": "Read more",
"status.reblog": "Boost",
"status.reblog_private": "Boost with original visibility",
@ -1098,7 +1097,7 @@
"status.translated_from_with": "Translated from {lang} using {provider}",
"status.uncached_media_warning": "Preview not available",
"status.unmute_conversation": "Unmute conversation",
"status.unpin": "Unpin from profile",
"status.unpin": "Don't feature on profile",
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
"subscribed_languages.save": "Save changes",
"subscribed_languages.target": "Change subscribed languages for {target}",

View file

@ -19,13 +19,16 @@
"account.block_domain": "Blokir domain {domain}",
"account.block_short": "Blokir",
"account.blocked": "Terblokir",
"account.blocking": "Memblokir",
"account.cancel_follow_request": "Batalkan permintaan ikut",
"account.copy": "Salin tautan ke profil",
"account.direct": "Sebut secara pribadi @{name}",
"account.disable_notifications": "Berhenti memberitahu saya ketika @{name} memposting",
"account.domain_blocking": "Memblokir domain",
"account.edit_profile": "Ubah profil",
"account.enable_notifications": "Beritahu saya saat @{name} memposting",
"account.endorse": "Tampilkan di profil",
"account.featured": "",
"account.featured_tags.last_status_at": "Kiriman terakhir pada {date}",
"account.featured_tags.last_status_never": "Tidak ada kiriman",
"account.follow": "Ikuti",
@ -36,6 +39,7 @@
"account.following": "Mengikuti",
"account.following_counter": "{count, plural, other {{counter} following}}",
"account.follows.empty": "Pengguna ini belum mengikuti siapa pun.",
"account.follows_you": "Mengikuti Anda",
"account.go_to_profile": "Buka profil",
"account.hide_reblogs": "Sembunyikan boosts dari @{name}",
"account.in_memoriam": "Mengenang.",

View file

@ -19,10 +19,12 @@
"account.block_domain": "{domain} 도메인 차단",
"account.block_short": "차단",
"account.blocked": "차단함",
"account.blocking": "차단함",
"account.cancel_follow_request": "팔로우 취소",
"account.copy": "프로필 링크 복사",
"account.direct": "@{name} 님에게 개인적으로 멘션",
"account.disable_notifications": "@{name} 의 게시물 알림 끄기",
"account.domain_blocking": "도메인 차단함",
"account.edit_profile": "프로필 편집",
"account.enable_notifications": "@{name} 의 게시물 알림 켜기",
"account.endorse": "프로필에 추천하기",
@ -39,6 +41,7 @@
"account.following": "팔로잉",
"account.following_counter": "{count, plural, other {팔로잉 {counter}명}}",
"account.follows.empty": "이 사용자는 아직 아무도 팔로우하고 있지 않습니다.",
"account.follows_you": "나를 팔로우",
"account.go_to_profile": "프로필로 이동",
"account.hide_reblogs": "@{name}의 부스트를 숨기기",
"account.in_memoriam": "고인의 계정입니다.",
@ -53,13 +56,17 @@
"account.mute_notifications_short": "알림 뮤트",
"account.mute_short": "뮤트",
"account.muted": "뮤트됨",
"account.muting": "뮤트함",
"account.mutual": "서로 팔로우",
"account.no_bio": "제공된 설명이 없습니다.",
"account.open_original_page": "원본 페이지 열기",
"account.posts": "게시물",
"account.posts_with_replies": "게시물과 답장",
"account.remove_from_followers": "팔로워에서 {name} 제거",
"account.report": "@{name} 신고",
"account.requested": "승인 대기 중. 클릭해서 취소하기",
"account.requested_follow": "{name} 님이 팔로우 요청을 보냈습니다",
"account.requests_to_follow_you": "팔로우 요청",
"account.share": "@{name}의 프로필 공유",
"account.show_reblogs": "@{name}의 부스트 보기",
"account.statuses_counter": "{count, plural, other {게시물 {counter}개}}",
@ -227,6 +234,9 @@
"confirmations.redraft.confirm": "삭제하고 다시 쓰기",
"confirmations.redraft.message": "정말로 이 게시물을 삭제하고 다시 쓰시겠습니까? 해당 게시물에 대한 부스트와 좋아요를 잃게 되고 원본에 대한 답장은 연결 되지 않습니다.",
"confirmations.redraft.title": "삭제하고 다시 작성할까요?",
"confirmations.remove_from_followers.confirm": "팔로워 제거",
"confirmations.remove_from_followers.message": "{name} 님이 나를 팔로우하지 않게 됩니다. 계속할까요?",
"confirmations.remove_from_followers.title": "팔로워를 제거할까요?",
"confirmations.reply.confirm": "답글",
"confirmations.reply.message": "지금 답장하면 작성 중인 메시지를 덮어쓰게 됩니다. 정말 진행합니까?",
"confirmations.reply.title": "게시물을 덮어쓸까요?",
@ -294,6 +304,9 @@
"emoji_button.search_results": "검색 결과",
"emoji_button.symbols": "기호",
"emoji_button.travel": "여행과 장소",
"empty_column.account_featured.me": "아직 아무 것도 추천하지 않았습니다. 게시물, 자주 사용하는 해시태그, 친구의 계정까지 내 계정에서 추천할 수 있다는 것을 알고 계신가요?",
"empty_column.account_featured.other": "{acct} 님은 아직 아무 것도 추천하지 않았습니다. 게시물, 자주 사용하는 해시태그, 친구의 계정까지 내 계정에서 추천할 수 있다는 것을 알고 계신가요?",
"empty_column.account_featured_other.unknown": "이 계정은 아직 아무 것도 추천하지 않았습니다.",
"empty_column.account_hides_collections": "이 사용자는 이 정보를 사용할 수 없도록 설정했습니다",
"empty_column.account_suspended": "계정 정지됨",
"empty_column.account_timeline": "이곳에는 게시물이 없습니다!",

View file

@ -6,10 +6,10 @@
"about.domain_blocks.preamble": "Mastodon parasti ļauj apskatīt saturu un mijiedarboties ar lietotājiem no jebkura cita fediversa servera. Šie ir izņēmumi, kas veikti tieši šajā serverī.",
"about.domain_blocks.silenced.explanation": "Parasti tu neredzēsi profilus un saturu no šī servera, ja vien tu nepārprotami izvēlēsies to pārskatīt vai sekot.",
"about.domain_blocks.silenced.title": "Ierobežotie",
"about.domain_blocks.suspended.explanation": "Nekādi dati no šī servera netiks apstrādāti, uzglabāti vai apmainīti, padarot neiespējamu mijiedarbību vai saziņu ar lietotājiem no šī servera.",
"about.domain_blocks.suspended.explanation": "Nekādi dati no šī servera netiks apstrādāti, uzglabāti vai apmainīti, padarot neiespējamu jebkādu mijiedarbību vai saziņu ar šī servera lietotājiem.",
"about.domain_blocks.suspended.title": "Apturētie",
"about.not_available": "Šī informācija nav padarīta pieejama šajā serverī.",
"about.powered_by": "Decentralizētu sociālo tīklu nodrošina {mastodon}",
"about.powered_by": "Decentralizētu sabiedrisko tīklu darbina {mastodon}",
"about.rules": "Servera noteikumi",
"account.account_note_header": "Personīga piezīme",
"account.add_or_remove_from_list": "Pievienot vai Noņemt no sarakstiem",
@ -22,12 +22,14 @@
"account.cancel_follow_request": "Atsaukt sekošanas pieprasījumu",
"account.copy": "Ievietot saiti uz profilu starpliktuvē",
"account.direct": "Pieminēt @{name} privāti",
"account.disable_notifications": "Pārtraukt man paziņot, kad @{name} publicē ierakstu",
"account.disable_notifications": "Pārtraukt man paziņot, kad @{name} izveido ierakstu",
"account.edit_profile": "Labot profilu",
"account.enable_notifications": "Paziņot man, kad @{name} publicē ierakstu",
"account.enable_notifications": "Paziņot man, kad @{name} izveido ierakstu",
"account.endorse": "Izcelts profilā",
"account.featured_tags.last_status_at": "Beidzamā ziņa {date}",
"account.featured_tags.last_status_never": "Ierakstu nav",
"account.featured.hashtags": "Tēmturi",
"account.featured.posts": "Ieraksti",
"account.featured_tags.last_status_at": "Pēdējais ieraksts {date}",
"account.featured_tags.last_status_never": "Nav ierakstu",
"account.follow": "Sekot",
"account.follow_back": "Sekot atpakaļ",
"account.followers": "Sekotāji",
@ -36,6 +38,7 @@
"account.following": "Seko",
"account.following_counter": "{count, plural, one {seko {counter}} other {seko {counter}}}",
"account.follows.empty": "Šis lietotājs pagaidām nevienam neseko.",
"account.follows_you": "Seko tev",
"account.go_to_profile": "Doties uz profilu",
"account.hide_reblogs": "Paslēpt @{name} pastiprinātos ierakstus",
"account.in_memoriam": "Piemiņai.",
@ -47,17 +50,19 @@
"account.mention": "Pieminēt @{name}",
"account.moved_to": "{name} norādīja, ka viņu jaunais konts tagad ir:",
"account.mute": "Apklusināt @{name}",
"account.mute_notifications_short": "Izslēgt paziņojumu skaņu",
"account.mute_notifications_short": "Apklusināt paziņojumus",
"account.mute_short": "Apklusināt",
"account.muted": "Apklusināts",
"account.mutual": "Jūs sekojat viens otram",
"account.no_bio": "Apraksts nav sniegts.",
"account.open_original_page": "Atvērt pirmavota lapu",
"account.posts": "Ieraksti",
"account.posts_with_replies": "Ieraksti un atbildes",
"account.remove_from_followers": "Dzēst sekotāju {name}",
"account.report": "Sūdzēties par @{name}",
"account.report": "Ziņot par @{name}",
"account.requested": "Gaida apstiprinājumu. Nospied, lai atceltu sekošanas pieparasījumu",
"account.requested_follow": "{name} nosūtīja Tev sekošanas pieprasījumu",
"account.requests_to_follow_you": "Sekošanas pieprasījumi",
"account.share": "Dalīties ar @{name} profilu",
"account.show_reblogs": "Parādīt @{name} pastiprinātos ierakstus",
"account.statuses_counter": "{count, plural, zero {{counter} ierakstu} one {{counter} ieraksts} other {{counter} ieraksti}}",
@ -67,7 +72,7 @@
"account.unendorse": "Neizcelt profilā",
"account.unfollow": "Pārstāt sekot",
"account.unmute": "Noņemt apklusinājumu @{name}",
"account.unmute_notifications_short": "Ieslēgt paziņojumu skaņu",
"account.unmute_notifications_short": "Atcelet paziņojumu apklusināšanu",
"account.unmute_short": "Noņemt apklusinājumu",
"account_note.placeholder": "Noklikšķini, lai pievienotu piezīmi",
"admin.dashboard.daily_retention": "Lietotāju saglabāšanas rādītājs dienā pēc reģistrēšanās",
@ -91,12 +96,13 @@
"alt_text_modal.describe_for_people_with_visual_impairments": "Aprakstīt šo cilvēkiem ar redzes traucējumiem…",
"alt_text_modal.done": "Gatavs",
"announcement.announcement": "Paziņojums",
"annual_report.summary.archetype.lurker": "Glūņa",
"annual_report.summary.archetype.oracle": "Orākuls",
"annual_report.summary.archetype.replier": "Sabiedriskais tauriņš",
"annual_report.summary.followers.followers": "sekotāji",
"annual_report.summary.followers.total": "pavisam {count}",
"annual_report.summary.here_it_is": "Šeit ir {year}. gada pārskats:",
"annual_report.summary.highlighted_post.by_favourites": "izlasēs visvairāk ievietotais ieraksts",
"annual_report.summary.highlighted_post.by_favourites": "izlasēm visvairāk pievienotais ieraksts",
"annual_report.summary.highlighted_post.by_reblogs": "vispastiprinātākais ieraksts",
"annual_report.summary.highlighted_post.by_replies": "ieraksts ar vislielāko atbilžu skaitu",
"annual_report.summary.highlighted_post.possessive": "{name}",
@ -109,16 +115,17 @@
"annual_report.summary.thanks": "Paldies, ka esi daļa no Mastodon!",
"attachments_list.unprocessed": "(neapstrādāti)",
"audio.hide": "Slēpt audio",
"block_modal.remote_users_caveat": "Mēs vaicāsim serverim {domain} ņemt vērā Tavu lēmumu. Tomēr atbilstība nav nodrošināta, jo atsevišķi serveri var apstrādāt bloķēšanu citādi. Publiski ieraksti joprojām var būt redzami lietotājiem, kuri nav pieteikušies.",
"block_modal.remote_users_caveat": "Mēs vaicāsim serverim {domain} ņemt vērā Tavu lēmumu. Tomēr atbilstība nav nodrošināta, jo atsevišķi serveri liegšanu var apstrādāt citādi. Publiski ieraksti joprojām var būt redzami lietotājiem, kuri nav pieteikušies.",
"block_modal.show_less": "Rādīt mazāk",
"block_modal.show_more": "Parādīt mazāk",
"block_modal.they_cant_mention": "Nevar Tevi pieminēt vai sekot Tev.",
"block_modal.they_cant_see_posts": "Nevar redzēt Tavus ierakstus, un Tu neredzēsi lietotāja.",
"block_modal.they_cant_see_posts": "Lietotajs nevarēs redzēt Tavus ierakstus, un Tu neredzēsi lietotāja.",
"block_modal.title": "Bloķēt lietotāju?",
"block_modal.you_wont_see_mentions": "Tu neredzēsi ierakstus, kuros ir minēts šis lietotājs.",
"boost_modal.combo": "Nospied {combo}, lai nākamreiz šo izlaistu",
"boost_modal.reblog": "Pastiprināt ierakstu?",
"boost_modal.undo_reblog": "Atcelt ieraksta pastiprināšanu?",
"bundle_column_error.copy_stacktrace": "Kopēt kļūdu ziņojumu",
"bundle_column_error.copy_stacktrace": "Ievietot kļūdu ziņojumu starpliktuvē",
"bundle_column_error.error.body": "Pieprasīto lapu nevarēja atveidot. Tas varētu būt saistīts ar kļūdu mūsu kodā, vai tā ir pārlūkprogrammas saderības problēma.",
"bundle_column_error.error.title": "Ak vai!",
"bundle_column_error.network.body": "Mēģinot ielādēt šo lapu, radās kļūda. Tas varētu būt saistīts ar īslaicīgu interneta savienojuma vai šī servera problēmu.",
@ -167,9 +174,9 @@
"community.column_settings.remote_only": "Tikai attālinātie",
"compose.language.change": "Mainīt valodu",
"compose.language.search": "Meklēt valodas...",
"compose.published.body": "Ieraksts izveidots.",
"compose.published.body": "Ieraksts pievienots.",
"compose.published.open": "Atvērt",
"compose.saved.body": "Ziņa saglabāta.",
"compose.saved.body": "Ieraksts saglabāts.",
"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 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.",
@ -193,7 +200,7 @@
"confirmation_modal.cancel": "Atcelt",
"confirmations.block.confirm": "Bloķēt",
"confirmations.delete.confirm": "Dzēst",
"confirmations.delete.message": "Vai tiešām vēlies dzēst šo ierakstu?",
"confirmations.delete.message": "Vai tiešām izdzēst šo ierakstu?",
"confirmations.delete.title": "Izdzēst ierakstu?",
"confirmations.delete_list.confirm": "Dzēst",
"confirmations.delete_list.message": "Vai tiešām neatgriezeniski izdzēst šo sarakstu?",
@ -206,14 +213,15 @@
"confirmations.follow_to_list.confirm": "Sekot un pievienot sarakstam",
"confirmations.follow_to_list.message": "Ir jāseko {name}, lai pievienotu sarakstam.",
"confirmations.follow_to_list.title": "Sekot lietotājam?",
"confirmations.logout.confirm": "Iziet",
"confirmations.logout.message": "Vai tiešām vēlies izrakstīties?",
"confirmations.logout.confirm": "Atteikties",
"confirmations.logout.message": "Vai tiešām atteikties?",
"confirmations.logout.title": "Atteikties?",
"confirmations.missing_alt_text.message": "Tavs ieraksts satur informācijas nesējus bez paskaidrojošā teksta. Aprakstu pievienošana palīdz padarīt saturu pieejamāku vairāk cilvēku.",
"confirmations.missing_alt_text.secondary": "Vienalga iesūtīt",
"confirmations.mute.confirm": "Apklusināt",
"confirmations.redraft.confirm": "Dzēst un pārrakstīt",
"confirmations.redraft.message": "Vai tiešām vēlies izdzēst šo ierakstu un veidot jaunu tā uzmetumu? Pievienošana izlasēs un pastiprinājumi tiks zaudēti, un sākotnējā ieraksta atbildes paliks bez saiknes ar to.",
"confirmations.redraft.title": "Dzēst un rakstīt vēlreiz?",
"confirmations.redraft.title": "Izdzēst un rakstīt ierakstu no jauna?",
"confirmations.remove_from_followers.confirm": "Dzēst sekotāju",
"confirmations.remove_from_followers.message": "{name} pārstās sekot jums. Vai tiešām vēlaties turpināt?",
"confirmations.remove_from_followers.title": "Vai dzēst sekotāju?",
@ -241,14 +249,23 @@
"disabled_account_banner.text": "Tavs konts {disabledAccount} pašlaik ir atspējots.",
"dismissable_banner.community_timeline": "Šie ir jaunākie publiskie ieraksti no cilvēkiem, kuru konti ir mitināti {domain}.",
"dismissable_banner.dismiss": "Atcelt",
"dismissable_banner.explore_links": "Šie jaunumi šodien Fediversā tiek visvairāk kopīgoti. Jaunākas ziņas, kuras pievienoši vairāki dažādi cilvēki, tiek novietotas augstāk.",
"dismissable_banner.public_timeline": "Šie ir jaunākie Fediverse lietotāju publiskie ieraksti, kuriem {domain} seko cilvēki.",
"domain_block_modal.block": "Bloķēt serveri",
"domain_block_modal.block_account_instead": "Tā vietā liegt @{name}",
"domain_block_modal.they_cant_follow": "Neviens šajā serverī nevar Tev sekot.",
"domain_block_modal.they_wont_know": "Viņi nezinās, ka tikuši bloķēti.",
"domain_block_modal.title": "Bloķēt domēnu?",
"domain_pill.activitypub_lets_connect": "Tas ļauj savienoties un mijiedarboties ar cilvēkiem ne tikai no Mastodon, bet arī starp dažādām sabiedriskajām lietotnēm.",
"domain_pill.activitypub_like_language": "ActivityPub ir kā valoda, kurā Mastodon sazināš ar citiem sabiedriskajiem tīkliem.",
"domain_pill.server": "Serveris",
"domain_pill.their_handle": "Turis:",
"domain_pill.username": "Lietotājvārds",
"embed.instructions": "Iestrādā šo ziņu savā mājaslapā, kopējot zemāk redzamo kodu.",
"domain_pill.whats_in_a_handle": "Kas ir turī?",
"domain_pill.who_they_are": "Tā kā turi norāda, kas kāds ir un kur viņi ir atrodami, Tu vari mijiedarboties ar cilvēkiem viscaur sabiedriskajā tīklā no <button>ar ActivityPub darbinātām platformām</button>.",
"domain_pill.who_you_are": "Tā kā Tavs turis norāda, kas Tu esi un kur atrodies, cilvēki var mijiedarboties ar Tevi viscaur sabiedriskajā tīklā no <button>ar ActivityPub darbinātām platformām</button>.",
"domain_pill.your_handle": "Tavs turis:",
"embed.instructions": "Iekļauj šo ierakstu savā tīmekļvietnē, ievietojot zemāk redzamo kodu starpliktuvē!",
"embed.preview": "Tas izskatīsies šādi:",
"emoji_button.activity": "Aktivitāte",
"emoji_button.clear": "Notīrīt",
@ -267,20 +284,20 @@
"emoji_button.travel": "Ceļošana un vietas",
"empty_column.account_hides_collections": "Šis lietotājs ir izvēlējies nedarīt šo informāciju pieejamu",
"empty_column.account_suspended": "Konta darbība ir apturēta",
"empty_column.account_timeline": "Šeit ziņojumu nav!",
"empty_column.account_timeline": "Šeit nav ierakstu.",
"empty_column.account_unavailable": "Profils nav pieejams",
"empty_column.blocks": "Pašreiz tu neesi nevienu bloķējis.",
"empty_column.bookmarked_statuses": "Pašlaik Tev nav neviena grāmatzīmēs pievienota ieraksta. Kad tādu pievienosi, tas parādīsies šeit.",
"empty_column.community": "Vietējā laika līnija ir tukša. Uzraksti kaut ko publiski, lai viss notiktu!",
"empty_column.community": "Vietējā laika līnija ir tukša. Uzraksti kaut ko publiski, lai iekustinātu visu!",
"empty_column.direct": "Tev vēl nav privātu pieminēšanu. Kad Tu nosūtīsi vai saņemsi kādu, tā pārādīsies šeit.",
"empty_column.domain_blocks": "Vēl nav neviena bloķēta domēna.",
"empty_column.explore_statuses": "Pašlaik nav nekā aktuāla. Ieskaties šeit vēlāk!",
"empty_column.favourited_statuses": "Tev vēl nav iecienītāko ierakstu. Kad pievienosi kādu izlasei, tas tiks parādīts šeit.",
"empty_column.favourites": "Šo ziņu neviens vēl nav pievienojis izlasei. Kad kāds to izdarīs, tas parādīsies šeit.",
"empty_column.favourited_statuses": "Tev vēl nav izlasei pievienotu ierakstu. Kad pievienosi kādu, tas tiks parādīts šeit.",
"empty_column.favourites": "Šo ierakstu vēl neviens nav pievienojis izlasei. Kad kāds to izdarīs, šeit parādīsies ieraksti.",
"empty_column.follow_requests": "Šobrīd Tev nav sekošanas pieprasījumu. Kad saņemsi kādu, tas parādīsies šeit.",
"empty_column.followed_tags": "Tu vēl neseko nevienam tēmturim. Kad to izdarīsi, tie tiks parādīti šeit.",
"empty_column.hashtag": "Ar šo tēmturi nekas nav atrodams.",
"empty_column.home": "Tava mājas laikjosla ir tukša. Seko vairāk cilvēkiem, lai to piepildītu!",
"empty_column.home": "Tava mājas laika līnija ir tukša. Seko vairāk cilvēkiem, lai to piepildītu!",
"empty_column.list": "Pagaidām šajā sarakstā nekā nav. Kad šī saraksta dalībnieki ievietos jaunus ierakstus, tie parādīsies šeit.",
"empty_column.mutes": "Neviens lietotājs vēl nav apklusināts.",
"empty_column.notifications": "Tev vēl nav paziņojumu. Kad citi cilvēki ar Tevi mijiedarbosies, Tu to redzēsi šeit.",
@ -303,15 +320,15 @@
"filter_modal.added.review_and_configure": "Lai pārskatītu un tālāk konfigurētu šo filtru kategoriju, dodies uz {settings_link}.",
"filter_modal.added.review_and_configure_title": "Filtra iestatījumi",
"filter_modal.added.settings_link": "iestatījumu lapu",
"filter_modal.added.short_explanation": ī ziņa ir pievienota šai filtra kategorijai: {title}.",
"filter_modal.added.short_explanation": is ieraksts tika pievienots šai atlasīšanas kategorijai: {title}.",
"filter_modal.added.title": "Filtrs pievienots!",
"filter_modal.select_filter.context_mismatch": "neattiecas uz šo kontekstu",
"filter_modal.select_filter.expired": "beidzies",
"filter_modal.select_filter.prompt_new": "Jauna kategorija: {name}",
"filter_modal.select_filter.search": "Meklēt vai izveidot",
"filter_modal.select_filter.subtitle": "Izmanto esošu kategoriju vai izveido jaunu",
"filter_modal.select_filter.title": "Filtrēt šo ziņu",
"filter_modal.title.status": "Filtrēt ziņu",
"filter_modal.select_filter.title": "Atlasīt šo ierakstu",
"filter_modal.title.status": "Atlasīt ziņu",
"filtered_notifications_banner.title": "Filtrētie paziņojumi",
"firehose.all": "Visi",
"firehose.local": "Šis serveris",
@ -368,12 +385,21 @@
"home.pending_critical_update.title": "Ir pieejams būtisks drošības atjauninājums.",
"home.show_announcements": "Rādīt paziņojumus",
"ignore_notifications_modal.ignore": "Neņemt vērā paziņojumus",
"ignore_notifications_modal.not_following_title": "Neņemt vērā paziņojumus no cilvēkiem, kuriem neseko?",
"interaction_modal.action.favourite": "Lai turpinātu, nepieciešams pievienot sava konta izlasei.",
"interaction_modal.action.follow": "Lai turpinātu, nepieciešams sekot no sava konta.",
"interaction_modal.action.reblog": "Lai turpinātu, nepieciešams pastiprināt no sava konta.",
"interaction_modal.action.reply": "Lai turpinātu, nepieciešams atbildēt no sava konta.",
"interaction_modal.action.vote": "Lai turpinātu, nepieciešams balsot no sava konta.",
"interaction_modal.go": "Aiziet",
"interaction_modal.no_account_yet": "Vēl nav konta?",
"interaction_modal.on_another_server": "Citā serverī",
"interaction_modal.on_this_server": "Šajā serverī",
"interaction_modal.title.favourite": "Pievienot {name} ziņu izlasei",
"interaction_modal.title.favourite": "Pievienot {name} ierakstu izlasei",
"interaction_modal.title.follow": "Sekot {name}",
"interaction_modal.title.reblog": "Pastiprināt {name} ierakstu",
"interaction_modal.title.reply": "Atbildēt uz {name} ziņu",
"interaction_modal.title.reply": "Atbildēt uz {name} ierakstu",
"interaction_modal.username_prompt": "Piem., {example}",
"intervals.full.days": "{number, plural, one {# diena} other {# dienas}}",
"intervals.full.hours": "{number, plural, one {# stunda} other {# stundas}}",
"intervals.full.minutes": "{number, plural, one {# minūte} other {# minūtes}}",
@ -385,8 +411,8 @@
"keyboard_shortcuts.description": "Apraksts",
"keyboard_shortcuts.direct": "lai atvērtu privāto pieminējumu sleju",
"keyboard_shortcuts.down": "Pārvietoties lejup sarakstā",
"keyboard_shortcuts.enter": "Atvērt ziņu",
"keyboard_shortcuts.favourite": "Pievienot izlasei",
"keyboard_shortcuts.enter": "Atvērt ierakstu",
"keyboard_shortcuts.favourite": "Pievienot ierakstu izlasei",
"keyboard_shortcuts.favourites": "Atvērt izlašu sarakstu",
"keyboard_shortcuts.federated": "Atvērt apvienoto laika līniju",
"keyboard_shortcuts.heading": "Īsinājumtaustiņi",
@ -399,7 +425,7 @@
"keyboard_shortcuts.my_profile": "Atvērt savu profilu",
"keyboard_shortcuts.notifications": "Atvērt paziņojumu kolonnu",
"keyboard_shortcuts.open_media": "Atvērt multividi",
"keyboard_shortcuts.pinned": "Atvērt piesprausto ziņu sarakstu",
"keyboard_shortcuts.pinned": "Atvērt piesprausto ierakstu sarakstu",
"keyboard_shortcuts.profile": "Atvērt autora profilu",
"keyboard_shortcuts.reply": "Atbildēt",
"keyboard_shortcuts.requests": "Atvērt sekošanas pieprasījumu sarakstu",
@ -408,7 +434,7 @@
"keyboard_shortcuts.start": "Atvērt kolonnu “Darba sākšana”",
"keyboard_shortcuts.toggle_hidden": "Rādīt/slēpt tekstu aiz satura brīdinājuma",
"keyboard_shortcuts.toggle_sensitivity": "Rādīt/slēpt multividi",
"keyboard_shortcuts.toot": "Sākt jaunu ziņu",
"keyboard_shortcuts.toot": "Uzsākt jaunu ierakstu",
"keyboard_shortcuts.unfocus": "Atfokusēt veidojamā teksta/meklēšanas lauku",
"keyboard_shortcuts.up": "Pārvietoties augšup sarakstā",
"lightbox.close": "Aizvērt",
@ -444,7 +470,7 @@
"navigation_bar.blocks": "Bloķētie lietotāji",
"navigation_bar.bookmarks": "Grāmatzīmes",
"navigation_bar.community_timeline": "Vietējā laika līnija",
"navigation_bar.compose": "Veidot jaunu ziņu",
"navigation_bar.compose": "Izveidot jaunu ierakstu",
"navigation_bar.direct": "Privātas pieminēšanas",
"navigation_bar.discover": "Atklāt",
"navigation_bar.domain_blocks": "Bloķētie domēni",
@ -460,15 +486,17 @@
"navigation_bar.mutes": "Apklusinātie lietotāji",
"navigation_bar.opened_in_classic_interface": "Ieraksti, konti un citas noteiktas lapas pēc noklusējuma tiek atvērtas klasiskajā tīmekļa saskarnē.",
"navigation_bar.personal": "Personīgie",
"navigation_bar.pins": "Piespraustās ziņas",
"navigation_bar.pins": "Piespraustie ieraksti",
"navigation_bar.preferences": "Iestatījumi",
"navigation_bar.public_timeline": "Apvienotā laika līnija",
"navigation_bar.search": "Meklēt",
"navigation_bar.security": "Drošība",
"not_signed_in_indicator.not_signed_in": "Ir jāpiesakās, lai piekļūtu šim resursam.",
"notification.admin.report": "{name} ziņoja par {target}",
"notification.admin.report_account": "{name} ziņoja par {count, plural, one {# ierakstu} other {# ierakstiem}} no {target} ar iemeslu: {category}",
"notification.admin.report_statuses": "{name} ziņoja par {target} ar iemeslu: {category}",
"notification.admin.sign_up": "{name} pierakstījās",
"notification.favourite": "{name} pievienoja tavu ziņu izlasei",
"notification.favourite": "{name} pievienoja izlasei Tavu ierakstu",
"notification.follow": "{name} uzsāka Tev sekot",
"notification.follow_request": "{name} nosūtīja Tev sekošanas pieprasījumu",
"notification.moderation-warning.learn_more": "Uzzināt vairāk",
@ -484,7 +512,7 @@
"notification.reblog": "{name} pastiprināja Tavu ierakstu",
"notification.relationships_severance_event": "Zaudēti savienojumi ar {name}",
"notification.relationships_severance_event.learn_more": "Uzzināt vairāk",
"notification.status": "{name} tikko publicēja",
"notification.status": "{name} tikko pievienoja ierakstu",
"notification.update": "{name} laboja ierakstu",
"notification_requests.accept": "Pieņemt",
"notification_requests.dismiss": "Noraidīt",
@ -586,15 +614,15 @@
"reply_indicator.cancel": "Atcelt",
"reply_indicator.poll": "Aptauja",
"report.block": "Bloķēt",
"report.block_explanation": "Tu neredzēsi viņu ierakstus. Viņi nevarēs redzēt Tavus ierakstus vai sekot tev. Viņi varēs saprast, ka ir bloķēti.",
"report.block_explanation": "Tu neredzēsi viņu ierakstus. Viņi nevarēs redzēt Tavus ierakstus vai sekot tev. Viņi varēs saprast, ka ir liegti.",
"report.categories.legal": "Tiesisks",
"report.categories.other": "Citi",
"report.categories.spam": "Spams",
"report.categories.spam": "Mēstule",
"report.categories.violation": "Saturs pārkāpj vienu vai vairākus servera noteikumus",
"report.category.subtitle": "Izvēlieties labāko atbilstību",
"report.category.title": "Pastāsti mums, kas notiek ar šo {type}",
"report.category.title_account": "profilu",
"report.category.title_status": "ziņu",
"report.category.title_status": "ierakstu",
"report.close": "Darīts",
"report.comment.title": "Vai, tavuprāt, mums vēl būtu kas jāzina?",
"report.forward": "Pārsūtīt {target}",
@ -609,7 +637,7 @@
"report.reasons.legal_description": "Tu uzskati, ka tas pārkāpj tavus vai servera valsts likumus",
"report.reasons.other": "Tas ir kaut kas cits",
"report.reasons.other_description": "Šī sūdzība neatbilst pārējām kategorijām",
"report.reasons.spam": "Tas ir spams",
"report.reasons.spam": "Tā ir mēstule",
"report.reasons.spam_description": "Ļaunprātīgas saites, viltus iesaistīšana vai atkārtotas atbildes",
"report.reasons.violation": "Tas pārkāpj servera noteikumus",
"report.reasons.violation_description": "Tu zini, ka tas pārkāpj īpašus noteikumus",
@ -621,15 +649,19 @@
"report.target": "Ziņošana par: {target}",
"report.thanks.take_action": "Šeit ir iespējas, lai pārvaldītu Mastodon redzamo saturu:",
"report.thanks.take_action_actionable": "Kamēr mēs to izskatām, tu vari veikt darbības pret @{name}:",
"report.thanks.title": "Vai nevēlies to redzēt?",
"report.thanks.title": "Nevēlies to redzēt?",
"report.thanks.title_actionable": "Paldies, ka ziņoji, mēs to izskatīsim.",
"report.unfollow": "Pārtraukt sekot @{name}",
"report.unfollow_explanation": "Tu seko šim kontam. Lai vairs neredzētu tā ierakstus savā mājas plūsmā, pārtrauc sekot tam!",
"report_notification.attached_statuses": "Pievienoti {count, plural,one {{count} sūtījums} other {{count} sūtījumi}}",
"report_notification.attached_statuses": "{count, plural, zero {Pievienoti {count} ierakstu} one {Pievienots {count} ieraksts} other {Pievienoti {count} ieraksti}}",
"report_notification.categories.legal": "Tiesisks",
"report_notification.categories.legal_sentence": "nelikumīgs saturs",
"report_notification.categories.other": "Cita",
"report_notification.categories.spam": "Spams",
"report_notification.categories.other_sentence": "cits",
"report_notification.categories.spam": "Mēstule",
"report_notification.categories.spam_sentence": "mēstule",
"report_notification.categories.violation": "Noteikumu pārkāpums",
"report_notification.categories.violation_sentence": "noteikumu pārkāpums",
"report_notification.open": "Atvērt ziņojumu",
"search.no_recent_searches": "Nav nesen veiktu meklējumu",
"search.placeholder": "Meklēšana",
@ -657,6 +689,7 @@
"server_banner.administered_by": "Pārvalda:",
"server_banner.server_stats": "Servera statistika:",
"sign_in_banner.create_account": "Izveidot kontu",
"sign_in_banner.follow_anyone": "Seko ikvienam Fediversā un redzi visu pievienošanas secībā! Nekādu algoritmu, reklāmu vai klikšķēsmu.",
"sign_in_banner.sign_in": "Pieteikties",
"sign_in_banner.sso_redirect": "Piesakies vai Reģistrējies",
"status.admin_account": "Atvērt @{name} satura pārraudzības saskarni",
@ -665,7 +698,7 @@
"status.block": "Bloķēt @{name}",
"status.bookmark": "Grāmatzīme",
"status.cancel_reblog_private": "Nepastiprināt",
"status.cannot_reblog": "Šo ziņu nevar izcelt",
"status.cannot_reblog": "Šo ierakstu nevar pastiprināt",
"status.continued_thread": "Turpināts pavediens",
"status.copy": "Ievietot ieraksta saiti starpliktuvē",
"status.delete": "Dzēst",
@ -676,8 +709,8 @@
"status.edited": "Pēdējoreiz labots {date}",
"status.edited_x_times": "Labots {count, plural, zero {{count} reižu} one {{count} reizi} other {{count} reizes}}",
"status.favourite": "Izlasē",
"status.favourites": "{count, plural, zero {izlasēs} one {izlasē} other {izlasēs}}",
"status.filter": "Filtrē šo ziņu",
"status.favourites": "{count, plural, one {izlasē} other {izlasēs}}",
"status.filter": "Atlasīt šo ierakstu",
"status.history.created": "{name} izveidoja {date}",
"status.history.edited": "{name} laboja {date}",
"status.load_more": "Ielādēt vairāk",
@ -688,7 +721,7 @@
"status.more": "Vairāk",
"status.mute": "Apklusināt @{name}",
"status.mute_conversation": "Apklusināt sarunu",
"status.open": "Paplašināt šo ziņu",
"status.open": "Izvērst šo ierakstu",
"status.pin": "Piespraust profilam",
"status.pinned": "Piesprausts ieraksts",
"status.read_more": "Lasīt vairāk",
@ -696,7 +729,7 @@
"status.reblog_private": "Pastiprināt ar sākotnējo redzamību",
"status.reblogged_by": "{name} pastiprināja",
"status.reblogs": "{count, plural, zero {pastiprinājumu} one {pastiprinājums} other {pastiprinājumi}}",
"status.reblogs.empty": "Neviens šo ierakstu vēl nav pastiprinājis. Kad būs, tie parādīsies šeit.",
"status.reblogs.empty": "Neviens vēl nav pastiprinājis šo ierakstu. Kad kāds to izdarīs, šeit tiks parādīti lietotāji.",
"status.redraft": "Dzēst un pārrakstīt",
"status.remove_bookmark": "Noņemt grāmatzīmi",
"status.replied_to": "Atbildēja {name}",
@ -708,13 +741,13 @@
"status.show_less_all": "Rādīt mazāk visiem",
"status.show_more_all": "Rādīt vairāk visiem",
"status.show_original": "Rādīt pirmavotu",
"status.title.with_attachments": "{user} publicējis {attachmentCount, plural, one {pielikumu} other {{attachmentCount} pielikumus}}",
"status.title.with_attachments": "{user} pievienoja {attachmentCount, plural, zero {{attachmentCount} pielikumu} one {{attachmentCount} pielikumu} other {{attachmentCount} pielikumus}}",
"status.translate": "Tulkot",
"status.translated_from_with": "Tulkots no {lang} izmantojot {provider}",
"status.uncached_media_warning": "Priekšskatījums nav pieejams",
"status.unmute_conversation": "Noņemt sarunas apklusinājumu",
"status.unpin": "Noņemt profila piespraudumu",
"subscribed_languages.lead": "Pēc izmaiņu veikšanas Tavā mājas un sarakstu laika līnijā tiks rādīti tikai tie ieraksti atlasītajās valodās. Neatlasīt nevienu, lai saņemtu ierakstus visās valodās.",
"subscribed_languages.lead": "Pēc izmaiņu veikšanas Tavā mājas un sarakstu laika līnijā tiks rādīti tikai ieraksti atlasītajās valodās. Neatlasīt nevienu, lai saņemtu ierakstus visās valodās.",
"subscribed_languages.save": "Saglabāt izmaiņas",
"subscribed_languages.target": "Mainīt abonētās valodas priekš {target}",
"tabs_bar.home": "Sākums",

View file

@ -98,8 +98,8 @@
"alt_text_modal.add_text_from_image": "Добавить текст из изображения",
"alt_text_modal.cancel": "Отмена",
"alt_text_modal.change_thumbnail": "Изменить обложку",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Опишите то, что слышите, для людей с нарушениями слуха…",
"alt_text_modal.describe_for_people_with_visual_impairments": "Опишите то, что видите, для людей с нарушениями зрения…",
"alt_text_modal.describe_for_people_with_hearing_impairments": "Добавьте описание для людей с нарушениями слуха…",
"alt_text_modal.describe_for_people_with_visual_impairments": "Добавьте описание для людей с нарушениями зрения…",
"alt_text_modal.done": "Готово",
"announcement.announcement": "Объявление",
"annual_report.summary.archetype.booster": "Репостер",
@ -456,36 +456,36 @@
"intervals.full.hours": "{number, plural, one {# час} few {# часа} other {# часов}}",
"intervals.full.minutes": "{number, plural, one {# минута} few {# минуты} other {# минут}}",
"keyboard_shortcuts.back": "перейти назад",
"keyboard_shortcuts.blocked": "чтобы открыть список заблокированных",
"keyboard_shortcuts.blocked": "открыть список заблокированных",
"keyboard_shortcuts.boost": "продвинуть пост",
"keyboard_shortcuts.column": "фокус на одном из столбцов",
"keyboard_shortcuts.compose": "фокус на поле ввода",
"keyboard_shortcuts.description": "Описание",
"keyboard_shortcuts.direct": "чтобы открыть столбец личных упоминаний",
"keyboard_shortcuts.direct": "перейти к личным упоминаниям",
"keyboard_shortcuts.down": "вниз по списку",
"keyboard_shortcuts.enter": "открыть пост",
"keyboard_shortcuts.favourite": "добавить пост в избранное",
"keyboard_shortcuts.favourites": "открыть «Избранные»",
"keyboard_shortcuts.favourites": "перейти к избранным постам",
"keyboard_shortcuts.federated": "перейти к глобальной ленте",
"keyboard_shortcuts.heading": "Сочетания клавиш",
"keyboard_shortcuts.home": "перейти к домашней ленте",
"keyboard_shortcuts.hotkey": "Гор. клавиша",
"keyboard_shortcuts.legend": "показать это окно",
"keyboard_shortcuts.hotkey": "Горячая клавиша",
"keyboard_shortcuts.legend": "показать эту справку",
"keyboard_shortcuts.local": "перейти к локальной ленте",
"keyboard_shortcuts.mention": "упомянуть автора поста",
"keyboard_shortcuts.muted": "открыть список игнорируемых",
"keyboard_shortcuts.my_profile": "перейти к своему профилю",
"keyboard_shortcuts.notifications": "перейти к уведомлениям",
"keyboard_shortcuts.open_media": "открыть вложение",
"keyboard_shortcuts.open_media": "открыть медиа",
"keyboard_shortcuts.pinned": "перейти к закреплённым постам",
"keyboard_shortcuts.profile": "перейти к профилю автора",
"keyboard_shortcuts.reply": "ответить",
"keyboard_shortcuts.requests": "перейти к запросам на подписку",
"keyboard_shortcuts.search": "перейти к поиску",
"keyboard_shortcuts.spoilers": "показать/скрыть поле предупреждения о содержании",
"keyboard_shortcuts.start": "перейти к разделу \"добро пожаловать\"",
"keyboard_shortcuts.start": "перейти к разделу «Добро пожаловать»",
"keyboard_shortcuts.toggle_hidden": "показать/скрыть текст за предупреждением",
"keyboard_shortcuts.toggle_sensitivity": "показать/скрыть медиафайлы",
"keyboard_shortcuts.toggle_sensitivity": "показать/скрыть медиа",
"keyboard_shortcuts.toot": "начать писать новый пост",
"keyboard_shortcuts.translate": "перевести пост",
"keyboard_shortcuts.unfocus": "убрать фокус с поля ввода/поиска",

View file

@ -38,6 +38,11 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
:elasticsearch_index_mismatch,
mismatched_indexes.join(' ')
)
elsif !specifications_match?
Admin::SystemCheck::Message.new(
:elasticsearch_analysis_mismatch,
mismatched_specifications_indexes.join(' ')
)
elsif cluster_health['status'] == 'red'
Admin::SystemCheck::Message.new(:elasticsearch_health_red)
elsif cluster_health['number_of_nodes'] < 2 && es_preset != 'single_node_cluster'
@ -111,10 +116,20 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
end
end
def mismatched_specifications_indexes
@mismatched_specifications_indexes ||= INDEXES.filter_map do |klass|
klass.base_name if klass.specification.changed?
end
end
def indexes_match?
mismatched_indexes.empty?
end
def specifications_match?
mismatched_specifications_indexes.empty?
end
def es_preset
ENV.fetch('ES_PRESET', 'single_node_cluster')
end

View file

@ -18,17 +18,17 @@ class FeaturedTag < ApplicationRecord
belongs_to :account, inverse_of: :featured_tags
belongs_to :tag, inverse_of: :featured_tags, optional: true # Set after validation
validates :name, presence: true, format: { with: Tag::HASHTAG_NAME_RE }, on: :create
validates :name, presence: true, on: :create, if: -> { tag_id.nil? }
validates :name, format: { with: Tag::HASHTAG_NAME_RE }, on: :create, allow_blank: true
validates :tag_id, uniqueness: { scope: :account_id }
validate :validate_tag_uniqueness, on: :create
validate :validate_featured_tags_limit, on: :create
normalizes :name, with: ->(name) { name.strip.delete_prefix('#') }
before_create :set_tag
before_create :reset_data
before_validation :set_tag
scope :by_name, ->(name) { joins(:tag).where(tag: { name: HashtagNormalizer.new.normalize(name) }) }
before_create :reset_data
LIMIT = 10
@ -59,7 +59,11 @@ class FeaturedTag < ApplicationRecord
private
def set_tag
self.tag = Tag.find_or_create_by_names(name)&.first
if tag.nil?
self.tag = Tag.find_or_create_by_names(name)&.first
elsif tag&.new_record?
tag.save
end
end
def reset_data
@ -73,14 +77,6 @@ class FeaturedTag < ApplicationRecord
errors.add(:base, I18n.t('featured_tags.errors.limit')) if account.featured_tags.count >= LIMIT
end
def validate_tag_uniqueness
errors.add(:name, :taken) if tag_already_featured_for_account?
end
def tag_already_featured_for_account?
FeaturedTag.by_name(name).exists?(account_id: account_id)
end
def visible_tagged_account_statuses
account.statuses.distributable_visibility.tagged_with(tag)
end

View file

@ -1,13 +1,15 @@
# frozen_string_literal: true
class TagRelationshipsPresenter
attr_reader :following_map
attr_reader :following_map, :featuring_map
def initialize(tags, current_account_id = nil, **options)
@following_map = if current_account_id.nil?
{}
else
TagFollow.select(:tag_id).where(tag_id: tags.map(&:id), account_id: current_account_id).each_with_object({}) { |f, h| h[f.tag_id] = true }.merge(options[:following_map] || {})
end
if current_account_id.nil?
@following_map = {}
@featuring_map = {}
else
@following_map = TagFollow.select(:tag_id).where(tag_id: tags.map(&:id), account_id: current_account_id).each_with_object({}) { |f, h| h[f.tag_id] = true }.merge(options[:following_map] || {})
@featuring_map = FeaturedTag.select(:tag_id).where(tag_id: tags.map(&:id), account_id: current_account_id).each_with_object({}) { |f, h| h[f.tag_id] = true }.merge(options[:featuring_map] || {})
end
end
end

View file

@ -75,7 +75,7 @@ class OEmbedSerializer < ActiveModel::Serializer
<<~HTML.squish
<blockquote class="mastodon-embed" data-embed-url="#{embed_short_account_status_url(object.account, object)}" style="#{INLINE_STYLES[:blockquote]}">
<a href="#{short_account_status_url(object.account, object)}" target="_blank" style="#{INLINE_STYLES[:status_link]}">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M74.7135 16.6043C73.6199 8.54587 66.5351 2.19527 58.1366 0.964691C56.7196 0.756754 51.351 0 38.9148 0H38.822C26.3824 0 23.7135 0.756754 22.2966 0.964691C14.1319 2.16118 6.67571 7.86752 4.86669 16.0214C3.99657 20.0369 3.90371 24.4888 4.06535 28.5726C4.29578 34.4289 4.34049 40.275 4.877 46.1075C5.24791 49.9817 5.89495 53.8251 6.81328 57.6088C8.53288 64.5968 15.4938 70.4122 22.3138 72.7848C29.6155 75.259 37.468 75.6697 44.9919 73.971C45.8196 73.7801 46.6381 73.5586 47.4475 73.3063C49.2737 72.7302 51.4164 72.086 52.9915 70.9542C53.0131 70.9384 53.0308 70.9178 53.0433 70.8942C53.0558 70.8706 53.0628 70.8445 53.0637 70.8179V65.1661C53.0634 65.1412 53.0574 65.1167 53.0462 65.0944C53.035 65.0721 53.0189 65.0525 52.9992 65.0371C52.9794 65.0218 52.9564 65.011 52.9318 65.0056C52.9073 65.0002 52.8819 65.0003 52.8574 65.0059C48.0369 66.1472 43.0971 66.7193 38.141 66.7103C29.6118 66.7103 27.3178 62.6981 26.6609 61.0278C26.1329 59.5842 25.7976 58.0784 25.6636 56.5486C25.6622 56.5229 25.667 56.4973 25.6775 56.4738C25.688 56.4502 25.7039 56.4295 25.724 56.4132C25.7441 56.397 25.7678 56.3856 25.7931 56.3801C25.8185 56.3746 25.8448 56.3751 25.8699 56.3816C30.6101 57.5151 35.4693 58.0873 40.3455 58.086C41.5183 58.086 42.6876 58.086 43.8604 58.0553C48.7647 57.919 53.9339 57.6701 58.7591 56.7361C58.8794 56.7123 58.9998 56.6918 59.103 56.6611C66.7139 55.2124 73.9569 50.665 74.6929 39.1501C74.7204 38.6967 74.7892 34.4016 74.7892 33.9312C74.7926 32.3325 75.3085 22.5901 74.7135 16.6043ZM62.9996 45.3371H54.9966V25.9069C54.9966 21.8163 53.277 19.7302 49.7793 19.7302C45.9343 19.7302 44.0083 22.1981 44.0083 27.0727V37.7082H36.0534V27.0727C36.0534 22.1981 34.124 19.7302 30.279 19.7302C26.8019 19.7302 25.0651 21.8163 25.0617 25.9069V45.3371H17.0656V25.3172C17.0656 21.2266 18.1191 17.9769 20.2262 15.568C22.3998 13.1648 25.2509 11.9308 28.7898 11.9308C32.8859 11.9308 35.9812 13.492 38.0447 16.6111L40.036 19.9245L42.0308 16.6111C44.0943 13.492 47.1896 11.9308 51.2788 11.9308C54.8143 11.9308 57.6654 13.1648 59.8459 15.568C61.9529 17.9746 63.0065 21.2243 63.0065 25.3172L62.9996 45.3371Z" fill="currentColor"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M63 45.3v-20c0-4.1-1-7.3-3.2-9.7-2.1-2.4-5-3.7-8.5-3.7-4.1 0-7.2 1.6-9.3 4.7l-2 3.3-2-3.3c-2-3.1-5.1-4.7-9.2-4.7-3.5 0-6.4 1.3-8.6 3.7-2.1 2.4-3.1 5.6-3.1 9.7v20h8V25.9c0-4.1 1.7-6.2 5.2-6.2 3.8 0 5.8 2.5 5.8 7.4V37.7H44V27.1c0-4.9 1.9-7.4 5.8-7.4 3.5 0 5.2 2.1 5.2 6.2V45.3h8ZM74.7 16.6c.6 6 .1 15.7.1 17.3 0 .5-.1 4.8-.1 5.3-.7 11.5-8 16-15.6 17.5-.1 0-.2 0-.3 0-4.9 1-10 1.2-14.9 1.4-1.2 0-2.4 0-3.6 0-4.8 0-9.7-.6-14.4-1.7-.1 0-.1 0-.1 0s-.1 0-.1 0 0 .1 0 .1 0 0 0 0c.1 1.6.4 3.1 1 4.5.6 1.7 2.9 5.7 11.4 5.7 5 0 9.9-.6 14.8-1.7 0 0 0 0 0 0 .1 0 .1 0 .1 0 0 .1 0 .1 0 .1.1 0 .1 0 .1.1v5.6s0 .1-.1.1c0 0 0 0 0 .1-1.6 1.1-3.7 1.7-5.6 2.3-.8.3-1.6.5-2.4.7-7.5 1.7-15.4 1.3-22.7-1.2-6.8-2.4-13.8-8.2-15.5-15.2-.9-3.8-1.6-7.6-1.9-11.5-.6-5.8-.6-11.7-.8-17.5C3.9 24.5 4 20 4.9 16 6.7 7.9 14.1 2.2 22.3 1c1.4-.2 4.1-1 16.5-1h.1C51.4 0 56.7.8 58.1 1c8.4 1.2 15.5 7.5 16.6 15.6Z" fill="currentColor"/></svg>
<div style="#{INLINE_STYLES[:div_account]}">Post by @#{object.account.pretty_acct}@#{provider_name}</div>
<div style="#{INLINE_STYLES[:div_view]}">View on Mastodon</div>
</a>

View file

@ -6,6 +6,7 @@ class REST::TagSerializer < ActiveModel::Serializer
attributes :id, :name, :url, :history
attribute :following, if: :current_user?
attribute :featuring, if: :current_user?
def id
object.id.to_s
@ -27,6 +28,14 @@ class REST::TagSerializer < ActiveModel::Serializer
end
end
def featuring
if instance_options && instance_options[:relationships]
instance_options[:relationships].featuring_map[object.id] || false
else
FeaturedTag.exists?(tag_id: object.id, account_id: current_user.account_id)
end
end
def current_user?
!current_user.nil?
end

View file

@ -142,11 +142,37 @@ class AccountSearchService < BaseService
def core_query
{
multi_match: {
query: @query,
type: 'best_fields',
fields: %w(username^2 display_name^2 text text.*),
operator: 'and',
dis_max: {
queries: [
{
match: {
username: {
query: @query,
analyzer: 'word_join_analyzer',
},
},
},
{
match: {
display_name: {
query: @query,
analyzer: 'word_join_analyzer',
},
},
},
{
multi_match: {
query: @query,
type: 'best_fields',
fields: %w(text text.*),
operator: 'and',
},
},
],
tie_breaker: 0.5,
},
}
end

View file

@ -3,18 +3,24 @@
class CreateFeaturedTagService < BaseService
include Payloadable
def call(account, name, force: true)
def call(account, name_or_tag, raise_error: true)
raise ArgumentError unless account.local?
@account = account
FeaturedTag.create!(account: account, name: name).tap do |featured_tag|
ActivityPub::AccountRawDistributionWorker.perform_async(build_json(featured_tag), account.id) if @account.local?
end
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid => e
if force && e.is_a(ActiveRecord::RecordNotUnique)
FeaturedTag.by_name(name).find_by!(account: account)
else
account.featured_tags.new(name: name)
@featured_tag = begin
if name_or_tag.is_a?(Tag)
account.featured_tags.find_or_initialize_by(tag: name_or_tag)
else
account.featured_tags.find_or_initialize_by(name: name_or_tag)
end
end
create_method = raise_error ? :save! : :save
ActivityPub::AccountRawDistributionWorker.perform_async(build_json(@featured_tag), @account.id) if @featured_tag.new_record? && @featured_tag.public_send(create_method)
@featured_tag
end
private

View file

@ -3,11 +3,24 @@
class RemoveFeaturedTagService < BaseService
include Payloadable
def call(account, featured_tag)
def call(account, featured_tag_or_tag)
raise ArgumentError unless account.local?
@account = account
featured_tag.destroy!
ActivityPub::AccountRawDistributionWorker.perform_async(build_json(featured_tag), account.id) if @account.local?
@featured_tag = begin
if featured_tag_or_tag.is_a?(FeaturedTag)
featured_tag_or_tag
elsif featured_tag_or_tag.is_a?(Tag)
FeaturedTag.find_by(account: account, tag: featured_tag_or_tag)
end
end
return if @featured_tag.nil?
@featured_tag.destroy!
ActivityPub::AccountRawDistributionWorker.perform_async(build_json(@featured_tag), account.id) if @account.local?
end
private

View file

@ -1,7 +1,7 @@
# This is a configuration file for environments that use Japanese and Sudachi plug-ins.
# To use this file, copy it to the Mastodon root directory and rename the file to ".elasticsearch.yml".
version: 1
version: 2
accounts:
filter:
@ -14,6 +14,10 @@ accounts:
english_possessive_stemmer:
type: stemmer
language: possessive_english
word_joiner:
type: shingle
output_unigrams: true
token_separator: ''
my_posfilter:
type: sudachi_part_of_speech
stoptags:
@ -45,6 +49,14 @@ accounts:
- lowercase
- asciifolding
- cjk_width
word_join_analyzer:
type: custom
tokenizer: standard
filter:
- lowercase
- asciifolding
- cjk_width
- word_joiner
edge_ngram:
tokenizer: edge_ngram
filter:

View file

@ -1,7 +1,7 @@
# The standard ElasticSearch settings described in the original Mastodon code are stored.
# This configuration file is overridden by creating a ".elasticsearch.yml" file in the Mastodon root directory.
version: 1
version: 2
accounts:
filter:
@ -14,6 +14,10 @@ accounts:
english_possessive_stemmer:
type: stemmer
language: possessive_english
word_joiner:
type: shingle
output_unigrams: true
token_separator: ''
analyzer:
natural:
@ -32,6 +36,14 @@ accounts:
- lowercase
- asciifolding
- cjk_width
word_join_analyzer:
type: custom
tokenizer: standard
filter:
- lowercase
- asciifolding
- cjk_width
- word_joiner
edge_ngram:
tokenizer: edge_ngram
filter:

View file

@ -1167,6 +1167,8 @@ en:
system_checks:
database_schema_check:
message_html: There are pending database migrations. Please run them to ensure the application behaves as expected
elasticsearch_analysis_index_mismatch:
message_html: Elasticsearch index analyzer settings are outdated. Please run <code>tootctl search deploy --only-mapping --only=%{value}</code>
elasticsearch_health_red:
message_html: Elasticsearch cluster is unhealthy (red status), search features are unavailable
elasticsearch_health_yellow:

View file

@ -325,6 +325,7 @@ ru:
create: Создать объявление
title: Новое объявление
preview:
disclaimer: Так как пользователи не могут отказаться от получения уведомлений по электронной почте, их следует использовать только для действительно важных объявлений, например, чтобы сообщить об утечке персональных данных или о закрытии сервера.
explanation_html: 'Сообщение будет отравлено <strong>%{display_count} пользователям</strong>. В теле письма будет указан следующий текст:'
title: Предпросмотр объявления по электронной почте
publish: Опубликовать
@ -509,6 +510,7 @@ ru:
save: Сохранить
sign_in:
status: Пост
title: FASP
follow_recommendations:
description_html: "<strong>Следуйте рекомендациям, чтобы помочь новым пользователям быстро находить интересный контент</strong>. Если пользователь не взаимодействовал с другими в достаточной степени, чтобы сформировать персонализированные рекомендации, вместо этого рекомендуется использовать эти учетные записи. Они пересчитываются на ежедневной основе на основе комбинации аккаунтов с наибольшим количеством недавних взаимодействий и наибольшим количеством местных подписчиков для данного языка."
language: Для языка
@ -1620,13 +1622,6 @@ ru:
action: Да, отписаться
complete: Подписка отменена
confirmation_html: Вы точно желаете отписаться от всех уведомления типа «%{type}», доставляемых из сервера Mastodon %{domain} на ваш адрес электронной почты %{email}? Вы всегда сможете подписаться снова в <a href="%{settings_path}">настройках e-mail уведомлений</a>.
emails:
notification_emails:
favourite: любимые электронные письма с уведомлениями
follow: Следить за электронными сообщениями
follow_request: Письма с просьбой о помощи
mention: Упоминание электронных писем с уведомлениями
reblog: Уведомления по электронной почте
resubscribe_html: Если вы отписались от рассылки по ошибке, вы можете повторно подписаться на рассылку в настройках <a href="%{settings_path}">настроек почтовых уведомлений</a>.
success_html: Вы больше не будете получать %{type} для Mastodon на %{domain} на вашу электронную почту %{email}.
title: Отписаться
@ -1710,7 +1705,6 @@ ru:
update:
subject: "%{name} изменил(а) пост"
notifications:
administration_emails: Уведомления администратора по электронной почте
email_events: События для уведомлений по электронной почте
email_events_hint: 'Выберите события, для которых вы хотели бы получать уведомления:'
number:

View file

@ -231,6 +231,8 @@ namespace :api, format: false do
member do
post :follow
post :unfollow
post :feature
post :unfeature
end
end

View file

@ -96,7 +96,7 @@ module Mastodon
def api_versions
{
mastodon: 5,
mastodon: 6,
kmyblue: KMYBLUE_API_VERSION,
}
end

View file

@ -23,7 +23,7 @@ RSpec.describe ApplicationController do
end
end
shared_examples 'respond_with_error' do |code|
shared_examples 'error response' do |code|
it "returns http #{code} for http and renders template" do
subject
@ -51,7 +51,7 @@ RSpec.describe ApplicationController do
post 'success'
end
include_examples 'respond_with_error', 422
it_behaves_like 'error response', 422
end
describe 'helper_method :current_account' do
@ -123,7 +123,7 @@ RSpec.describe ApplicationController do
get 'routing_error'
end
include_examples 'respond_with_error', 404
it_behaves_like 'error response', 404
end
context 'with ActiveRecord::RecordNotFound' do
@ -132,7 +132,7 @@ RSpec.describe ApplicationController do
get 'record_not_found'
end
include_examples 'respond_with_error', 404
it_behaves_like 'error response', 404
end
context 'with ActionController::InvalidAuthenticityToken' do
@ -141,7 +141,7 @@ RSpec.describe ApplicationController do
get 'invalid_authenticity_token'
end
include_examples 'respond_with_error', 422
it_behaves_like 'error response', 422
end
describe 'before_action :check_suspension' do
@ -186,7 +186,7 @@ RSpec.describe ApplicationController do
get 'route_forbidden'
end
include_examples 'respond_with_error', 403
it_behaves_like 'error response', 403
end
describe 'not_found' do
@ -201,7 +201,7 @@ RSpec.describe ApplicationController do
get 'route_not_found'
end
include_examples 'respond_with_error', 404
it_behaves_like 'error response', 404
end
describe 'gone' do
@ -216,7 +216,7 @@ RSpec.describe ApplicationController do
get 'route_gone'
end
include_examples 'respond_with_error', 410
it_behaves_like 'error response', 410
end
describe 'unprocessable_entity' do
@ -231,6 +231,6 @@ RSpec.describe ApplicationController do
get 'route_unprocessable_entity'
end
include_examples 'respond_with_error', 422
it_behaves_like 'error response', 422
end
end

View file

@ -5,7 +5,7 @@ require 'rails_helper'
RSpec.describe Auth::RegistrationsController do
render_views
shared_examples 'checks for enabled registrations' do |path|
shared_examples 'registration mode based responses' do |path|
context 'when in single user mode and open for registration' do
before do
Setting.registrations_mode = 'open'
@ -156,7 +156,7 @@ RSpec.describe Auth::RegistrationsController do
end
end
include_examples 'checks for enabled registrations', :new
it_behaves_like 'registration mode based responses', :new
end
describe 'POST #create' do
@ -542,7 +542,8 @@ RSpec.describe Auth::RegistrationsController do
it_behaves_like 'registration with time', 'only secondary time range is set', 0, 0, 9, 12, true
end
include_examples 'checks for enabled registrations', :create
it_behaves_like 'checks for enabled registrations', :create
it_behaves_like 'registration mode based responses', :create
end
describe 'DELETE #destroy' do

View file

@ -59,10 +59,10 @@ RSpec.describe Localized do
sign_in(user)
end
include_examples 'default locale'
it_behaves_like 'default locale'
end
context 'with a user who has not signed in' do
include_examples 'default locale'
it_behaves_like 'default locale'
end
end

View file

@ -35,7 +35,7 @@ RSpec.describe RelationshipsController do
describe 'PATCH #update' do
let(:alice) { Fabricate(:account, username: 'alice', domain: 'example.com') }
shared_examples 'redirects back to followers page' do
shared_examples 'general behavior for followed user' do
it 'redirects back to followers page' do
alice.follow!(user.account)
@ -49,7 +49,7 @@ RSpec.describe RelationshipsController do
context 'when select parameter is not provided' do
subject { patch :update }
include_examples 'redirects back to followers page'
it_behaves_like 'general behavior for followed user'
end
context 'when select parameter is provided' do
@ -83,7 +83,7 @@ RSpec.describe RelationshipsController do
end
end
include_examples 'redirects back to followers page'
it_behaves_like 'general behavior for followed user'
end
end
end

View file

@ -162,7 +162,7 @@ RSpec.describe Settings::ImportsController do
]
end
include_examples 'export failed rows', "Account address,Show boosts,Notify on new posts,Languages\nfoo@bar,true,false,\nuser@bar,false,true,\"fr, de\"\n"
it_behaves_like 'export failed rows', "Account address,Show boosts,Notify on new posts,Languages\nfoo@bar,true,false,\nuser@bar,false,true,\"fr, de\"\n"
end
context 'with blocks' do
@ -175,7 +175,7 @@ RSpec.describe Settings::ImportsController do
]
end
include_examples 'export failed rows', "foo@bar\nuser@bar\n"
it_behaves_like 'export failed rows', "foo@bar\nuser@bar\n"
end
context 'with mutes' do
@ -188,7 +188,7 @@ RSpec.describe Settings::ImportsController do
]
end
include_examples 'export failed rows', "Account address,Hide notifications\nfoo@bar,true\nuser@bar,false\n"
it_behaves_like 'export failed rows', "Account address,Hide notifications\nfoo@bar,true\nuser@bar,false\n"
end
context 'with domain blocks' do
@ -201,7 +201,7 @@ RSpec.describe Settings::ImportsController do
]
end
include_examples 'export failed rows', "bad.domain\nevil.domain\n"
it_behaves_like 'export failed rows', "bad.domain\nevil.domain\n"
end
context 'with bookmarks' do
@ -214,7 +214,7 @@ RSpec.describe Settings::ImportsController do
]
end
include_examples 'export failed rows', "https://foo.com/1\nhttps://foo.com/2\n"
it_behaves_like 'export failed rows', "https://foo.com/1\nhttps://foo.com/2\n"
end
context 'with lists' do
@ -227,7 +227,7 @@ RSpec.describe Settings::ImportsController do
]
end
include_examples 'export failed rows', "Amigos,user@example.com\nFrenemies,user@org.org\n"
it_behaves_like 'export failed rows', "Amigos,user@example.com\nFrenemies,user@org.org\n"
end
end

View file

@ -34,7 +34,7 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
get :new, session: { challenge_passed_at: Time.now.utc, new_otp_secret: 'thisisasecretforthespecofnewview' }
end
include_examples 'renders expected page'
it_behaves_like 'renders expected page'
end
it 'redirects if a new otp_secret has not been set in the session' do
@ -94,7 +94,7 @@ RSpec.describe Settings::TwoFactorAuthentication::ConfirmationsController do
.to include(I18n.t('otp_authentication.wrong_code'))
end
include_examples 'renders expected page'
it_behaves_like 'renders expected page'
end
private

View file

@ -12,7 +12,7 @@ RSpec.describe CacheBuster do
let(:purge_url) { 'https://example.com/test_purge' }
describe '#bust' do
shared_examples 'makes_request' do
shared_examples 'cache busting request' do
it 'makes an HTTP purging request' do
method = http_method&.to_sym || :get
stub_request(method, purge_url).to_return(status: 200)
@ -28,28 +28,28 @@ RSpec.describe CacheBuster do
end
context 'when using default options' do
include_examples 'makes_request'
it_behaves_like 'cache busting request'
end
context 'when specifying a secret header' do
let(:secret_header) { 'X-Purge-Secret' }
let(:secret) { SecureRandom.hex(20) }
include_examples 'makes_request'
it_behaves_like 'cache busting request'
end
context 'when specifying a PURGE method' do
let(:http_method) { 'purge' }
context 'when not using headers' do
include_examples 'makes_request'
it_behaves_like 'cache busting request'
end
context 'when specifying a secret header' do
let(:secret_header) { 'X-Purge-Secret' }
let(:secret) { SecureRandom.hex(20) }
include_examples 'makes_request'
it_behaves_like 'cache busting request'
end
end
end

View file

@ -44,14 +44,14 @@ RSpec.describe Fasp::Request do
end
describe '#get' do
include_examples 'a provider request', :get
it_behaves_like 'a provider request', :get
end
describe '#post' do
include_examples 'a provider request', :post
it_behaves_like 'a provider request', :post
end
describe '#delete' do
include_examples 'a provider request', :delete
it_behaves_like 'a provider request', :delete
end
end

View file

@ -118,7 +118,7 @@ RSpec.describe LinkDetailsExtractor do
</html>
HTML
include_examples 'structured data'
it_behaves_like 'structured data'
end
context 'with the first tag is invalid JSON' do
@ -136,7 +136,7 @@ RSpec.describe LinkDetailsExtractor do
</html>
HTML
include_examples 'structured data'
it_behaves_like 'structured data'
end
context 'with the first tag is null' do
@ -154,7 +154,7 @@ RSpec.describe LinkDetailsExtractor do
</html>
HTML
include_examples 'structured data'
it_behaves_like 'structured data'
end
context 'with preceding block of unsupported LD+JSON' do
@ -194,7 +194,7 @@ RSpec.describe LinkDetailsExtractor do
</html>
HTML
include_examples 'structured data'
it_behaves_like 'structured data'
end
context 'with unsupported in same block LD+JSON' do
@ -218,7 +218,7 @@ RSpec.describe LinkDetailsExtractor do
</html>
HTML
include_examples 'structured data'
it_behaves_like 'structured data'
end
context 'with author names as array' do

View file

@ -56,7 +56,7 @@ RSpec.describe Mastodon::CLI::IpBlocks do
end
context 'with valid IP addresses' do
include_examples 'ip address blocking'
it_behaves_like 'ip address blocking'
end
context 'when a specified IP address is already blocked' do
@ -84,7 +84,7 @@ RSpec.describe Mastodon::CLI::IpBlocks do
.to('sign_up_requires_approval')
end
include_examples 'ip address blocking'
it_behaves_like 'ip address blocking'
end
end
@ -101,25 +101,25 @@ RSpec.describe Mastodon::CLI::IpBlocks do
context 'with --comment option' do
let(:options) { { severity: 'no_access', comment: 'Spam' } }
include_examples 'ip address blocking'
it_behaves_like 'ip address blocking'
end
context 'with --duration option' do
let(:options) { { severity: 'no_access', duration: 10.days } }
include_examples 'ip address blocking'
it_behaves_like 'ip address blocking'
end
context 'with "sign_up_requires_approval" severity' do
let(:options) { { severity: 'sign_up_requires_approval' } }
include_examples 'ip address blocking'
it_behaves_like 'ip address blocking'
end
context 'with "sign_up_block" severity' do
let(:options) { { severity: 'sign_up_block' } }
include_examples 'ip address blocking'
it_behaves_like 'ip address blocking'
end
context 'when a specified IP address fails to be blocked' do

View file

@ -207,18 +207,18 @@ RSpec.describe Mastodon::RedisConfiguration do
end
end
include_examples 'setting a different driver'
include_examples 'setting a namespace'
include_examples 'sentinel support'
it_behaves_like 'setting a different driver'
it_behaves_like 'setting a namespace'
it_behaves_like 'sentinel support'
end
describe '#sidekiq' do
subject { redis_environment.sidekiq }
include_examples 'secondary configuration', 'SIDEKIQ'
include_examples 'setting a different driver'
include_examples 'setting a namespace'
include_examples 'sentinel support', 'SIDEKIQ'
it_behaves_like 'secondary configuration', 'SIDEKIQ'
it_behaves_like 'setting a different driver'
it_behaves_like 'setting a namespace'
it_behaves_like 'sentinel support', 'SIDEKIQ'
end
describe '#cache' do
@ -256,8 +256,8 @@ RSpec.describe Mastodon::RedisConfiguration do
end
end
include_examples 'secondary configuration', 'CACHE'
include_examples 'setting a different driver'
include_examples 'sentinel support', 'CACHE'
it_behaves_like 'secondary configuration', 'CACHE'
it_behaves_like 'setting a different driver'
it_behaves_like 'sentinel support', 'CACHE'
end
end

View file

@ -35,7 +35,7 @@ RSpec.describe NotificationMailer do
let(:notification) { Notification.create!(account: receiver.account, activity: mention) }
let(:mail) { prepared_mailer_for(receiver.account).mention }
include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob'
it_behaves_like 'localized subject', 'notification_mailer.mention.subject', name: 'bob'
it 'renders the email' do
expect(mail)
@ -47,8 +47,8 @@ RSpec.describe NotificationMailer do
.and have_standard_headers('mention').for(receiver)
end
include_examples 'delivery to non functional user'
include_examples 'delivery without status'
it_behaves_like 'delivery to non functional user'
it_behaves_like 'delivery without status'
end
describe 'follow' do
@ -56,7 +56,7 @@ RSpec.describe NotificationMailer do
let(:notification) { Notification.create!(account: receiver.account, activity: follow) }
let(:mail) { prepared_mailer_for(receiver.account).follow }
include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob'
it_behaves_like 'localized subject', 'notification_mailer.follow.subject', name: 'bob'
it 'renders the email' do
expect(mail)
@ -66,7 +66,7 @@ RSpec.describe NotificationMailer do
.and have_standard_headers('follow').for(receiver)
end
include_examples 'delivery to non functional user'
it_behaves_like 'delivery to non functional user'
end
describe 'favourite' do
@ -74,7 +74,7 @@ RSpec.describe NotificationMailer do
let(:notification) { Notification.create!(account: receiver.account, activity: favourite) }
let(:mail) { prepared_mailer_for(own_status.account).favourite }
include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob'
it_behaves_like 'localized subject', 'notification_mailer.favourite.subject', name: 'bob'
it 'renders the email' do
expect(mail)
@ -86,8 +86,8 @@ RSpec.describe NotificationMailer do
.and have_standard_headers('favourite').for(receiver)
end
include_examples 'delivery to non functional user'
include_examples 'delivery without status'
it_behaves_like 'delivery to non functional user'
it_behaves_like 'delivery without status'
end
describe 'reblog' do
@ -95,7 +95,7 @@ RSpec.describe NotificationMailer do
let(:notification) { Notification.create!(account: receiver.account, activity: reblog) }
let(:mail) { prepared_mailer_for(own_status.account).reblog }
include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob'
it_behaves_like 'localized subject', 'notification_mailer.reblog.subject', name: 'bob'
it 'renders the email' do
expect(mail)
@ -107,8 +107,8 @@ RSpec.describe NotificationMailer do
.and have_standard_headers('reblog').for(receiver)
end
include_examples 'delivery to non functional user'
include_examples 'delivery without status'
it_behaves_like 'delivery to non functional user'
it_behaves_like 'delivery without status'
end
describe 'follow_request' do
@ -116,7 +116,7 @@ RSpec.describe NotificationMailer do
let(:notification) { Notification.create!(account: receiver.account, activity: follow_request) }
let(:mail) { prepared_mailer_for(receiver.account).follow_request }
include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob'
it_behaves_like 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob'
it 'renders the email' do
expect(mail)
@ -126,7 +126,7 @@ RSpec.describe NotificationMailer do
.and have_standard_headers('follow_request').for(receiver)
end
include_examples 'delivery to non functional user'
it_behaves_like 'delivery to non functional user'
end
private

View file

@ -29,10 +29,10 @@ RSpec.describe UserMailer do
.and(have_body_text(Rails.configuration.x.local_domain))
end
include_examples 'localized subject',
'devise.mailer.confirmation_instructions.subject',
instance: Rails.configuration.x.local_domain
include_examples 'delivery to memorialized user'
it_behaves_like 'localized subject',
'devise.mailer.confirmation_instructions.subject',
instance: Rails.configuration.x.local_domain
it_behaves_like 'delivery to memorialized user'
end
describe '#reconfirmation_instructions' do
@ -48,10 +48,10 @@ RSpec.describe UserMailer do
.and(have_body_text(Rails.configuration.x.local_domain))
end
include_examples 'localized subject',
'devise.mailer.confirmation_instructions.subject',
instance: Rails.configuration.x.local_domain
include_examples 'delivery to memorialized user'
it_behaves_like 'localized subject',
'devise.mailer.confirmation_instructions.subject',
instance: Rails.configuration.x.local_domain
it_behaves_like 'delivery to memorialized user'
end
describe '#reset_password_instructions' do
@ -66,9 +66,9 @@ RSpec.describe UserMailer do
.and(have_body_text('spec'))
end
include_examples 'localized subject',
'devise.mailer.reset_password_instructions.subject'
include_examples 'delivery to memorialized user'
it_behaves_like 'localized subject',
'devise.mailer.reset_password_instructions.subject'
it_behaves_like 'delivery to memorialized user'
end
describe '#password_change' do
@ -82,9 +82,9 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.password_change.title')))
end
include_examples 'localized subject',
'devise.mailer.password_change.subject'
include_examples 'delivery to memorialized user'
it_behaves_like 'localized subject',
'devise.mailer.password_change.subject'
it_behaves_like 'delivery to memorialized user'
end
describe '#email_changed' do
@ -98,9 +98,9 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.email_changed.title')))
end
include_examples 'localized subject',
'devise.mailer.email_changed.subject'
include_examples 'delivery to memorialized user'
it_behaves_like 'localized subject',
'devise.mailer.email_changed.subject'
it_behaves_like 'delivery to memorialized user'
end
describe '#warning' do
@ -129,9 +129,9 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.webauthn_credential.deleted.title')))
end
include_examples 'localized subject',
'devise.mailer.webauthn_credential.deleted.subject'
include_examples 'delivery to memorialized user'
it_behaves_like 'localized subject',
'devise.mailer.webauthn_credential.deleted.subject'
it_behaves_like 'delivery to memorialized user'
end
describe '#suspicious_sign_in' do
@ -148,8 +148,8 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('user_mailer.suspicious_sign_in.explanation')))
end
include_examples 'localized subject',
'user_mailer.suspicious_sign_in.subject'
it_behaves_like 'localized subject',
'user_mailer.suspicious_sign_in.subject'
end
describe '#failed_2fa' do
@ -166,8 +166,8 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('user_mailer.failed_2fa.explanation')))
end
include_examples 'localized subject',
'user_mailer.failed_2fa.subject'
it_behaves_like 'localized subject',
'user_mailer.failed_2fa.subject'
end
describe '#appeal_approved' do
@ -204,7 +204,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.two_factor_enabled.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#two_factor_disabled' do
@ -217,7 +217,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.two_factor_disabled.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#webauthn_enabled' do
@ -230,7 +230,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.webauthn_enabled.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#webauthn_disabled' do
@ -243,7 +243,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.webauthn_disabled.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#two_factor_recovery_codes_changed' do
@ -256,7 +256,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.two_factor_recovery_codes_changed.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#webauthn_credential_added' do
@ -270,7 +270,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('devise.mailer.webauthn_credential.added.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#welcome' do
@ -289,7 +289,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('user_mailer.welcome.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#backup_ready' do
@ -303,7 +303,7 @@ RSpec.describe UserMailer do
.and(have_body_text(I18n.t('user_mailer.backup_ready.explanation')))
end
include_examples 'delivery to memorialized user'
it_behaves_like 'delivery to memorialized user'
end
describe '#terms_of_service_changed' do

View file

@ -3,8 +3,8 @@
require 'rails_helper'
RSpec.describe Account do
include_examples 'Account::Search'
include_examples 'Reviewable'
it_behaves_like 'Account::Search'
it_behaves_like 'Reviewable'
context 'with an account record' do
subject { Fabricate(:account) }
@ -992,8 +992,8 @@ RSpec.describe Account do
end
end
include_examples 'AccountAvatar', :account
include_examples 'AccountHeader', :account
it_behaves_like 'AccountAvatar', :account
it_behaves_like 'AccountHeader', :account
describe '#increment_count!' do
subject { Fabricate(:account) }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe CustomFilter do
include_examples 'Expireable'
it_behaves_like 'Expireable'
describe 'Validations' do
it { is_expected.to validate_presence_of(:title) }

View file

@ -17,7 +17,7 @@ RSpec.describe FeaturedTag do
let(:account) { Fabricate :account }
it { is_expected.to_not allow_value('Test').for(:name) }
it { is_expected.to_not allow_value('Test').for(:name).against(:tag_id) }
context 'when account has hit limit' do
before { stub_const 'FeaturedTag::LIMIT', 1 }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe Invite do
include_examples 'Expireable'
it_behaves_like 'Expireable'
describe 'Associations' do
it { is_expected.to belong_to(:user).inverse_of(:invites) }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe IpBlock do
include_examples 'Expireable'
it_behaves_like 'Expireable'
describe 'Validations' do
subject { Fabricate.build :ip_block }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe LoginActivity do
include_examples 'BrowserDetection'
it_behaves_like 'BrowserDetection'
describe 'Associations' do
it { is_expected.to belong_to(:user).required }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe Mute do
include_examples 'Expireable'
it_behaves_like 'Expireable'
describe 'Associations' do
it { is_expected.to belong_to(:account).required }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe Poll do
include_examples 'Expireable'
it_behaves_like 'Expireable'
describe '#reset_votes!' do
let(:poll) { Fabricate :poll, cached_tallies: [2, 3], votes_count: 5, voters_count: 5 }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe PreviewCardProvider do
include_examples 'Reviewable'
it_behaves_like 'Reviewable'
describe 'scopes' do
let(:trendable_and_reviewed) { Fabricate(:preview_card_provider, trendable: true, reviewed_at: 5.days.ago) }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe PreviewCardTrend do
include_examples 'RankedTrend'
it_behaves_like 'RankedTrend'
describe 'Associations' do
it { is_expected.to belong_to(:preview_card).required }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe SessionActivation do
include_examples 'BrowserDetection'
it_behaves_like 'BrowserDetection'
describe '.active?' do
subject { described_class.active?(id) }

View file

@ -9,7 +9,7 @@ RSpec.describe Status do
let(:bob) { Fabricate(:account, username: 'bob') }
let(:other) { Fabricate(:status, account: bob, text: 'Skulls for the skull god! The enemy\'s gates are sideways!') }
include_examples 'Status::Visibility'
it_behaves_like 'Status::Visibility'
describe '#local?' do
it 'returns true when no remote URI is set' do

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe StatusTrend do
include_examples 'RankedTrend'
it_behaves_like 'RankedTrend'
describe 'Associations' do
it { is_expected.to belong_to(:account).required }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe Tag do
include_examples 'Reviewable'
it_behaves_like 'Reviewable'
describe 'Validations' do
describe 'name' do

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe TagTrend do
include_examples 'RankedTrend'
it_behaves_like 'RankedTrend'
describe 'Associations' do
it { is_expected.to belong_to(:tag).required }

View file

@ -25,10 +25,10 @@ RSpec.describe Admin::Fasp::ProviderPolicy, type: :policy do
end
permissions :index?, :create? do
include_examples 'admin only', Fasp::Provider
it_behaves_like 'admin only', Fasp::Provider
end
permissions :show?, :create?, :update?, :destroy? do
include_examples 'admin only', :fasp_provider
it_behaves_like 'admin only', :fasp_provider
end
end

View file

@ -127,10 +127,10 @@ RSpec.describe 'FeaturedTags' do
FeaturedTag.create(name: params[:name], account: user.account)
end
it 'returns http unprocessable entity' do
it 'returns http success' do
post '/api/v1/featured_tags', headers: headers, params: params
expect(response).to have_http_status(422)
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
end

View file

@ -161,4 +161,116 @@ RSpec.describe 'Tags' do
end
end
end
describe 'POST /api/v1/tags/:id/feature' do
subject do
post "/api/v1/tags/#{name}/feature", headers: headers
end
let!(:tag) { Fabricate(:tag) }
let(:name) { tag.name }
let(:scopes) { 'write:accounts' }
it_behaves_like 'forbidden for wrong scope', 'read read:follows'
context 'when the tag exists' do
it 'creates featured tag', :aggregate_failures do
subject
expect(response).to have_http_status(:success)
expect(response.content_type)
.to start_with('application/json')
expect(FeaturedTag.where(tag: tag, account: user.account)).to exist
end
end
context 'when the tag does not exist' do
let(:name) { 'hoge' }
it 'creates a new tag with the specified name', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(Tag.where(name: name)).to exist
expect(FeaturedTag.where(tag: Tag.find_by(name: name), account: user.account)).to exist
end
end
context 'when the tag name is invalid' do
let(:name) { 'tag-name' }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when the Authorization header is missing' do
let(:headers) { {} }
let(:name) { 'unauthorized' }
it 'returns http unauthorized' do
subject
expect(response).to have_http_status(401)
expect(response.content_type)
.to start_with('application/json')
end
end
end
describe 'POST #unfeature' do
subject do
post "/api/v1/tags/#{name}/unfeature", headers: headers
end
let(:name) { tag.name }
let!(:tag) { Fabricate(:tag, name: 'foo') }
let(:scopes) { 'write:accounts' }
before do
Fabricate(:featured_tag, account: user.account, tag: tag)
end
it_behaves_like 'forbidden for wrong scope', 'read read:follows'
it 'removes the featured tag', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(FeaturedTag.where(tag: tag, account: user.account)).to_not exist
end
context 'when the tag name is invalid' do
let(:name) { 'tag-name' }
it 'returns http not found' do
subject
expect(response).to have_http_status(404)
expect(response.content_type)
.to start_with('application/json')
end
end
context 'when the Authorization header is missing' do
let(:headers) { {} }
let(:name) { 'unauthorized' }
it 'returns http unauthorized' do
subject
expect(response).to have_http_status(401)
expect(response.content_type)
.to start_with('application/json')
end
end
end
end

View file

@ -29,7 +29,7 @@ RSpec.describe 'Managing OAuth Tokens' do
access_grant.plaintext_token
end
shared_examples 'returns originally requested scopes' do
shared_examples 'original scope request preservation' do
it 'returns all scopes requested for the given code' do
subject
@ -41,26 +41,26 @@ RSpec.describe 'Managing OAuth Tokens' do
context 'with no scopes specified' do
let(:scope) { nil }
include_examples 'returns originally requested scopes'
it_behaves_like 'original scope request preservation'
end
context 'with scopes specified' do
context 'when the scopes were requested for this code' do
let(:scope) { 'write' }
include_examples 'returns originally requested scopes'
it_behaves_like 'original scope request preservation'
end
context 'when the scope was not requested for the code' do
let(:scope) { 'follow' }
include_examples 'returns originally requested scopes'
it_behaves_like 'original scope request preservation'
end
context 'when the scope does not belong to the application' do
let(:scope) { 'push' }
include_examples 'returns originally requested scopes'
it_behaves_like 'original scope request preservation'
end
end
end

View file

@ -130,14 +130,14 @@ RSpec.describe 'OmniAuth callbacks' do
end
describe '#openid_connect', if: ENV['OIDC_ENABLED'] == 'true' && ENV['OIDC_SCOPE'].present? do
include_examples 'omniauth provider callbacks', :openid_connect
it_behaves_like 'omniauth provider callbacks', :openid_connect
end
describe '#cas', if: ENV['CAS_ENABLED'] == 'true' do
include_examples 'omniauth provider callbacks', :cas
it_behaves_like 'omniauth provider callbacks', :cas
end
describe '#saml', if: ENV['SAML_ENABLED'] == 'true' do
include_examples 'omniauth provider callbacks', :saml
it_behaves_like 'omniauth provider callbacks', :saml
end
end

View file

@ -70,7 +70,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do
expect(account.domain).to eq 'example.com'
end
include_examples 'sets profile data'
it_behaves_like 'sets profile data'
end
context 'when WebFinger presents different domain than URI' do
@ -94,7 +94,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do
expect(account.domain).to eq 'iscool.af'
end
include_examples 'sets profile data'
it_behaves_like 'sets profile data'
end
context 'when WebFinger returns a different URI' do

View file

@ -70,7 +70,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do
expect(account.domain).to eq 'example.com'
end
include_examples 'sets profile data'
it_behaves_like 'sets profile data'
end
context 'when WebFinger presents different domain than URI' do
@ -94,7 +94,7 @@ RSpec.describe ActivityPub::FetchRemoteActorService do
expect(account.domain).to eq 'iscool.af'
end
include_examples 'sets profile data'
it_behaves_like 'sets profile data'
end
context 'when WebFinger returns a different URI' do

View file

@ -115,7 +115,7 @@ RSpec.describe BulkImportRowService do
account.follow!(target_account)
end
include_examples 'row import success and list addition'
it_behaves_like 'row import success and list addition'
end
context 'when the user already requested to follow the target account' do
@ -123,17 +123,17 @@ RSpec.describe BulkImportRowService do
account.request_follow!(target_account)
end
include_examples 'row import success and list addition'
it_behaves_like 'row import success and list addition'
end
context 'when the target account is neither followed nor requested' do
include_examples 'row import success and list addition'
it_behaves_like 'row import success and list addition'
end
context 'when the target account is the user themself' do
let(:target_account) { account }
include_examples 'row import success and list addition'
it_behaves_like 'row import success and list addition'
end
def add_target_account_to_list
@ -153,7 +153,7 @@ RSpec.describe BulkImportRowService do
end
context 'when the list does not exist yet' do
include_examples 'common behavior'
it_behaves_like 'common behavior'
end
context 'when the list exists' do
@ -161,7 +161,7 @@ RSpec.describe BulkImportRowService do
Fabricate(:list, account: account, title: list_name)
end
include_examples 'common behavior'
it_behaves_like 'common behavior'
it 'does not create a new list' do
account.follow!(target_account)

View file

@ -20,11 +20,9 @@ RSpec.describe CreateFeaturedTagService do
context 'with a remote account' do
let(:account) { Fabricate(:account, domain: 'host.example') }
it 'creates a new featured tag and does not distributes' do
it 'raises argument error' do
expect { subject.call(account, tag) }
.to change(FeaturedTag, :count).by(1)
expect(ActivityPub::AccountRawDistributionWorker)
.to_not have_enqueued_sidekiq_job(any_args)
.to raise_error ArgumentError
end
end
end

View file

@ -128,7 +128,7 @@ RSpec.describe DeleteAccountService do
let!(:remote_alice) { Fabricate(:account, inbox_url: 'https://alice.com/inbox', domain: 'alice.com', protocol: :activitypub) }
let!(:remote_bob) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', domain: 'bob.com', protocol: :activitypub) }
include_examples 'common behavior' do
it_behaves_like 'common behavior' do
let(:account) { Fabricate(:account) }
let(:local_follower) { Fabricate(:account) }
@ -145,7 +145,7 @@ RSpec.describe DeleteAccountService do
stub_request(:post, account.inbox_url).to_return(status: 201)
end
include_examples 'common behavior' do
it_behaves_like 'common behavior' do
let(:account) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
let(:local_follower) { Fabricate(:account) }

View file

@ -23,13 +23,9 @@ RSpec.describe RemoveFeaturedTagService do
context 'when called by a non local account' do
let(:account) { Fabricate(:account, domain: 'host.example') }
it 'destroys the featured tag and does not send a distribution' do
subject.call(account, featured_tag)
expect { featured_tag.reload }
.to raise_error(ActiveRecord::RecordNotFound)
expect(ActivityPub::AccountRawDistributionWorker)
.to_not have_enqueued_sidekiq_job(any_args)
it 'raises argument error' do
expect { subject.call(account, featured_tag) }
.to raise_error(ArgumentError)
end
end
end

View file

@ -46,7 +46,7 @@ RSpec.describe SuspendAccountService do
json['type'] == 'Update' && json['actor'] == actor_id && json['object']['id'] == actor_id && json['object']['suspended']
end
include_examples 'common behavior' do
it_behaves_like 'common behavior' do
let!(:account) { Fabricate(:account) }
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
let!(:remote_reporter) { Fabricate(:account, uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
@ -72,7 +72,7 @@ RSpec.describe SuspendAccountService do
json['type'] == 'Reject' && json['actor'] == ActivityPub::TagManager.instance.uri_for(followee) && json['object']['actor'] == account.uri
end
include_examples 'common behavior' do
it_behaves_like 'common behavior' do
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
let!(:local_followee) { Fabricate(:account) }

View file

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe UnsuspendAccountService do
shared_context 'with common context' do
shared_context 'when account is unsuspended' do
subject { described_class.new.call(account) }
let!(:local_follower) { Fabricate(:user, current_sign_in_at: 1.hour.ago).account }
@ -31,12 +31,13 @@ RSpec.describe UnsuspendAccountService do
stub_request(:post, 'https://bob.com/inbox').to_return(status: 201)
end
let!(:account) { Fabricate(:account) }
it 'does not change the “suspended” flag' do
expect { subject }.to_not change(account, :suspended?)
end
include_examples 'with common context' do
let!(:account) { Fabricate(:account) }
include_context 'when account is unsuspended' do
let!(:remote_follower) { Fabricate(:account, uri: 'https://alice.com', inbox_url: 'https://alice.com/inbox', protocol: :activitypub, domain: 'alice.com') }
let!(:remote_reporter) { Fabricate(:account, uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
@ -65,8 +66,8 @@ RSpec.describe UnsuspendAccountService do
end
describe 'unsuspending a remote account' do
include_examples 'with common context' do
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
include_context 'when account is unsuspended' do
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
let!(:resolve_account_service) { instance_double(ResolveAccountService) }
before do

View file

@ -8,95 +8,82 @@ RSpec.describe Import::RowWorker do
let(:row) { Fabricate(:bulk_import_row, bulk_import: import) }
describe '#perform' do
before do
allow(BulkImportRowService).to receive(:new).and_return(service_double)
before { allow(BulkImportRowService).to receive(:new).and_return(service_double) }
shared_context 'when service succeeds' do
let(:service_double) { instance_double(BulkImportRowService, call: true) }
end
shared_context 'when service fails' do
let(:service_double) { instance_double(BulkImportRowService, call: false) }
end
shared_context 'when service errors' do
let(:service_double) { instance_double(BulkImportRowService) }
before { allow(service_double).to receive(:call).and_raise('dummy error') }
end
shared_examples 'clean failure' do
let(:service_double) { instance_double(BulkImportRowService, call: false) }
it 'calls BulkImportRowService' do
subject.perform(row.id)
expect(service_double).to have_received(:call).with(row)
end
it 'increases the number of processed items' do
expect { subject.perform(row.id) }.to(change { import.reload.processed_items }.by(+1))
end
it 'does not increase the number of imported items' do
expect { subject.perform(row.id) }.to_not(change { import.reload.imported_items })
end
it 'does not delete the row' do
subject.perform(row.id)
expect(BulkImportRow.exists?(row.id)).to be true
it 'calls service, increases processed items, preserves imported items, and keeps row' do
expect { subject.perform(row.id) }
.to change { import.reload.processed_items }.by(+1)
.and not_change { import.reload.imported_items }
.and(not_change { BulkImportRow.exists?(row.id) }.from(true))
expect(service_double)
.to have_received(:call).with(row)
end
end
shared_examples 'unclean failure' do
let(:service_double) { instance_double(BulkImportRowService) }
before do
allow(service_double).to receive(:call) do
raise 'dummy error'
end
end
it 'raises an error and does not change processed items count' do
expect { subject.perform(row.id) }.to raise_error(StandardError, 'dummy error').and(not_change { import.reload.processed_items })
end
it 'does not delete the row' do
expect { subject.perform(row.id) }.to raise_error(StandardError, 'dummy error').and(not_change { BulkImportRow.exists?(row.id) })
it 'raises an error, preserves processed items, and keeps row' do
expect { subject.perform(row.id) }
.to raise_error(StandardError, 'dummy error')
.and(not_change { import.reload.processed_items })
.and(not_change { BulkImportRow.exists?(row.id) }.from(true))
end
end
shared_examples 'clean success' do
let(:service_double) { instance_double(BulkImportRowService, call: true) }
it 'calls BulkImportRowService' do
subject.perform(row.id)
it 'calls service, increases processed items, increases imported items, and deletes row' do
expect { subject.perform(row.id) }
.to change { import.reload.processed_items }.by(+1)
.and change { import.reload.imported_items }.by(+1)
.and(change { BulkImportRow.exists?(row.id) }.from(true).to(false))
expect(service_double).to have_received(:call).with(row)
end
it 'increases the number of processed items' do
expect { subject.perform(row.id) }.to(change { import.reload.processed_items }.by(+1))
end
it 'increases the number of imported items' do
expect { subject.perform(row.id) }.to(change { import.reload.imported_items }.by(+1))
end
it 'deletes the row' do
expect { subject.perform(row.id) }.to change { BulkImportRow.exists?(row.id) }.from(true).to(false)
end
end
context 'when there are multiple rows to process' do
let(:import) { Fabricate(:bulk_import, total_items: 2, processed_items: 0, imported_items: 0, state: :in_progress) }
context 'with a clean failure' do
include_examples 'clean failure'
include_context 'when service fails'
it_behaves_like 'clean failure'
it 'does not mark the import as finished' do
expect { subject.perform(row.id) }.to_not(change { import.reload.state.to_sym })
expect { subject.perform(row.id) }
.to_not(change { import.reload.state.to_sym })
end
end
context 'with an unclean failure' do
include_examples 'unclean failure'
include_context 'when service errors'
it_behaves_like 'unclean failure'
it 'does not mark the import as finished' do
expect { subject.perform(row.id) }.to raise_error(StandardError).and(not_change { import.reload.state.to_sym })
expect { subject.perform(row.id) }
.to raise_error(StandardError)
.and(not_change { import.reload.state.to_sym })
end
end
context 'with a clean success' do
include_examples 'clean success'
include_context 'when service succeeds'
it_behaves_like 'clean success'
it 'does not mark the import as finished' do
expect { subject.perform(row.id) }.to_not(change { import.reload.state.to_sym })
expect { subject.perform(row.id) }
.to_not(change { import.reload.state.to_sym })
end
end
end
@ -105,21 +92,28 @@ RSpec.describe Import::RowWorker do
let(:import) { Fabricate(:bulk_import, total_items: 2, processed_items: 1, imported_items: 0, state: :in_progress) }
context 'with a clean failure' do
include_examples 'clean failure'
include_context 'when service fails'
it_behaves_like 'clean failure'
it 'marks the import as finished' do
expect { subject.perform(row.id) }.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished)
expect { subject.perform(row.id) }
.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished)
end
end
# NOTE: sidekiq retry logic may be a bit too difficult to test, so leaving this blind spot for now
it_behaves_like 'unclean failure'
context 'with an unclean failure' do
# NOTE: sidekiq retry logic may be a bit too difficult to test, so leaving this blind spot for now
include_context 'when service errors'
it_behaves_like 'unclean failure'
end
context 'with a clean success' do
include_examples 'clean success'
include_context 'when service succeeds'
it_behaves_like 'clean success'
it 'marks the import as finished' do
expect { subject.perform(row.id) }.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished)
expect { subject.perform(row.id) }
.to change { import.reload.state.to_sym }.from(:in_progress).to(:finished)
end
end
end

View file

@ -113,27 +113,27 @@ RSpec.describe MoveWorker do
end
shared_examples 'common tests' do
include_examples 'user note handling'
include_examples 'block and mute handling'
include_examples 'followers count handling'
include_examples 'lists handling'
it_behaves_like 'user note handling'
it_behaves_like 'block and mute handling'
it_behaves_like 'followers count handling'
it_behaves_like 'lists handling'
context 'when a local user already follows both source and target' do
before do
local_follower.request_follow!(target_account)
end
include_examples 'user note handling'
include_examples 'block and mute handling'
include_examples 'followers count handling'
include_examples 'lists handling'
it_behaves_like 'user note handling'
it_behaves_like 'block and mute handling'
it_behaves_like 'followers count handling'
it_behaves_like 'lists handling'
context 'when the local user already has the target in a list' do
before do
list.accounts << target_account
end
include_examples 'lists handling'
it_behaves_like 'lists handling'
end
end
@ -142,17 +142,17 @@ RSpec.describe MoveWorker do
local_follower.follow!(target_account)
end
include_examples 'user note handling'
include_examples 'block and mute handling'
include_examples 'followers count handling'
include_examples 'lists handling'
it_behaves_like 'user note handling'
it_behaves_like 'block and mute handling'
it_behaves_like 'followers count handling'
it_behaves_like 'lists handling'
context 'when the local user already has the target in a list' do
before do
list.accounts << target_account
end
include_examples 'lists handling'
it_behaves_like 'lists handling'
end
end
end
@ -164,7 +164,7 @@ RSpec.describe MoveWorker do
expect(UnfollowFollowWorker).to have_enqueued_sidekiq_job(local_follower.id, source_account.id, target_account.id, false)
end
include_examples 'common tests'
it_behaves_like 'common tests'
end
context 'when target account is local' do
@ -175,7 +175,7 @@ RSpec.describe MoveWorker do
expect(UnfollowFollowWorker).to have_enqueued_sidekiq_job(local_follower.id, source_account.id, target_account.id, true)
end
include_examples 'common tests'
it_behaves_like 'common tests'
end
context 'when both target and source accounts are local' do
@ -187,7 +187,7 @@ RSpec.describe MoveWorker do
expect(local_follower.following?(target_account)).to be true
end
include_examples 'common tests'
it_behaves_like 'common tests'
it 'does not allow the moved account to follow themselves' do
source_account.follow!(target_account)

View file

@ -108,7 +108,7 @@ RSpec.describe Scheduler::AccountsStatusesCleanupScheduler do
context 'when the budget is lower than the number of toots to delete' do
it 'deletes the appropriate statuses' do
expect(Status.count).to be > (subject.compute_budget) # Data check
expect(Status.count).to be > subject.compute_budget # Data check
expect { subject.perform }
.to change(Status, :count).by(-subject.compute_budget) # Cleanable statuses

View file

@ -13432,9 +13432,9 @@ __metadata:
linkType: hard
"pg-connection-string@npm:^2.6.0":
version: 2.8.1
resolution: "pg-connection-string@npm:2.8.1"
checksum: 10c0/87cb519d97a5bdc756f71a6b051eea4d4887e2e102bc694ecda935fe636a037666a0444729b08c7a26c2e9e4b052b2b366af58492ccc49704bacd6876f946ce8
version: 2.8.5
resolution: "pg-connection-string@npm:2.8.5"
checksum: 10c0/5f65afc9dfc99ecf1583a1699c97511f3d505659c9c6a91db8cd0ffe862caa29060722712a034abd6da493356567261febf18b3a6ef223d0a219f0d50d959b97
languageName: node
linkType: hard
@ -13469,9 +13469,9 @@ __metadata:
linkType: hard
"pg-protocol@npm:*":
version: 1.9.0
resolution: "pg-protocol@npm:1.9.0"
checksum: 10c0/c37e61d7fafa97f22eabf12de69863f42fdabb3671df9cc2623bd0ffd6bdedc212e7e8460ad2c721c8a08d8477b4f128a923bf2381905d68a23a532ec7517c77
version: 1.9.5
resolution: "pg-protocol@npm:1.9.5"
checksum: 10c0/5cb3444cf973adadd22ee9ea26bb1674f0d980ef8f9c66d426bbe67fc9cb5f0ca4a204bf7e432b3ab2ab59ac8227f4b18ab3b2e64eaed537e037e916991c7319
languageName: node
linkType: hard