Merge branch 'main' into mute-prefill

This commit is contained in:
Sebastian Hädrich 2025-06-25 13:34:34 +02:00 committed by GitHub
commit ff7e62f3c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
82 changed files with 1010 additions and 262 deletions

View File

@ -23,5 +23,6 @@ RSpec/SpecFilePathFormat:
ActivityPub: activitypub
DeepL: deepl
FetchOEmbedService: fetch_oembed_service
OAuth: oauth
OEmbedController: oembed_controller
OStatus: ostatus

View File

@ -11,7 +11,21 @@ const config: StorybookConfig = {
name: '@storybook/react-vite',
options: {},
},
staticDirs: ['./static'],
staticDirs: [
'./static',
// We need to manually specify the assets because of the symlink in public/sw.js
...[
'avatars',
'emoji',
'headers',
'sounds',
'badge.png',
'loading.gif',
'loading.png',
'oops.gif',
'oops.png',
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
],
};
export default config;

View File

@ -2,16 +2,19 @@ import { useEffect, useState } from 'react';
import { IntlProvider } from 'react-intl';
import { MemoryRouter, Route } from 'react-router';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import type { Preview } from '@storybook/react-vite';
import { http, passthrough } from 'msw';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { action } from 'storybook/actions';
import type { LocaleData } from '@/mastodon/locales';
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
import { defaultMiddleware } from '@/mastodon/store/store';
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
// If you want to run the dark theme during development,
// you can change the below to `/application.scss`
@ -22,7 +25,9 @@ const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
});
// Initialize MSW
initialize();
initialize({
onUnhandledRequest: unhandledRequestHandler,
});
const preview: Preview = {
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
@ -94,6 +99,21 @@ const preview: Preview = {
</IntlProvider>
);
},
(Story) => (
<MemoryRouter>
<Story />
<Route
path='*'
// eslint-disable-next-line react/jsx-no-bind
render={({ location }) => {
if (location.pathname !== '/') {
action(`route change to ${location.pathname}`)(location);
}
return null;
}}
/>
</MemoryRouter>
),
],
loaders: [mswLoader],
parameters: {
@ -115,20 +135,10 @@ const preview: Preview = {
state: {},
// Force docs to use an iframe as it breaks MSW handlers.
// See: https://github.com/mswjs/msw-storybook-addon/issues/83
docs: {
story: {
inline: false,
},
},
docs: {},
msw: {
handlers: [
http.get('/index.json', passthrough),
http.get('/packs-dev/*', passthrough),
http.get('/sounds/*', passthrough),
],
handlers: mockHandlers,
},
},
};

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
class Admin::Instances::ModerationNotesController < Admin::BaseController
before_action :set_instance, only: [:create]
before_action :set_instance_note, only: [:destroy]
def create
authorize :instance_moderation_note, :create?
@instance_moderation_note = current_account.instance_moderation_notes.new(content: resource_params[:content], domain: @instance.domain)
if @instance_moderation_note.save
redirect_to admin_instance_path(@instance.domain, anchor: helpers.dom_id(@instance_moderation_note)), notice: I18n.t('admin.instances.moderation_notes.created_msg')
else
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(5)
render 'admin/instances/show'
end
end
def destroy
authorize @instance_moderation_note, :destroy?
@instance_moderation_note.destroy!
redirect_to admin_instance_path(@instance_moderation_note.domain, anchor: 'instance-notes'), notice: I18n.t('admin.instances.moderation_notes.destroyed_msg')
end
private
def resource_params
params
.expect(instance_moderation_note: [:content])
end
def set_instance
domain = params[:instance_id]&.strip
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
end
def set_instance_note
@instance_moderation_note = InstanceModerationNote.find(params[:id])
end
end

View File

@ -14,6 +14,9 @@ module Admin
def show
authorize :instance, :show?
@instance_moderation_note = @instance.moderation_notes.new
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
end
@ -52,7 +55,8 @@ module Admin
private
def set_instance
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(params[:id]&.strip))
domain = params[:id]&.strip
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
end
def set_instances

View File

@ -17,6 +17,9 @@ module Admin
def edit
authorize @rule, :update?
missing_languages = RuleTranslation.languages - @rule.translations.pluck(:language)
missing_languages.each { |lang| @rule.translations.build(language: lang) }
end
def create

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
skip_before_action :authenticate_resource_owner!
before_action :store_current_location

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
class OAuth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
skip_before_action :authenticate_resource_owner!
before_action :store_current_location

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Oauth::TokensController < Doorkeeper::TokensController
class OAuth::TokensController < Doorkeeper::TokensController
def revoke
unsubscribe_for_token if token.present? && authorized? && token.accessible?

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
class Oauth::UserinfoController < Api::BaseController
class OAuth::UserinfoController < Api::BaseController
before_action -> { doorkeeper_authorize! :profile }, only: [:show]
before_action :require_user!
def show
@account = current_account
render json: @account, serializer: OauthUserinfoSerializer
render json: @account, serializer: OAuthUserinfoSerializer
end
end

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module WellKnown
class OauthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
class OAuthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
include CacheConcern
# Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
@ -13,8 +13,8 @@ module WellKnown
# new OAuth scopes are added), we don't use expires_in to cache upstream,
# instead just caching in the rails cache:
render_with_cache(
json: ::OauthMetadataPresenter.new,
serializer: ::OauthMetadataSerializer,
json: ::OAuthMetadataPresenter.new,
serializer: ::OAuthMetadataSerializer,
content_type: 'application/json',
expires_in: 15.minutes
)

View File

@ -1,6 +1,6 @@
import renderer from 'react-test-renderer';
import { render, fireEvent, screen } from 'mastodon/test_helpers';
import { render, fireEvent, screen } from '@/testing/rendering';
import { Button } from '../button';

View File

@ -0,0 +1,120 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { accountFactoryState, relationshipsFactory } from '@/testing/factories';
import { Account } from './index';
const meta = {
title: 'Components/Account',
component: Account,
argTypes: {
id: {
type: 'string',
description: 'ID of the account to display',
},
size: {
type: 'number',
description: 'Size of the avatar in pixels',
},
hidden: {
type: 'boolean',
description: 'Whether the account is hidden or not',
},
minimal: {
type: 'boolean',
description: 'Whether to display a minimal version of the account',
},
defaultAction: {
type: 'string',
control: 'select',
options: ['block', 'mute'],
description: 'Default action to take on the account',
},
withBio: {
type: 'boolean',
description: 'Whether to display the account bio or not',
},
withMenu: {
type: 'boolean',
description: 'Whether to display the account menu or not',
},
},
args: {
id: '1',
size: 46,
hidden: false,
minimal: false,
defaultAction: 'mute',
withBio: false,
withMenu: true,
},
parameters: {
state: {
accounts: {
'1': accountFactoryState(),
},
},
},
} satisfies Meta<typeof Account>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
id: '1',
},
};
export const Hidden: Story = {
args: {
hidden: true,
},
};
export const Minimal: Story = {
args: {
minimal: true,
},
};
export const WithBio: Story = {
args: {
withBio: true,
},
};
export const NoMenu: Story = {
args: {
withMenu: false,
},
};
export const Blocked: Story = {
args: {
defaultAction: 'block',
},
parameters: {
state: {
relationships: {
'1': relationshipsFactory({
blocking: true,
}),
},
},
},
};
export const Muted: Story = {
args: {},
parameters: {
state: {
relationships: {
'1': relationshipsFactory({
muting: true,
}),
},
},
},
};

View File

@ -1,4 +1,4 @@
import { render, fireEvent, screen } from 'mastodon/test_helpers';
import { render, fireEvent, screen } from '@/testing/rendering';
import Column from '../column';

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Porušení pravidla",
"report_notification.categories.violation_sentence": "porušení pravidel",
"report_notification.open": "Otevřít hlášení",
"search.clear": "Vymazat hledání",
"search.no_recent_searches": "Žádná nedávná vyhledávání",
"search.placeholder": "Hledat",
"search.quick_action.account_search": "Profily odpovídající {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Regelverstoß",
"report_notification.categories.violation_sentence": "Regelverletzung",
"report_notification.open": "Meldung öffnen",
"search.clear": "Suchanfrage löschen",
"search.no_recent_searches": "Keine früheren Suchanfragen",
"search.placeholder": "Suchen",
"search.quick_action.account_search": "Profile passend zu {x}",

View File

@ -1,6 +1,7 @@
{
"about.blocks": "Reguligitaj serviloj",
"about.contact": "Kontakto:",
"about.default_locale": "기본",
"about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Kialo ne disponeblas",
"about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.",
@ -8,6 +9,7 @@
"about.domain_blocks.silenced.title": "Limigita",
"about.domain_blocks.suspended.explanation": "Neniuj datumoj el tiu servilo estos prilaboritaj, konservitaj, aŭ interŝanĝitaj, do neeblas interagi aŭ komuniki kun uzantoj de tiu servilo.",
"about.domain_blocks.suspended.title": "Suspendita",
"about.language_label": "Lingvo",
"about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.",
"about.powered_by": "Malcentrigita socia retejo pere de {mastodon}",
"about.rules": "Reguloj de la servilo",
@ -328,9 +330,13 @@
"errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo",
"errors.unexpected_crash.report_issue": "Raporti problemon",
"explore.suggested_follows": "Homoj",
"explore.title": "Popularaĵoj",
"explore.trending_links": "Novaĵoj",
"explore.trending_statuses": "Afiŝoj",
"explore.trending_tags": "Kradvortoj",
"featured_carousel.next": "Antaŭen",
"featured_carousel.post": "Afiŝi",
"featured_carousel.previous": "Malantaŭen",
"filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.",
"filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!",
"filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.",
@ -547,6 +553,7 @@
"navigation_bar.lists": "Listoj",
"navigation_bar.logout": "Elsaluti",
"navigation_bar.moderation": "Modereco",
"navigation_bar.more": "Pli",
"navigation_bar.mutes": "Silentigitaj uzantoj",
"navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.",
"navigation_bar.preferences": "Preferoj",
@ -777,6 +784,7 @@
"report_notification.categories.violation": "Malobservo de la regulo",
"report_notification.categories.violation_sentence": "malobservo de la regulo",
"report_notification.open": "Malfermi la raporton",
"search.clear": "검색어 지우기",
"search.no_recent_searches": "Neniuj lastaj serĉoj",
"search.placeholder": "Serĉi",
"search.quick_action.account_search": "Profiloj kiuj kongruas kun {x}",
@ -872,7 +880,9 @@
"subscribed_languages.save": "Konservi ŝanĝojn",
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
"tabs_bar.home": "Hejmo",
"tabs_bar.menu": "Menuo",
"tabs_bar.notifications": "Sciigoj",
"tabs_bar.search": "Serĉi",
"terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
"terms_of_service.title": "Kondiĉoj de uzado",
"terms_of_service.upcoming_changes_on": "Venontaj ŝanĝoj el {date}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Violación de regla",
"report_notification.categories.violation_sentence": "violación de regla",
"report_notification.open": "Abrir denuncia",
"search.clear": "Limpiar búsqueda",
"search.no_recent_searches": "Sin búsquedas recientes",
"search.placeholder": "Buscar",
"search.quick_action.account_search": "Perfiles que coinciden con {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Infracción de regla",
"report_notification.categories.violation_sentence": "infracción de regla",
"report_notification.open": "Abrir denuncia",
"search.clear": "Limpiar búsqueda",
"search.no_recent_searches": "Sin búsquedas recientes",
"search.placeholder": "Buscar",
"search.quick_action.account_search": "Perfiles que coinciden con {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Infracción de regla",
"report_notification.categories.violation_sentence": "infracción de regla",
"report_notification.open": "Abrir informe",
"search.clear": "Limpiar búsqueda",
"search.no_recent_searches": "No hay búsquedas recientes",
"search.placeholder": "Buscar",
"search.quick_action.account_search": "Perfiles que coinciden con {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Brotin regla",
"report_notification.categories.violation_sentence": "brot á reglu",
"report_notification.open": "Opna melding",
"search.clear": "Nullstilla leiting",
"search.no_recent_searches": "Ongar nýggjar leitingar",
"search.placeholder": "Leita",
"search.quick_action.account_search": "Vangar, ið samsvara {x}",

View File

@ -563,6 +563,8 @@
"navigation_bar.follows_and_followers": "Seguindo e seguidoras",
"navigation_bar.import_export": "Importar e exportar",
"navigation_bar.lists": "Listaxes",
"navigation_bar.live_feed_local": "En directo (local)",
"navigation_bar.live_feed_public": "En directo (federada)",
"navigation_bar.logout": "Pechar sesión",
"navigation_bar.moderation": "Moderación",
"navigation_bar.more": "Máis",
@ -802,6 +804,7 @@
"report_notification.categories.violation": "Faltou ás regras",
"report_notification.categories.violation_sentence": "violación das regras",
"report_notification.open": "Abrir a denuncia",
"search.clear": "Limpar a busca",
"search.no_recent_searches": "Non hai buscas recentes",
"search.placeholder": "Procurar",
"search.quick_action.account_search": "Perfís coincidentes {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "הפרת כלל",
"report_notification.categories.violation_sentence": "הפרת כלל",
"report_notification.open": "פתח דו\"ח",
"search.clear": "ניקוי חיפוש",
"search.no_recent_searches": "לא נמצאו חיפושים אחרונים",
"search.placeholder": "חיפוש",
"search.quick_action.account_search": "פרופילים המכילים {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Szabálysértés",
"report_notification.categories.violation_sentence": "szabálysértés",
"report_notification.open": "Bejelentés megnyitása",
"search.clear": "Keresés törlése",
"search.no_recent_searches": "Nincsenek keresési előzmények",
"search.placeholder": "Keresés",
"search.quick_action.account_search": "Profilok a következő keresésre: {x}",

View File

@ -563,6 +563,8 @@
"navigation_bar.follows_and_followers": "팔로우와 팔로워",
"navigation_bar.import_export": "가져오기 & 내보내기",
"navigation_bar.lists": "리스트",
"navigation_bar.live_feed_local": "라이브 피드 (로컬)",
"navigation_bar.live_feed_public": "라이브 피드 (공개)",
"navigation_bar.logout": "로그아웃",
"navigation_bar.moderation": "중재",
"navigation_bar.more": "더 보기",
@ -802,6 +804,7 @@
"report_notification.categories.violation": "규칙 위반",
"report_notification.categories.violation_sentence": "규칙 위반",
"report_notification.open": "신고 열기",
"search.clear": "검색 초기화",
"search.no_recent_searches": "최근 검색 기록이 없습니다",
"search.placeholder": "검색",
"search.quick_action.account_search": "{x}에 맞는 프로필",

View File

@ -563,6 +563,8 @@
"navigation_bar.follows_and_followers": "Volgers en gevolgde accounts",
"navigation_bar.import_export": "Importeren en exporteren",
"navigation_bar.lists": "Lijsten",
"navigation_bar.live_feed_local": "Openbare tijdlijn (deze server)",
"navigation_bar.live_feed_public": "Openbare tijdlijn (alles)",
"navigation_bar.logout": "Uitloggen",
"navigation_bar.moderation": "Moderatie",
"navigation_bar.more": "Meer",
@ -802,6 +804,7 @@
"report_notification.categories.violation": "Overtreden regel(s)",
"report_notification.categories.violation_sentence": "serverregel overtreden",
"report_notification.open": "Rapportage openen",
"search.clear": "Zoekopdracht wissen",
"search.no_recent_searches": "Geen recente zoekopdrachten",
"search.placeholder": "Zoeken",
"search.quick_action.account_search": "Accounts die overeenkomen met {x}",

View File

@ -563,6 +563,8 @@
"navigation_bar.follows_and_followers": "Seguindo e seguidores",
"navigation_bar.import_export": "Importar e exportar",
"navigation_bar.lists": "Listas",
"navigation_bar.live_feed_local": "Cronologia local",
"navigation_bar.live_feed_public": "Cronologia federada",
"navigation_bar.logout": "Sair",
"navigation_bar.moderation": "Moderação",
"navigation_bar.more": "Mais",
@ -799,6 +801,7 @@
"report_notification.categories.violation": "Violação de regra",
"report_notification.categories.violation_sentence": "violação de regra",
"report_notification.open": "Abrir denúncia",
"search.clear": "Limpar pesquisa",
"search.no_recent_searches": "Nenhuma pesquisa recente",
"search.placeholder": "Pesquisar",
"search.quick_action.account_search": "Perfis com correspondência a {x}",

View File

@ -563,6 +563,8 @@
"navigation_bar.follows_and_followers": "Подписки и подписчики",
"navigation_bar.import_export": "Импорт и экспорт",
"navigation_bar.lists": "Списки",
"navigation_bar.live_feed_local": "Живая лента (локальная)",
"navigation_bar.live_feed_public": "Живая лента (глобальная)",
"navigation_bar.logout": "Выйти",
"navigation_bar.moderation": "Модерирование",
"navigation_bar.more": "Ещё",
@ -578,9 +580,9 @@
"navigation_panel.expand_lists": "Развернуть меню списков",
"not_signed_in_indicator.not_signed_in": "Эта страница доступна только авторизованным пользователям.",
"notification.admin.report": "{name} пожаловался (-лась) на {target}",
"notification.admin.report_account": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target} по причине «{category}»",
"notification.admin.report_account": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target}, выбрав категорию «{category}»",
"notification.admin.report_account_other": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target}",
"notification.admin.report_statuses": "{name} пожаловался (-лась) на {target} по причине «{category}»",
"notification.admin.report_statuses": "{name} пожаловался (-лась) на {target}, выбрав категорию «{category}»",
"notification.admin.report_statuses_other": "{name} пожаловался (-лась) на {target}",
"notification.admin.sign_up": "{name} зарегистрировался (-лась) на сервере",
"notification.admin.sign_up.name_and_others": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} зарегистрировались на сервере",
@ -602,13 +604,13 @@
"notification.mentioned_you": "{name} упомянул(а) вас",
"notification.moderation-warning.learn_more": "Узнать больше",
"notification.moderation_warning": "Модераторы вынесли вам предупреждение",
"notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.",
"notification.moderation_warning.action_delete_statuses": "Некоторые ваши посты были удалены.",
"notification.moderation_warning.action_disable": "Ваша учётная запись была отключена.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые из ваших сообщений были отмечены как деликатные.",
"notification.moderation_warning.action_none": "Ваша учётная запись получила предупреждение от модерации.",
"notification.moderation_warning.action_sensitive": "С этого момента ваши сообщения будут помечены как деликатные.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые ваши посты получили отметку деликатного содержания.",
"notification.moderation_warning.action_none": "Модераторы вынесли вам предупреждение.",
"notification.moderation_warning.action_sensitive": "С этого момента ваши посты будут иметь отметку деликатного содержания.",
"notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
"notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.",
"notification.moderation_warning.action_suspend": "Ваша учётная запись была заблокирована.",
"notification.own_poll": "Ваш опрос завершился",
"notification.poll": "Опрос, в котором вы приняли участие, завершился",
"notification.reblog": "{name} продвинул(а) ваш пост",
@ -792,16 +794,17 @@
"report.thanks.title_actionable": "Спасибо, что сообщили о проблеме, мы рассмотрим вашу жалобу.",
"report.unfollow": "Отписаться от @{name}",
"report.unfollow_explanation": "Вы подписаны на этого пользователя. Отпишитесь от пользователя, чтобы перестать видеть посты этого человека в домашней ленте.",
"report_notification.attached_statuses": "{count, plural, one {{count} сообщение} few {{count} сообщения} many {{count} сообщений} other {{count} сообщений}} вложено",
"report_notification.attached_statuses": "{count, plural, one {{count} пост прикреплён} few {{count} поста прикреплено} other {{count} постов прикреплено}}",
"report_notification.categories.legal": "Нарушение закона",
"report_notification.categories.legal_sentence": "запрещённый контент",
"report_notification.categories.legal_sentence": "Нарушение закона",
"report_notification.categories.other": "Другое",
"report_notification.categories.other_sentence": "другое",
"report_notification.categories.other_sentence": "Другое",
"report_notification.categories.spam": "Спам",
"report_notification.categories.spam_sentence": "спам",
"report_notification.categories.spam_sentence": "Спам",
"report_notification.categories.violation": "Нарушение правил",
"report_notification.categories.violation_sentence": "нарушение правила",
"report_notification.open": "Открыть жалобу",
"report_notification.categories.violation_sentence": "Нарушение правил",
"report_notification.open": "Перейти к жалобе",
"search.clear": "Очистить поисковый запрос",
"search.no_recent_searches": "Недавние запросы отсутствуют",
"search.placeholder": "Поиск",
"search.quick_action.account_search": "Профили, соответствующие {x}",
@ -868,6 +871,7 @@
"status.mute_conversation": "Игнорировать обсуждение",
"status.open": "Открыть пост",
"status.pin": "Закрепить в профиле",
"status.quote_error.filtered": "Скрыто одним из ваших фильтров",
"status.quote_error.removed": "Этот пост был удалён его автором.",
"status.read_more": "Читать далее",
"status.reblog": "Продвинуть",

View File

@ -779,6 +779,7 @@
"report_notification.categories.violation": "Порушення правил",
"report_notification.categories.violation_sentence": "порушення правил",
"report_notification.open": "Відкрити скаргу",
"search.clear": "Очистити пошук",
"search.no_recent_searches": "Немає останніх пошуків",
"search.placeholder": "Пошук",
"search.quick_action.account_search": "Збіг профілів {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Vi phạm nội quy",
"report_notification.categories.violation_sentence": "vi phạm nội quy",
"report_notification.open": "Mở báo cáo",
"search.clear": "Xóa tìm kiếm",
"search.no_recent_searches": "Gần đây chưa tìm gì",
"search.placeholder": "Tìm kiếm",
"search.quick_action.account_search": "Người tên {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "違反規則",
"report_notification.categories.violation_sentence": "違反規則",
"report_notification.open": "開啟檢舉報告",
"search.clear": "清除搜尋紀錄",
"search.no_recent_searches": "尚無最近的搜尋紀錄",
"search.placeholder": "搜尋",
"search.quick_action.account_search": "符合的個人檔案 {x}",

View File

@ -1632,6 +1632,17 @@ a.sparkline {
}
}
a.timestamp {
color: $darker-text-color;
text-decoration: none;
&:hover,
&:focus,
&:active {
text-decoration: underline;
}
}
time {
margin-inline-start: 5px;
vertical-align: baseline;

View File

@ -0,0 +1,53 @@
import { http, HttpResponse } from 'msw';
import { action } from 'storybook/actions';
import { relationshipsFactory } from './factories';
export const mockHandlers = {
mute: http.post<{ id: string }>('/api/v1/accounts/:id/mute', ({ params }) => {
action('muting account')(params);
return HttpResponse.json(
relationshipsFactory({ id: params.id, muting: true }),
);
}),
unmute: http.post<{ id: string }>(
'/api/v1/accounts/:id/unmute',
({ params }) => {
action('unmuting account')(params);
return HttpResponse.json(
relationshipsFactory({ id: params.id, muting: false }),
);
},
),
block: http.post<{ id: string }>(
'/api/v1/accounts/:id/block',
({ params }) => {
action('blocking account')(params);
return HttpResponse.json(
relationshipsFactory({ id: params.id, blocking: true }),
);
},
),
unblock: http.post<{ id: string }>(
'/api/v1/accounts/:id/unblock',
({ params }) => {
action('unblocking account')(params);
return HttpResponse.json(
relationshipsFactory({
id: params.id,
blocking: false,
}),
);
},
),
};
export const unhandledRequestHandler = ({ url }: Request) => {
const { pathname } = new URL(url);
if (pathname.startsWith('/api/v1/')) {
action(`unhandled request to ${pathname}`)(url);
console.warn(
`Unhandled request to ${pathname}. Please add a handler for this request in your storybook configuration.`,
);
}
};

View File

@ -0,0 +1,70 @@
import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships';
import { createAccountFromServerJSON } from '@/mastodon/models/account';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
type FactoryOptions<T> = {
id?: string;
} & Partial<T>;
type FactoryFunction<T> = (options?: FactoryOptions<T>) => T;
export const accountFactory: FactoryFunction<ApiAccountJSON> = ({
id,
...data
} = {}) => ({
id: id ?? '1',
acct: 'testuser',
avatar: '/avatars/original/missing.png',
avatar_static: '/avatars/original/missing.png',
username: 'testuser',
display_name: 'Test User',
bot: false,
created_at: '2023-01-01T00:00:00.000Z',
discoverable: true,
emojis: [],
fields: [],
followers_count: 0,
following_count: 0,
group: false,
header: '/header.png',
header_static: '/header_static.png',
indexable: true,
last_status_at: '2023-01-01',
locked: false,
mute_expires_at: null,
note: 'This is a test user account.',
statuses_count: 0,
suspended: false,
url: '/@testuser',
uri: '/users/testuser',
noindex: false,
roles: [],
hide_collections: false,
...data,
});
export const accountFactoryState = (
options: FactoryOptions<ApiAccountJSON> = {},
) => createAccountFromServerJSON(accountFactory(options));
export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
id,
...data
} = {}) => ({
id: id ?? '1',
following: false,
followed_by: false,
blocking: false,
blocked_by: false,
languages: null,
muting_notifications: false,
note: '',
requested_by: false,
muting: false,
requested: false,
domain_blocking: false,
endorsed: false,
notifying: false,
showing_reblogs: true,
...data,
});

View File

@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router';
import type { RenderOptions } from '@testing-library/react';
import { render as rtlRender } from '@testing-library/react';
import { IdentityContext } from './identity_context';
import { IdentityContext } from '@/mastodon/identity_context';
beforeAll(() => {
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {

View File

@ -163,6 +163,7 @@ class Account < ApplicationRecord
after_update_commit :trigger_update_webhooks
delegate :email,
:email_domain,
:unconfirmed_email,
:current_sign_in_at,
:created_at,

View File

@ -18,6 +18,7 @@ module Account::Associations
has_many :favourites
has_many :featured_tags, -> { includes(:tag) }
has_many :list_accounts
has_many :instance_moderation_notes
has_many :media_attachments
has_many :mentions
has_many :migrations, class_name: 'AccountMigration'

View File

@ -21,6 +21,7 @@ class Instance < ApplicationRecord
belongs_to :unavailable_domain
has_many :accounts, dependent: nil
has_many :moderation_notes, class_name: 'InstanceModerationNote', dependent: :destroy
end
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: instance_moderation_notes
#
# id :bigint(8) not null, primary key
# content :text
# domain :string not null
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint(8) not null
#
class InstanceModerationNote < ApplicationRecord
include DomainNormalizable
include DomainMaterializable
CONTENT_SIZE_LIMIT = 2_000
belongs_to :account
belongs_to :instance, inverse_of: :moderation_notes, foreign_key: :domain, primary_key: :domain, optional: true
scope :chronological, -> { reorder(id: :asc) }
validates :content, presence: true, length: { maximum: CONTENT_SIZE_LIMIT }
validates :domain, presence: true, domain: true
end

View File

@ -20,7 +20,7 @@ class Rule < ApplicationRecord
self.discard_column = :deleted_at
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
accepts_nested_attributes_for :translations, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :translations, reject_if: ->(attributes) { attributes['text'].blank? }, allow_destroy: true
validates :text, presence: true, length: { maximum: TEXT_SIZE_LIMIT }

View File

@ -20,4 +20,8 @@ class RuleTranslation < ApplicationRecord
scope :for_locale, ->(locale) { where(language: I18n::Locale::Tag.tag(locale).to_a.first) }
scope :by_language_length, -> { order(Arel.sql('LENGTH(LANGUAGE)').desc) }
def self.languages
RuleTranslation.joins(:rule).merge(Rule.kept).select(:language).distinct.pluck(:language).sort
end
end

View File

@ -223,6 +223,12 @@ class User < ApplicationRecord
end
end
def email_domain
Mail::Address.new(email).domain
rescue Mail::Field::ParseError
nil
end
def update_sign_in!(new_sign_in: false)
old_current = current_sign_in_at
new_current = Time.now.utc

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class InstanceModerationNotePolicy < ApplicationPolicy
def create?
role.can?(:manage_federation)
end
def destroy?
owner? || (role.can?(:manage_federation) && role.overrides?(record.account.user_role))
end
private
def owner?
record.account_id == current_account&.id
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class OauthMetadataPresenter < ActiveModelSerializers::Model
class OAuthMetadataPresenter < ActiveModelSerializers::Model
include RoutingHelper
attributes :issuer, :authorization_endpoint, :token_endpoint,

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class OauthMetadataSerializer < ActiveModel::Serializer
class OAuthMetadataSerializer < ActiveModel::Serializer
attributes :issuer, :authorization_endpoint, :token_endpoint,
:revocation_endpoint, :userinfo_endpoint, :scopes_supported,
:response_types_supported, :response_modes_supported,

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class OauthUserinfoSerializer < ActiveModel::Serializer
class OAuthUserinfoSerializer < ActiveModel::Serializer
include RoutingHelper
attributes :iss, :sub, :name, :preferred_username, :profile, :picture

View File

@ -25,7 +25,7 @@
%td.accounts-table__extra
- if account.local?
- if account.user_email
= link_to account.user_email.split('@').last, admin_accounts_path(email: "%@#{account.user_email.split('@').last}"), title: account.user_email
= link_to account.user_email_domain, admin_accounts_path(email: "%@#{account.user_email_domain}"), title: account.user_email
- else
\-
%br/

View File

@ -22,10 +22,10 @@
%td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= account.user_email
%td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(account.id) if can?(:change_email, account.user)
%tr
%td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{account.user_email.split('@').last}")
%td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{account.user_email_domain}")
- if can?(:create, :email_domain_block)
%tr
%td= table_link_to 'hide_source', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: account.user_email.split('@').last)
%td= table_link_to 'hide_source', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: account.user_email_domain)
- if account.user_unconfirmed_email.present?
%tr
%th= t('admin.accounts.unconfirmed_email')

View File

@ -6,7 +6,7 @@
= date_range(@time_period)
- if @instance.persisted?
= render 'dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first
= render 'admin/instances/dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first
- else
%p
= t('admin.instances.unknown_instance')
@ -55,6 +55,24 @@
= render partial: 'admin/action_logs/action_log', collection: @action_logs
= link_to t('admin.instances.audit_log.view_all'), admin_action_logs_path(target_domain: @instance.domain), class: 'button'
%hr.spacer/
- if @instance.domain.present?
%h3#instance-notes= t('admin.instances.moderation_notes.title')
%p= t('admin.instances.moderation_notes.description_html')
.report-notes
= render partial: 'admin/report_notes/report_note', collection: @instance_moderation_notes
= simple_form_for @instance_moderation_note, url: admin_instance_moderation_notes_path(instance_id: @instance.domain) do |form|
= render 'shared/error_messages', object: @instance_moderation_note
.field-group
= form.input :content, input_html: { placeholder: t('admin.instances.moderation_notes.placeholder'), maxlength: InstanceModerationNote::CONTENT_SIZE_LIMIT, rows: 6, autofocus: @instance_moderation_note.errors.any? }
.actions
= form.button :button, t('admin.instances.moderation_notes.create'), type: :submit
- if @instance.persisted?
%hr.spacer/
%h3= t('admin.instances.availability.title')

View File

@ -1,11 +1,12 @@
.report-notes__item
.report-notes__item{ id: dom_id(report_note) }
= image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
.report-notes__item__header
%span.username
= link_to report_note.account.username, admin_account_path(report_note.account_id)
%time.relative-formatted{ datetime: report_note.created_at.iso8601, title: report_note.created_at }
= l report_note.created_at.to_date
%a.timestamp{ href: "##{dom_id(report_note)}" }
%time.relative-formatted{ datetime: report_note.created_at.iso8601, title: report_note.created_at }
= l report_note.created_at.to_date
.report-notes__item__content
= linkify(report_note.content)
@ -14,5 +15,7 @@
.report-notes__item__actions
- if report_note.is_a?(AccountModerationNote)
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_account_moderation_note_path(report_note), method: :delete
- elsif report_note.is_a?(InstanceModerationNote)
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_instance_moderation_note_path(instance_id: report_note.domain, id: report_note.id), method: :delete
- else
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete

View File

@ -5,7 +5,7 @@
= f.input :language,
collection: ui_languages,
include_blank: false,
label_method: ->(locale) { native_locale_name(locale) }
label_method: ->(locale) { "#{native_locale_name(locale)} (#{standard_locale_name(locale)})" }
.fields-row__column.fields-group
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>

View File

@ -47,7 +47,6 @@ require_relative '../lib/chewy/strategy/mastodon'
require_relative '../lib/chewy/strategy/bypass_with_warning'
require_relative '../lib/rails/engine_extensions'
require_relative '../lib/action_dispatch/remote_ip_extensions'
require_relative '../lib/stoplight/redis_data_store_extensions'
require_relative '../lib/active_record/database_tasks_extensions'
require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions'

View File

@ -20,6 +20,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym 'DeepL'
inflect.acronym 'DSL'
inflect.acronym 'JsonLd'
inflect.acronym 'OAuth'
inflect.acronym 'OEmbed'
inflect.acronym 'OStatus'
inflect.acronym 'PubSubHubbub'

View File

@ -2,117 +2,117 @@
ru:
devise:
confirmations:
confirmed: Ваш адрес e-mail был успешно подтвержден.
send_instructions: Вы получите e-mail с инструкцией по подтверждению вашего адреса e-mail в течение нескольких минут.
send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail с инструкцией по подтверждению вашего адреса в течение нескольких минут.
confirmed: Ваш адрес электронной почты успешно подтверждён.
send_instructions: В течение нескольких минут вы получите письмо с инструкциями по подтверждению адреса электронной почты. Если письмо не приходит, проверьте папку «Спам».
send_paranoid_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по его подтверждению. Если письмо не приходит, проверьте папку «Спам».
failure:
already_authenticated: Вы уже вошли.
already_authenticated: Вы уже авторизованы.
inactive: Ваша учётная запись ещё не активирована.
invalid: Неверно введены %{authentication_keys} или пароль.
last_attempt: У Вас есть последняя попытка, после чего вход будет заблокирован.
invalid: "%{authentication_keys} или пароль введены неверно."
last_attempt: У вас осталась последняя попытка ввода пароля до блокировки учётной записи.
locked: Ваша учётная запись заблокирована.
not_found_in_database: Неверно введены %{authentication_keys} или пароль.
omniauth_user_creation_failure: Ошибка создания учетной записи с этим идентификатором.
pending: Ваша заявка на вступление всё ещё рассматривается.
timeout: Ваша сессия истекла. Пожалуйста, войдите снова, чтобы продолжить.
not_found_in_database: "%{authentication_keys} или пароль введены неверно."
omniauth_user_creation_failure: Не удалось создать учётную запись с помощью выбранного способа идентификации.
pending: Ваша заявка на регистрацию всё ещё рассматривается.
timeout: Ваш сеанс закончился. Пожалуйста, войдите снова.
unauthenticated: Вам необходимо войти или зарегистрироваться.
unconfirmed: Вам необходимо подтвердить ваш адрес e-mail для продолжения.
unconfirmed: Вы должны подтвердить свой адрес электронной почты.
mailer:
confirmation_instructions:
action: Подтвердить e-mail адрес
action: Подтвердить
action_with_app: Подтвердить и вернуться в %{app}
explanation: Вы создали учётную запись на сайте %{host}, используя этот e-mail адрес. Остался лишь один шаг для активации. Если это были не вы, просто игнорируйте письмо.
explanation_when_pending: Вы подали заявку на %{host}, используя этот адрес e-mail. Как только вы его подтвердите, мы начнём изучать вашу заявку. До тех пор вы не сможете войти на сайт. Если ваша заявка будет отклонена, все данные будут автоматически удалены, от вас не потребуется никаких дополнительных действий. Если это были не вы, пожалуйста, проигнорируйте данное письмо.
extra_html: Пожалуйста, ознакомьтесь <a href="%{terms_path}">правилами узла</a> and <a href="%{policy_path}">условиями пользования Сервисом</a>.
subject: 'Mastodon: Инструкция по подтверждению на узле %{instance}'
title: Подтвердите e-mail адрес
explanation: Вы создали учётную запись на сайте %{host}, используя этот адрес электронной почты. Остался лишь один шаг для её активации. Если это сделали не вы, просто проигнорируйте это письмо.
explanation_when_pending: Вы подали заявку, чтобы создать учётную запись на сайте %{host}, используя этот адрес электронной почты. После того как вы его подтвердите, мы начнём рассматривать вашу заявку. До тех пор вы сможете войти на сайт только для того, чтобы редактировать данные своей учётной записи или удалить её. Если ваша заявка будет отклонена, все данные будут автоматически удалены, от вас не потребуется никаких дополнительных действий. Если заявку подали не вы, пожалуйста, проигнорируйте это письмо.
extra_html: Пожалуйста, также ознакомьтесь с <a href="%{terms_path}">правилами сервера</a> и <a href="%{policy_path}">политикой конфиденциальности</a>.
subject: 'Mastodon: Инструкции по подтверждению учётной записи на %{instance}'
title: Подтверждение адреса электронной почты
email_changed:
explanation: 'E-mail адрес вашей учётной записи будет изменён на:'
extra: Если вы не меняли e-mail адрес, возможно кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором узла, если вы уже потеряли доступ к ней.
subject: 'Mastodon: Изменён e-mail адрес'
title: Новый адрес e-mail
explanation: 'Ваш адрес электронной почты будет изменён на:'
extra: Если вы не меняли адрес электронной почты, возможно, кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором сервера, если вы уже потеряли к ней доступ.
subject: 'Mastodon: Адрес электронной почты изменён'
title: Адрес электронной почты изменён
password_change:
explanation: Пароль Вашей учётной записи был изменён.
extra: Если вы не меняли пароль, возможно кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором узла, если вы уже потеряли доступ к ней.
subject: 'Mastodon: Пароль изменен'
explanation: Ваш пароль был изменён.
extra: Если вы не меняли пароль, возможно, кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором сервера, если вы уже потеряли к ней доступ.
subject: 'Mastodon: Пароль изменён'
title: Пароль изменён
reconfirmation_instructions:
explanation: Для завершения смены e-mail, нажмите кнопку ниже.
extra: Если вы не изменяли e-mail, пожалуйста, игнорируйте это письмо. Новый адрес не будет привязан к учётной записи, пока вы не перейдёте по ссылке ниже.
subject: 'Mastodon: Подтвердите свой новый e-mail на %{instance}'
title: Подтвердите e-mail адрес
explanation: Чтобы завершить изменение адреса электронной почты, подтвердите новый адрес.
extra: Если запрос инициировали не вы, пожалуйста, проигнорируйте это письмо. Новый адрес не будет привязан к учётной записи, пока вы не перейдёте по ссылке выше.
subject: 'Mastodon: Подтвердите новый адрес электронной почты на %{instance}'
title: Подтверждение адреса электронной почты
reset_password_instructions:
action: Смена пароля
action: Сменить пароль
explanation: Вы запросили новый пароль для вашей учётной записи.
extra: Если это сделали не вы, пожалуйста, игнорируйте письмо. Ваш пароль не будет изменён, пока вы не перейдёте по ссылке выше и не создадите новый пароль.
subject: 'Mastodon: Инструкция по сбросу пароля'
title: Сброс пароля
extra: Если вы не запрашивали изменение пароля, проигнорируйте это письмо. Ваш пароль не будет изменён, пока вы не перейдёте по ссылке и не введёте новый пароль.
subject: 'Mastodon: Инструкции по восстановлению пароля'
title: Восстановление пароля
two_factor_disabled:
explanation: Вход в систему теперь возможен только с использованием адреса электронной почты и пароля.
subject: 'Mastodon: Двухфакторная авторизация отключена'
subtitle: Двухфакторная аутентификация для вашей учетной записи была отключена.
title: 2ФА отключена
explanation: Теперь вход возможен с использованием одних лишь адреса электронной почты и пароля.
subject: 'Mastodon: Двухфакторная аутентификация отключена'
subtitle: Двухфакторная аутентификация отключена для вашей учетной записи.
title: 2FA отключена
two_factor_enabled:
explanation: Для входа в систему потребуется токен, сгенерированный сопряженным приложением TOTP.
subject: 'Mastodon: Настроена двухфакторная авторизация'
subtitle: Для вашей учетной записи была включена двухфакторная аутентификация.
title: 2ФА включена
explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением TOTP.
subject: 'Mastodon: Двухфакторная аутентификация включена'
subtitle: Двухфакторная аутентификация включена для вашей учётной записи.
title: 2FA включена
two_factor_recovery_codes_changed:
explanation: Предыдущие резервные коды были аннулированы и созданы новые.
subject: 'Mastodon: Резервные коды двуфакторной авторизации обновлены'
subtitle: Предыдущие коды восстановления были аннулированы и сгенерированы новые.
title: Коды восстановления 2FA изменены
explanation: Прежние резервные коды были аннулированы, и были созданы новые.
subject: 'Mastodon: Резервные коды двухфакторной аутентификации пересозданы'
subtitle: Прежние резервные коды были аннулированы, и были созданы новые.
title: Резервные коды 2FA изменены
unlock_instructions:
subject: 'Mastodon: Инструкция по разблокировке'
subject: 'Mastodon: Инструкции по снятию блокировки учётной записи'
webauthn_credential:
added:
explanation: Следующий ключ безопасности был добавлен в вашу учётную запись
subject: 'Мастодон: Новый ключ безопасности'
title: Был добавлен новый ключ безопасности
explanation: Новый электронный ключ добавлен для вашей учётной записи
subject: 'Mastodon: Новый электронный ключ'
title: Добавлен новый электронный ключ
deleted:
explanation: Следующий ключ безопасности был удален из вашей учётной записи
subject: 'Мастодон: Ключ Безопасности удален'
title: Один из ваших защитных ключей был удален
explanation: Один из ваших электронных ключей удалён и больше не сможет быть использован для входа в вашу учётную запись
subject: 'Mastodon: Электронный ключ удалён'
title: Один из ваших электронных ключей удалён
webauthn_disabled:
explanation: Аутентификация с помощью ключей безопасности была отключена для вашей учетной записи.
extra: Теперь вход в систему возможен только с использованием токена, сгенерированного сопряженным приложением TOTP.
subject: 'Мастодон: Аутентификация с ключами безопасности отключена'
title: Ключи безопасности отключены
explanation: Аутентификация по электронным ключам деактивирована для вашей учетной записи.
extra: Теперь вход возможен с использованием только лишь одноразового кода, сгенерированного сопряжённым приложением TOTP.
subject: 'Mastodon: Аутентификация по электронным ключам деактивирована'
title: Вход по электронным ключам деактивирован
webauthn_enabled:
explanation: Для вашей учетной записи включена аутентификация по ключу безопасности.
extra: Теперь ваш ключ безопасности можно использовать для входа в систему.
subject: 'Мастодон: Включена аутентификация по ключу безопасности'
title: Ключи безопасности включены
explanation: Аутентификация по электронным ключам активирована для вашей учетной записи.
extra: Теперь ваш электронный ключ можно использовать для входа.
subject: 'Mastodon: Аутентификация по электронным ключам активирована'
title: Вход по электронным ключам активирован
omniauth_callbacks:
failure: Не получилось аутентифицировать вас с помощью %{kind} по следующей причине - "%{reason}".
success: Аутентификация с помощью учётной записи %{kind} прошла успешно.
failure: Вы не можете войти под учётной записью %{kind}, так как «%{reason}».
success: Вход выполнен под учётной записью %{kind}.
passwords:
no_token: Вы можете получить доступ к этой странице, только перейдя по ссылке в e-mail для сброса пароля. Если вы действительно перешли по такой ссылке, пожалуйста, удостоверьтесь, что ссылка была введена полностью и без изменений.
send_instructions: Вы получите e-mail с инструкцией по сбросу пароля в течение нескольких минут.
send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail со ссылкой для сброса пароля в течение нескольких минут.
updated: Ваш пароль был успешно изменен. Вход выполнен.
updated_not_active: Ваш пароль был успешно изменен.
no_token: Доступ к этой странице возможен только по ссылке из письма о восстановлении пароля. Если вы перешли по такой ссылке, пожалуйста, убедитесь, что вы скопировали всю ссылку целиком.
send_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по восстановлению пароля. Если письмо не приходит, проверьте папку «Спам».
send_paranoid_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по восстановлению пароля. Если письмо не приходит, проверьте папку «Спам».
updated: Ваш пароль изменён. Теперь вы авторизованы.
updated_not_active: Ваш пароль изменён.
registrations:
destroyed: До свидания! Ваша учётная запись была успешно удалена. Мы надеемся скоро увидеть вас снова.
update_needs_confirmation: Данные учётной записи обновлены, но нам необходимо подтвердить ваш новый e-mail адрес. Проверьте почту и перейдите по ссылке из письма. Если оно не приходит, проверьте папку «спам».
updated: Ваша учётная запись успешно обновлена.
destroyed: До свидания! Ваша учётная запись удалена. Надеемся увидеть вас снова.
update_needs_confirmation: Вы успешно обновили данные своей учётной записи, но необходимо подтвердить новый адрес электронной почты. Пожалуйста, проверьте свой почтовый ящик и перейдите по ссылке, чтобы закончить процедуру проверки нового адреса. Если письмо не приходит, проверьте папку «Спам».
updated: Ваша учётная запись обновлена.
sessions:
already_signed_out: Выход прошел успешно.
signed_in: Вход прошел успешно.
signed_out: Выход прошел успешно.
already_signed_out: Выход выполнен.
signed_in: Вход выполнен.
signed_out: Выход выполнен.
unlocks:
send_instructions: Вы получите e-mail с инструкцией по разблокировке вашей учётной записи в течение нескольких минут.
send_paranoid_instructions: Если ваша учётная запись существует, вы получите e-mail с инструкцией по её разблокировке в течение нескольких минут.
unlocked: Ваша учётная запись был успешно разблокирована. Пожалуйста, войдите для продолжения.
send_instructions: В течение нескольких минут вы получите письмо с инструкциями по разблокировке учётной записи. Если письмо не приходит, проверьте папку «Спам».
send_paranoid_instructions: Если ваша учётная запись существует, то в течение нескольких минут вы получите письмо с инструкциями по её разблокировке. Если письмо не приходит, проверьте папку «Спам».
unlocked: Ваша учётная запись разблокирована. Теперь вы можете войти.
errors:
messages:
already_confirmed: уже подтвержден, пожалуйста, попробуйте войти
confirmation_period_expired: не был подтвержден в течение %{period}, пожалуйста, запросите новый
expired: истек, пожалуйста, запросите новый
already_confirmed: уже подтверждён. Пожалуйста, попробуйте войти
confirmation_period_expired: не был подтверждён в течение %{period}. Пожалуйста, повторите запрос на подтверждение
expired: истёк. Пожалуйста, запросите новый код
not_found: не найден
not_locked: не был заблокирован
not_locked: не заблокирован
not_saved:
few: "%{count} ошибки помешали сохранению этого %{resource}:"
many: "%{count} ошибок помешали сохранению этого %{resource}:"
one: '1 ошибка помешала сохранению этого %{resource}:'
other: "%{count} ошибок помешали сохранению этого %{resource}:"
few: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
many: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
one: "%{resource}: сохранение не удалось из-за %{count} ошибки:"
other: "%{resource}: сохранение не удалось из-за %{count} ошибок:"

View File

@ -15,43 +15,43 @@ ru:
fragment_present: не может содержать фрагмент.
invalid_uri: должен быть правильным URI.
relative_uri: должен быть абсолютным URI.
secured_uri: нужен HTTPS/SSL URI.
secured_uri: должен быть HTTPS/SSL URI.
doorkeeper:
applications:
buttons:
authorize: Авторизовать
cancel: Отменить
cancel: Отмена
destroy: Удалить
edit: Изменить
submit: Принять
edit: Редактировать
submit: Готово
confirmations:
destroy: Вы уверены?
edit:
title: Изменить приложение
title: Редактирование приложения
form:
error: Ой! Проверьте Вашу форму на возможные ошибки
error: Упс! Проверьте форму на наличие ошибок
help:
native_redirect_uri: Используйте %{native_redirect_uri} для локального тестирования
redirect_uri: Используйте по одной строке на URI
redirect_uri: По одному URI на строку
scopes: Разделяйте список разрешений пробелами. Оставьте незаполненным для использования разрешений по умолчанию.
index:
application: Приложение
callback_url: URL-адреса обратного вызова
callback_url: Callback-адрес URL
delete: Удалить
empty: У вас нет созданных приложений.
empty: Вы ещё не создали ни одного приложения.
name: Название
new: Новое приложение
new: Создать приложение
scopes: Разрешения
show: Показывать
show: Открыть
title: Ваши приложения
new:
title: Создание приложения
title: Создать приложение
show:
actions: Действия
application_id: Ключ клиента
callback_urls: URL-адреса обратного вызова
application_id: ID приложения
callback_urls: Callback-адреса URL
scopes: Разрешения
secret: Секрет
secret: Секретный ключ
title: 'Приложение: %{name}'
authorizations:
buttons:
@ -60,47 +60,47 @@ ru:
error:
title: Произошла ошибка
new:
prompt_html: "%{client_name} хочет получить доступ к вашему аккаунту. <strong>Принимайте запрос только в том случае, если узнаёте, откуда он, и доверяете источнику.</strong>"
review_permissions: Просмотр разрешений
prompt_html: Приложение %{client_name} запрашивает доступ к вашей учётной записи. <strong>Не принимайте этот запрос, если он исходит из незнакомого или недоверенного источника.</strong>
review_permissions: Запрашиваемые разрешения
title: Требуется авторизация
show:
title: Скопируйте этот код авторизации и вставьте его в приложении.
title: Скопируйте этот код авторизации и вставьте его в приложение.
authorized_applications:
buttons:
revoke: Отозвать авторизацию
revoke: Отозвать доступ
confirmations:
revoke: Вы уверены?
index:
authorized_at: Доступ получен %{date}
description_html: Это приложения, которые могут получить доступ к вашей учетной записи с помощью API. Если здесь есть приложения, которые вы не узнаете, или приложения, работающие неправильно, вы можете отозвать их доступ.
last_used_at: Последнее использование %{date}
description_html: Это приложения, которые могут получить доступ к вашей учётной записи с помощью API. Если здесь есть приложения, которые вы не узнаёте, или приложения, работающие неправильно, вы можете отозвать им доступ.
last_used_at: В последний раз использовалось %{date}
never_used: Не использовалось
scopes: Разрешения
superapp: Внутреннее
title: Ваши авторизованные приложения
superapp: Служебное приложение
title: Связанные приложения
errors:
messages:
access_denied: Владелец ресурса или сервер авторизации ответил отказом на Ваш запрос.
credential_flow_not_configured: Поток с предоставлением клиенту пароля завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_from_credentials не был сконфигурирован.
invalid_client: Клиентская аутентификация завершилась неудачей (неизвестный клиент, не включена клиентская аутентификация, или метод аутентификации не поддерживается.
invalid_code_challenge_method: Метод проверки кода должен быть S256, простой не годится.
invalid_grant: Предоставленный доступ некорректен, истек, отозван, не совпадает с URI перенаправления, использованным в запросе авторизации, или был выпущен для другого клиента.
invalid_redirect_uri: Включенный URI перенаправления некорректен.
access_denied: Владелец ресурса или сервер авторизации ответил отказом на ваш запрос.
credential_flow_not_configured: Процесс Resource Owner Password Credentials завершился неудачей, поскольку параметр конфигурации Doorkeeper.configure.resource_owner_from_credentials не был задан.
invalid_client: 'Не удалось аутентифицировать клиент по одной из следующих причин: неизвестный клиент; отсутствует аутентификация клиента; неподдерживаемый метод аутентификации.'
invalid_code_challenge_method: Функция хеширования для механизма PKCE должна быть установлена в значение S256, метод PLAIN не поддерживается.
invalid_grant: Предоставленное разрешение на авторизацию либо недействительно, либо истекло, либо отозвано, либо не соответствует использованному в запросе на авторизацию URI перенаправления, либо было выдано для другого клиента.
invalid_redirect_uri: Предоставленный URI перенаправления недействителен.
invalid_request:
missing_param: 'Отсутствует обязательный параметр: %{value}.'
request_not_authorized: Запрос должен быть авторизован. Обязательный параметр для авторизации запроса отсутствует или недействителен.
unknown: В запросе отсутствует обязательный параметр, включено неподдерживаемое значение параметра или он имеет иной формат.
invalid_resource_owner: Предоставленные данные владельца ресурса некорректны, или владелец ресурса не может быть найден
invalid_scope: Запрошенное разрешение некорректно, неизвестно или неверно сформировано.
unknown: В запросе отсутствует обязательный параметр либо присутствует неподдерживаемое значение параметра, или запрос является недействительным по какой-либо ещё причине.
invalid_resource_owner: Предоставленные данные владельца ресурса недействительны, или владелец ресурса не найден
invalid_scope: Запрошенное разрешение недействительно, неизвестно или имеет неправильный формат.
invalid_token:
expired: Токен доступа истек
revoked: Токен доступа был отменен
unknown: Токен доступа некорректен
resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_authenticator не был сконфигурирован.
server_error: Сервер авторизации встретился с неожиданной ошибкой, не позволившей ему выполнить запрос.
temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или профилактики.
expired: Срок действия токена доступа истёк
revoked: Токен доступа был отозван
unknown: Токен доступа недействителен
resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр конфигурации Doorkeeper.configure.resource_owner_authenticator не был задан.
server_error: На сервере авторизации произошла непредвиденная ошибка, не позволившая ему выполнить запрос.
temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или технического обслуживания.
unauthorized_client: Клиент не авторизован для выполнения этого запроса с использованием этого метода.
unsupported_grant_type: Тип авторизации не поддерживается сервером авторизации.
unsupported_grant_type: Тип разрешения на авторизацию не поддерживается сервером авторизации.
unsupported_response_type: Сервер авторизации не поддерживает этот тип ответа.
flash:
applications:
@ -112,33 +112,33 @@ ru:
notice: Приложение обновлено.
authorized_applications:
destroy:
notice: Авторизация приложения отозвана.
notice: Приложению был отозван доступ.
grouped_scopes:
access:
read: Доступ только для чтения
read/write: Доступ на чтение и запись
read/write: Доступ для чтения и записи
write: Доступ только для записи
title:
accounts: Учётные записи
admin/accounts: Управление учётными записями
admin/accounts: Управление учётными записями пользователей
admin/all: Все административные функции
admin/reports: Управление отчётами
all: Полный доступ к вашей учетной записи Mastodon
admin/reports: Управление жалобами
all: Полный доступ к вашей учётной записи Mastodon
blocks: Блокировки
bookmarks: Закладки
conversations: Диалоги
conversations: Беседы
crypto: Сквозное шифрование
favourites: Избранные
favourites: Избранное
filters: Фильтры
follow: Подписки, заглушенные и заблокированные
follow: Подписки, а также списки игнорируемых и заблокированных пользователей
follows: Подписки
lists: Списки
media: Медиафайлы
mutes: Игнорирует
mutes: Игнорируемые пользователи
notifications: Уведомления
profile: Ваш профиль Mastodon
push: Push-уведомления
reports: Обращения
reports: Жалобы
search: Поиск
statuses: Посты
layouts:
@ -150,49 +150,49 @@ ru:
title: Требуется авторизация OAuth
scopes:
admin:read: читать все данные на сервере
admin:read:accounts: читать конфиденциальную информацию всех учётных записей
admin:read:canonical_email_blocks: чтение конфиденциальной информации всех канонических блоков электронной почты
admin:read:domain_allows: чтение конфиденциальной информации для всего домена позволяет
admin:read:domain_blocks: чтение конфиденциальной информации для всего домена позволяет
admin:read:email_domain_blocks: читать конфиденциальную информацию обо всех блоках домена электронной почты
admin:read:ip_blocks: читать конфиденциальную информацию обо всех IP-блоках
admin:read:reports: читать конфиденциальную информацию о всех жалобах и учётных записях с жалобами
admin:write: модифицировать все данные на сервере
admin:write:accounts: производить модерацию учётных записей
admin:write:canonical_email_blocks: выполнять действия по модерации канонических блоков электронной почты
admin:write:domain_allows: производить модерацию учётных записей
admin:write:domain_blocks: выполнять модерационные действия над блокировкой домена
admin:write:email_domain_blocks: выполнять действия по модерации блоков домена электронной почты
admin:write:ip_blocks: выполнять модерационные действия над блокировками IP
admin:write:reports: производить модерацию жалоб
crypto: использ. сквозное шифрование
follow: управлять подписками и списком блокировок
profile: данные вашего профиля только для чтения
admin:read:accounts: читать конфиденциальные сведения обо всех учётных записях
admin:read:canonical_email_blocks: читать конфиденциальные сведения обо всех блокировках по каноническому адресу электронной почты
admin:read:domain_allows: читать конфиденциальные сведения обо всех разрешённых доменах
admin:read:domain_blocks: читать конфиденциальные сведения обо всех заблокированных доменах
admin:read:email_domain_blocks: читать конфиденциальные сведения обо всех блокировках по домену электронной почты
admin:read:ip_blocks: читать конфиденциальные сведения обо всех блокировках по IP-адресу
admin:read:reports: читать конфиденциальные сведения обо всех жалобах и учётных записях с жалобами
admin:write: вносить изменения во все данные на сервере
admin:write:accounts: осуществлять модерацию применительно к учётным записям
admin:write:canonical_email_blocks: осуществлять модерацию применительно к блокировкам по каноническому адресу электронной почты
admin:write:domain_allows: осуществлять модерацию применительно к разрешённым доменам
admin:write:domain_blocks: осуществлять модерацию применительно к заблокированным доменам
admin:write:email_domain_blocks: осуществлять модерацию применительно к блокировкам по домену электронной почты
admin:write:ip_blocks: осуществлять модерацию применительно к блокировкам по IP-адресу
admin:write:reports: осуществлять модерацию применительно к жалобам
crypto: использовать сквозное шифрование
follow: вносить изменения в отношения с другими пользователями
profile: читать исключительно сведения о вашем профиле
push: получать push-уведомления
read: просматривать данные вашей учётной записи
read:accounts: видеть информацию об учётных записях
read:blocks: видеть ваши блокировки
read:bookmarks: видеть ваши закладки
read:favourites: видеть ваше избранное
read:filters: видеть ваши фильтры
read:follows: видеть ваши подписки
read:lists: видеть ваши списки
read:mutes: смотреть список игнорируемых
read:notifications: получать уведомления
read:reports: видеть ваши жалобы
read: читать данные вашей учётной записи
read:accounts: иметь доступ к информации об учётных записях
read:blocks: иметь доступ к вашим блокировкам
read:bookmarks: иметь доступ к вашим закладкам
read:favourites: иметь доступ к списку постов, которые вы добавили в избранное
read:filters: иметь доступ к вашим фильтрам
read:follows: иметь доступ к вашим подпискам
read:lists: иметь доступ к вашим спискам
read:mutes: иметь доступ к списку пользователей, которых вы игнорируете
read:notifications: иметь доступ к вашим уведомлениям
read:reports: иметь доступ к вашим жалобам
read:search: использовать поиск
read:statuses: видеть все ваши посты
write: изменять все данные вашей учётной записи
write:accounts: редактировать ваш профиль
read:statuses: иметь доступ ко всем постам
write: вносить изменения во все данные вашей учётной записи
write:accounts: вносить изменения в ваш профиль
write:blocks: блокировать учётные записи и домены
write:bookmarks: добавлять посты в закладки
write:conversations: игнорировать и удалить разговоры
write:favourites: добавить посты в избранное
write:conversations: игнорировать и удалять беседы
write:favourites: добавлять посты в избранное
write:filters: создавать фильтры
write:follows: подписываться на людей
write:lists: создавать списки
write:media: загружать медиафайлы
write:mutes: игнорировать людей и обсуждения
write:notifications: очищать список уведомлений
write:reports: отправлять жалобы на других
write:reports: отправлять жалобы на других пользователей
write:statuses: публиковать посты

View File

@ -578,6 +578,13 @@ en:
all: All
limited: Limited
title: Moderation
moderation_notes:
create: Add Moderation Note
created_msg: Instance moderation note successfully created!
description_html: View and leave notes for other moderators and your future self
destroyed_msg: Instance moderation note successfully deleted!
placeholder: Information about this instance, actions taken, or anything else that will help you moderate this instance in the future.
title: Moderation Notes
private_comment: Private comment
public_comment: Public comment
purge: Purge

View File

@ -91,6 +91,8 @@ namespace :admin do
post :restart_delivery
post :stop_delivery
end
resources :moderation_notes, controller: 'instances/moderation_notes', only: [:create, :destroy]
end
resources :rules, only: [:index, :new, :create, :edit, :update, :destroy] do

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class AddSuperappToOauthApplications < ActiveRecord::Migration[5.0]
class AddSuperappToOAuthApplications < ActiveRecord::Migration[5.0]
def change
add_column :oauth_applications, :superapp, :boolean, default: false, null: false
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class AddWebsiteToOauthApplication < ActiveRecord::Migration[5.0]
class AddWebsiteToOAuthApplication < ActiveRecord::Migration[5.0]
def change
add_column :oauth_applications, :website, :string
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class AddLastUsedAtToOauthAccessTokens < ActiveRecord::Migration[6.1]
class AddLastUsedAtToOAuthAccessTokens < ActiveRecord::Migration[6.1]
def change
safety_assured do
change_table(:oauth_access_tokens, bulk: true) do |t|

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateInstanceModerationNotes < ActiveRecord::Migration[8.0]
def change
create_table :instance_moderation_notes do |t|
t.string :domain, null: false
t.belongs_to :account, foreign_key: { on_delete: :cascade }, index: false, null: false
t.text :content
t.timestamps
t.index ['domain'], name: 'index_instance_moderation_notes_on_domain'
end
end
end

View File

@ -2,7 +2,7 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class OptimizeNullIndexOauthAccessTokensRefreshToken < ActiveRecord::Migration[5.2]
class OptimizeNullIndexOAuthAccessTokensRefreshToken < ActiveRecord::Migration[5.2]
include Mastodon::MigrationHelpers
disable_ddl_transaction!

View File

@ -2,7 +2,7 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class OptimizeNullIndexOauthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2]
class OptimizeNullIndexOAuthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2]
include Mastodon::MigrationHelpers
disable_ddl_transaction!

View File

@ -191,8 +191,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
t.boolean "hide_collections"
t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version"
t.datetime "sensitized_at", precision: nil
t.integer "suspension_origin"
t.datetime "sensitized_at", precision: nil
t.boolean "trendable"
t.datetime "reviewed_at", precision: nil
t.datetime "requested_review_at", precision: nil
@ -580,6 +580,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
t.index ["user_id"], name: "index_identities_on_user_id"
end
create_table "instance_moderation_notes", force: :cascade do |t|
t.string "domain", null: false
t.bigint "account_id", null: false
t.text "content"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["domain"], name: "index_instance_moderation_notes_on_domain"
end
create_table "invites", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "code", default: "", null: false
@ -595,12 +604,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
end
create_table "ip_blocks", force: :cascade do |t|
t.inet "ip", default: "0.0.0.0", null: false
t.integer "severity", default: 0, null: false
t.datetime "expires_at", precision: nil
t.text "comment", default: "", null: false
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.datetime "expires_at", precision: nil
t.inet "ip", default: "0.0.0.0", null: false
t.integer "severity", default: 0, null: false
t.text "comment", default: "", null: false
t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
end
@ -1372,6 +1381,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
add_foreign_key "generated_annual_reports", "accounts"
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
add_foreign_key "instance_moderation_notes", "accounts", on_delete: :cascade
add_foreign_key "invites", "users", on_delete: :cascade
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade

View File

@ -251,8 +251,7 @@ export default tseslint.config([
devDependencies: [
'eslint.config.mjs',
'app/javascript/mastodon/performance.js',
'app/javascript/mastodon/test_setup.js',
'app/javascript/mastodon/test_helpers.tsx',
'app/javascript/testing/**/*',
'app/javascript/**/__tests__/**',
'app/javascript/**/*.stories.ts',
'app/javascript/**/*.stories.tsx',

View File

@ -1,17 +0,0 @@
# frozen_string_literal: true
# Restore compatibility with Redis < 6.2
module Stoplight
module DataStore
module RedisExtensions
def query_failures(light, transaction: @redis)
window_start = Time.now.to_i - light.window_size
transaction.zrevrangebyscore(failures_key(light), Float::INFINITY, window_start)
end
end
end
end
Stoplight::DataStore::Redis.prepend(Stoplight::DataStore::RedisExtensions)

View File

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe Oauth::AuthorizationsController do
RSpec.describe OAuth::AuthorizationsController do
let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') }
describe 'GET #new' do

View File

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe Oauth::AuthorizedApplicationsController do
RSpec.describe OAuth::AuthorizedApplicationsController do
render_views
describe 'GET #index' do

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
Fabricator(:instance_moderation_note) do
domain { sequence(:domain) { |i| "#{i}#{Faker::Internet.domain_name}" } }
account { Fabricate.build(:account) }
content { Faker::Lorem.sentence }
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Doorkeeper::AccessGrant do
describe 'Validations' do
subject { Fabricate :access_grant }
it { is_expected.to validate_presence_of(:application_id) }
it { is_expected.to validate_presence_of(:expires_in) }
it { is_expected.to validate_presence_of(:redirect_uri) }
it { is_expected.to validate_presence_of(:token) }
end
describe 'Scopes' do
describe '.expired' do
let!(:unexpired) { Fabricate :access_grant, expires_in: 10.hours }
let!(:expired) do
travel_to 10.minutes.ago do
Fabricate :access_grant, expires_in: 5.minutes
end
end
it 'returns records past their expired time' do
expect(described_class.expired)
.to include(expired)
.and not_include(unexpired)
end
end
describe '.revoked' do
let!(:revoked) { Fabricate :access_grant, revoked_at: 10.minutes.ago }
let!(:unrevoked) { Fabricate :access_grant, revoked_at: 10.minutes.from_now }
it 'returns records past their expired time' do
expect(described_class.revoked)
.to include(revoked)
.and not_include(unrevoked)
end
end
end
end

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Doorkeeper::AccessToken do
describe 'Associations' do
it { is_expected.to have_many(:web_push_subscriptions).class_name('Web::PushSubscription').inverse_of(:access_token) }
end
describe 'Validations' do
subject { Fabricate :access_token }
it { is_expected.to validate_presence_of(:token) }
end
describe 'Scopes' do
describe '.expired' do
let!(:unexpired) { Fabricate :access_token, expires_in: 10.hours }
let!(:expired) do
travel_to 10.minutes.ago do
Fabricate :access_token, expires_in: 5.minutes
end
end
it 'returns records past their expired time' do
expect(described_class.expired)
.to include(expired)
.and not_include(unexpired)
end
end
describe '.revoked' do
let!(:revoked) { Fabricate :access_token, revoked_at: 10.minutes.ago }
let!(:unrevoked) { Fabricate :access_token, revoked_at: 10.minutes.from_now }
it 'returns records past their expired time' do
expect(described_class.revoked)
.to include(revoked)
.and not_include(unrevoked)
end
end
end
describe '#revoke' do
let(:record) { Fabricate :access_token, revoked_at: 10.days.from_now }
it 'marks the record as revoked' do
expect { record.revoke }
.to change(record, :revoked_at).to(be_within(1).of(Time.now.utc))
end
end
describe '#update_last_used' do
let(:record) { Fabricate :access_token, last_used_at: nil, last_used_ip: nil }
let(:request) { instance_double(ActionDispatch::Request, remote_ip: '1.1.1.1') }
it 'marks the record as revoked' do
expect { record.update_last_used(request) }
.to change(record, :last_used_at).to(be_within(1).of(Time.now.utc))
.and change(record, :last_used_ip).to(IPAddr.new('1.1.1.1'))
end
end
end

View File

@ -8,8 +8,45 @@ RSpec.describe Doorkeeper::Application do
end
describe 'Validations' do
subject { Fabricate :application }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:uid) }
it { is_expected.to validate_length_of(:name).is_at_most(described_class::APP_NAME_LIMIT) }
it { is_expected.to validate_length_of(:redirect_uri).is_at_most(described_class::APP_REDIRECT_URI_LIMIT) }
it { is_expected.to validate_length_of(:website).is_at_most(described_class::APP_WEBSITE_LIMIT) }
end
describe '#redirect_uris' do
subject { Fabricate.build(:application, redirect_uri:).redirect_uris }
context 'with single value' do
let(:redirect_uri) { 'https://test.example/one' }
it { is_expected.to be_an(Array).and(eq(['https://test.example/one'])) }
end
context 'with multiple values' do
let(:redirect_uri) { "https://test.example/one\nhttps://test.example/two" }
it { is_expected.to be_an(Array).and(eq(['https://test.example/one', 'https://test.example/two'])) }
end
end
describe '#confirmation_redirect_uri' do
subject { Fabricate.build(:application, redirect_uri:).confirmation_redirect_uri }
context 'with single value' do
let(:redirect_uri) { 'https://test.example/one ' }
it { is_expected.to eq('https://test.example/one') }
end
context 'with multiple values' do
let(:redirect_uri) { "https://test.example/one \nhttps://test.example/two " }
it { is_expected.to eq('https://test.example/one') }
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe InstanceModerationNote do
describe 'chronological' do
it 'returns the instance notes sorted by oldest first' do
instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain('mastodon.example'))
note1 = Fabricate(:instance_moderation_note, domain: instance.domain)
note2 = Fabricate(:instance_moderation_note, domain: instance.domain)
expect(instance.moderation_notes.chronological).to eq [note1, note2]
end
end
describe 'validations' do
it 'is invalid if the content is empty' do
note = Fabricate.build(:instance_moderation_note, domain: 'mastodon.example', content: '')
expect(note.valid?).to be false
end
it 'is invalid if content is longer than character limit' do
note = Fabricate.build(:instance_moderation_note, domain: 'mastodon.example', content: comment_over_limit)
expect(note.valid?).to be false
end
it 'is valid even if the instance does not exist yet' do
note = Fabricate.build(:instance_moderation_note, domain: 'non-existent.example', content: 'test comment')
expect(note.valid?).to be true
end
def comment_over_limit
Faker::Lorem.paragraph_by_chars(number: described_class::CONTENT_SIZE_LIMIT * 2)
end
end
end

View File

@ -3,9 +3,9 @@
require 'rails_helper'
RSpec.describe Instance do
describe 'Scopes' do
before { described_class.refresh }
before { described_class.refresh }
describe 'Scopes' do
describe '#searchable' do
let(:expected_domain) { 'host.example' }
let(:blocked_domain) { 'other.example' }

View File

@ -166,6 +166,34 @@ RSpec.describe User do
end
end
describe '#email_domain' do
subject { described_class.new(email: email).email_domain }
context 'when value is nil' do
let(:email) { nil }
it { is_expected.to be_nil }
end
context 'when value is blank' do
let(:email) { '' }
it { is_expected.to be_nil }
end
context 'when value has valid domain' do
let(:email) { 'user@host.example' }
it { is_expected.to eq('host.example') }
end
context 'when value has no split' do
let(:email) { 'user$host.example' }
it { is_expected.to be_nil }
end
end
describe '#update_sign_in!' do
context 'with an existing user' do
let!(:user) { Fabricate :user, last_sign_in_at: 10.days.ago, current_sign_in_at: 1.hour.ago, sign_in_count: 123 }

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin Report Notes' do
describe 'POST /admin/instance/moderation_notes' do
before { sign_in Fabricate(:admin_user) }
it 'gracefully handles invalid nested params' do
post admin_instance_moderation_notes_path(instance_id: 'mastodon.test', instance_note: 'invalid')
expect(response)
.to have_http_status(400)
end
end
end

View File

@ -4,9 +4,9 @@ require 'rails_helper'
RSpec.describe 'Admin Instances' do
describe 'GET /admin/instances/:id' do
context 'with an unknown domain' do
before { sign_in Fabricate(:admin_user) }
before { sign_in Fabricate(:admin_user) }
context 'with an unknown domain' do
it 'returns http success' do
get admin_instance_path(id: 'unknown.example')
@ -14,5 +14,14 @@ RSpec.describe 'Admin Instances' do
.to have_http_status(200)
end
end
context 'with an invalid domain' do
it 'returns http success' do
get admin_instance_path(id: ' ')
expect(response)
.to have_http_status(200)
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe 'Oauth Userinfo Endpoint' do
RSpec.describe 'OAuth Userinfo Endpoint' do
include RoutingHelper
let(:user) { Fabricate(:user) }

View File

@ -35,7 +35,7 @@ RSpec.describe 'Admin::AccountModerationNotes' do
end
def delete_note
within('.report-notes__item__actions') do
within('.report-notes__item:first-child .report-notes__item__actions') do
click_on I18n.t('admin.reports.notes.delete')
end
end

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Admin::Instances::ModerationNotesController' do
let(:current_user) { Fabricate(:admin_user) }
let(:instance_domain) { 'mastodon.example' }
before { sign_in current_user }
describe 'Managing instance moderation notes' do
it 'saves and then deletes a record' do
visit admin_instance_path(instance_domain)
fill_in 'instance_moderation_note_content', with: ''
expect { submit_form }
.to not_change(InstanceModerationNote, :count)
expect(page)
.to have_content(/error below/)
fill_in 'instance_moderation_note_content', with: 'Test message ' * InstanceModerationNote::CONTENT_SIZE_LIMIT
expect { submit_form }
.to not_change(InstanceModerationNote, :count)
expect(page)
.to have_content(/error below/)
fill_in 'instance_moderation_note_content', with: 'Test message'
expect { submit_form }
.to change(InstanceModerationNote, :count).by(1)
expect(page)
.to have_current_path(admin_instance_path(instance_domain))
expect(page)
.to have_content(I18n.t('admin.instances.moderation_notes.created_msg'))
expect { delete_note }
.to change(InstanceModerationNote, :count).by(-1)
expect(page)
.to have_content(I18n.t('admin.instances.moderation_notes.destroyed_msg'))
end
def submit_form
click_on I18n.t('admin.instances.moderation_notes.create')
end
def delete_note
within('.report-notes__item:first-child .report-notes__item__actions') do
click_on I18n.t('admin.reports.notes.delete')
end
end
end
end

View File

@ -29,10 +29,7 @@
"vite.config.mts",
"vitest.config.mts",
"config/vite",
"app/javascript/mastodon",
"app/javascript/entrypoints",
"app/javascript/types",
".storybook/*.ts",
".storybook/*.tsx"
"app/javascript",
".storybook/*"
]
}