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 ActivityPub: activitypub
DeepL: deepl DeepL: deepl
FetchOEmbedService: fetch_oembed_service FetchOEmbedService: fetch_oembed_service
OAuth: oauth
OEmbedController: oembed_controller OEmbedController: oembed_controller
OStatus: ostatus OStatus: ostatus

View File

@ -11,7 +11,21 @@ const config: StorybookConfig = {
name: '@storybook/react-vite', name: '@storybook/react-vite',
options: {}, 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; export default config;

View File

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

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 def show
authorize :instance, :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) @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) @action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
end end
@ -52,7 +55,8 @@ module Admin
private private
def set_instance 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 end
def set_instances def set_instances

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import renderer from 'react-test-renderer'; 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'; 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'; import Column from '../column';

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Porušení pravidla", "report_notification.categories.violation": "Porušení pravidla",
"report_notification.categories.violation_sentence": "porušení pravidel", "report_notification.categories.violation_sentence": "porušení pravidel",
"report_notification.open": "Otevřít hlášení", "report_notification.open": "Otevřít hlášení",
"search.clear": "Vymazat hledání",
"search.no_recent_searches": "Žádná nedávná vyhledávání", "search.no_recent_searches": "Žádná nedávná vyhledávání",
"search.placeholder": "Hledat", "search.placeholder": "Hledat",
"search.quick_action.account_search": "Profily odpovídající {x}", "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": "Regelverstoß",
"report_notification.categories.violation_sentence": "Regelverletzung", "report_notification.categories.violation_sentence": "Regelverletzung",
"report_notification.open": "Meldung öffnen", "report_notification.open": "Meldung öffnen",
"search.clear": "Suchanfrage löschen",
"search.no_recent_searches": "Keine früheren Suchanfragen", "search.no_recent_searches": "Keine früheren Suchanfragen",
"search.placeholder": "Suchen", "search.placeholder": "Suchen",
"search.quick_action.account_search": "Profile passend zu {x}", "search.quick_action.account_search": "Profile passend zu {x}",

View File

@ -1,6 +1,7 @@
{ {
"about.blocks": "Reguligitaj serviloj", "about.blocks": "Reguligitaj serviloj",
"about.contact": "Kontakto:", "about.contact": "Kontakto:",
"about.default_locale": "기본",
"about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.", "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.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.", "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.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.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.domain_blocks.suspended.title": "Suspendita",
"about.language_label": "Lingvo",
"about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.", "about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.",
"about.powered_by": "Malcentrigita socia retejo pere de {mastodon}", "about.powered_by": "Malcentrigita socia retejo pere de {mastodon}",
"about.rules": "Reguloj de la servilo", "about.rules": "Reguloj de la servilo",
@ -328,9 +330,13 @@
"errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo", "errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo",
"errors.unexpected_crash.report_issue": "Raporti problemon", "errors.unexpected_crash.report_issue": "Raporti problemon",
"explore.suggested_follows": "Homoj", "explore.suggested_follows": "Homoj",
"explore.title": "Popularaĵoj",
"explore.trending_links": "Novaĵoj", "explore.trending_links": "Novaĵoj",
"explore.trending_statuses": "Afiŝoj", "explore.trending_statuses": "Afiŝoj",
"explore.trending_tags": "Kradvortoj", "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_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.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.", "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.lists": "Listoj",
"navigation_bar.logout": "Elsaluti", "navigation_bar.logout": "Elsaluti",
"navigation_bar.moderation": "Modereco", "navigation_bar.moderation": "Modereco",
"navigation_bar.more": "Pli",
"navigation_bar.mutes": "Silentigitaj uzantoj", "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.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", "navigation_bar.preferences": "Preferoj",
@ -777,6 +784,7 @@
"report_notification.categories.violation": "Malobservo de la regulo", "report_notification.categories.violation": "Malobservo de la regulo",
"report_notification.categories.violation_sentence": "malobservo de la regulo", "report_notification.categories.violation_sentence": "malobservo de la regulo",
"report_notification.open": "Malfermi la raporton", "report_notification.open": "Malfermi la raporton",
"search.clear": "검색어 지우기",
"search.no_recent_searches": "Neniuj lastaj serĉoj", "search.no_recent_searches": "Neniuj lastaj serĉoj",
"search.placeholder": "Serĉi", "search.placeholder": "Serĉi",
"search.quick_action.account_search": "Profiloj kiuj kongruas kun {x}", "search.quick_action.account_search": "Profiloj kiuj kongruas kun {x}",
@ -872,7 +880,9 @@
"subscribed_languages.save": "Konservi ŝanĝojn", "subscribed_languages.save": "Konservi ŝanĝojn",
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}", "subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
"tabs_bar.home": "Hejmo", "tabs_bar.home": "Hejmo",
"tabs_bar.menu": "Menuo",
"tabs_bar.notifications": "Sciigoj", "tabs_bar.notifications": "Sciigoj",
"tabs_bar.search": "Serĉi",
"terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}", "terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
"terms_of_service.title": "Kondiĉoj de uzado", "terms_of_service.title": "Kondiĉoj de uzado",
"terms_of_service.upcoming_changes_on": "Venontaj ŝanĝoj el {date}", "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": "Violación de regla",
"report_notification.categories.violation_sentence": "violación de regla", "report_notification.categories.violation_sentence": "violación de regla",
"report_notification.open": "Abrir denuncia", "report_notification.open": "Abrir denuncia",
"search.clear": "Limpiar búsqueda",
"search.no_recent_searches": "Sin búsquedas recientes", "search.no_recent_searches": "Sin búsquedas recientes",
"search.placeholder": "Buscar", "search.placeholder": "Buscar",
"search.quick_action.account_search": "Perfiles que coinciden con {x}", "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": "Infracción de regla",
"report_notification.categories.violation_sentence": "infracción de regla", "report_notification.categories.violation_sentence": "infracción de regla",
"report_notification.open": "Abrir denuncia", "report_notification.open": "Abrir denuncia",
"search.clear": "Limpiar búsqueda",
"search.no_recent_searches": "Sin búsquedas recientes", "search.no_recent_searches": "Sin búsquedas recientes",
"search.placeholder": "Buscar", "search.placeholder": "Buscar",
"search.quick_action.account_search": "Perfiles que coinciden con {x}", "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": "Infracción de regla",
"report_notification.categories.violation_sentence": "infracción de regla", "report_notification.categories.violation_sentence": "infracción de regla",
"report_notification.open": "Abrir informe", "report_notification.open": "Abrir informe",
"search.clear": "Limpiar búsqueda",
"search.no_recent_searches": "No hay búsquedas recientes", "search.no_recent_searches": "No hay búsquedas recientes",
"search.placeholder": "Buscar", "search.placeholder": "Buscar",
"search.quick_action.account_search": "Perfiles que coinciden con {x}", "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": "Brotin regla",
"report_notification.categories.violation_sentence": "brot á reglu", "report_notification.categories.violation_sentence": "brot á reglu",
"report_notification.open": "Opna melding", "report_notification.open": "Opna melding",
"search.clear": "Nullstilla leiting",
"search.no_recent_searches": "Ongar nýggjar leitingar", "search.no_recent_searches": "Ongar nýggjar leitingar",
"search.placeholder": "Leita", "search.placeholder": "Leita",
"search.quick_action.account_search": "Vangar, ið samsvara {x}", "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.follows_and_followers": "Seguindo e seguidoras",
"navigation_bar.import_export": "Importar e exportar", "navigation_bar.import_export": "Importar e exportar",
"navigation_bar.lists": "Listaxes", "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.logout": "Pechar sesión",
"navigation_bar.moderation": "Moderación", "navigation_bar.moderation": "Moderación",
"navigation_bar.more": "Máis", "navigation_bar.more": "Máis",
@ -802,6 +804,7 @@
"report_notification.categories.violation": "Faltou ás regras", "report_notification.categories.violation": "Faltou ás regras",
"report_notification.categories.violation_sentence": "violación das regras", "report_notification.categories.violation_sentence": "violación das regras",
"report_notification.open": "Abrir a denuncia", "report_notification.open": "Abrir a denuncia",
"search.clear": "Limpar a busca",
"search.no_recent_searches": "Non hai buscas recentes", "search.no_recent_searches": "Non hai buscas recentes",
"search.placeholder": "Procurar", "search.placeholder": "Procurar",
"search.quick_action.account_search": "Perfís coincidentes {x}", "search.quick_action.account_search": "Perfís coincidentes {x}",

View File

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

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "Szabálysértés", "report_notification.categories.violation": "Szabálysértés",
"report_notification.categories.violation_sentence": "szabálysértés", "report_notification.categories.violation_sentence": "szabálysértés",
"report_notification.open": "Bejelentés megnyitása", "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.no_recent_searches": "Nincsenek keresési előzmények",
"search.placeholder": "Keresés", "search.placeholder": "Keresés",
"search.quick_action.account_search": "Profilok a következő keresésre: {x}", "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.follows_and_followers": "팔로우와 팔로워",
"navigation_bar.import_export": "가져오기 & 내보내기", "navigation_bar.import_export": "가져오기 & 내보내기",
"navigation_bar.lists": "리스트", "navigation_bar.lists": "리스트",
"navigation_bar.live_feed_local": "라이브 피드 (로컬)",
"navigation_bar.live_feed_public": "라이브 피드 (공개)",
"navigation_bar.logout": "로그아웃", "navigation_bar.logout": "로그아웃",
"navigation_bar.moderation": "중재", "navigation_bar.moderation": "중재",
"navigation_bar.more": "더 보기", "navigation_bar.more": "더 보기",
@ -802,6 +804,7 @@
"report_notification.categories.violation": "규칙 위반", "report_notification.categories.violation": "규칙 위반",
"report_notification.categories.violation_sentence": "규칙 위반", "report_notification.categories.violation_sentence": "규칙 위반",
"report_notification.open": "신고 열기", "report_notification.open": "신고 열기",
"search.clear": "검색 초기화",
"search.no_recent_searches": "최근 검색 기록이 없습니다", "search.no_recent_searches": "최근 검색 기록이 없습니다",
"search.placeholder": "검색", "search.placeholder": "검색",
"search.quick_action.account_search": "{x}에 맞는 프로필", "search.quick_action.account_search": "{x}에 맞는 프로필",

View File

@ -563,6 +563,8 @@
"navigation_bar.follows_and_followers": "Volgers en gevolgde accounts", "navigation_bar.follows_and_followers": "Volgers en gevolgde accounts",
"navigation_bar.import_export": "Importeren en exporteren", "navigation_bar.import_export": "Importeren en exporteren",
"navigation_bar.lists": "Lijsten", "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.logout": "Uitloggen",
"navigation_bar.moderation": "Moderatie", "navigation_bar.moderation": "Moderatie",
"navigation_bar.more": "Meer", "navigation_bar.more": "Meer",
@ -802,6 +804,7 @@
"report_notification.categories.violation": "Overtreden regel(s)", "report_notification.categories.violation": "Overtreden regel(s)",
"report_notification.categories.violation_sentence": "serverregel overtreden", "report_notification.categories.violation_sentence": "serverregel overtreden",
"report_notification.open": "Rapportage openen", "report_notification.open": "Rapportage openen",
"search.clear": "Zoekopdracht wissen",
"search.no_recent_searches": "Geen recente zoekopdrachten", "search.no_recent_searches": "Geen recente zoekopdrachten",
"search.placeholder": "Zoeken", "search.placeholder": "Zoeken",
"search.quick_action.account_search": "Accounts die overeenkomen met {x}", "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.follows_and_followers": "Seguindo e seguidores",
"navigation_bar.import_export": "Importar e exportar", "navigation_bar.import_export": "Importar e exportar",
"navigation_bar.lists": "Listas", "navigation_bar.lists": "Listas",
"navigation_bar.live_feed_local": "Cronologia local",
"navigation_bar.live_feed_public": "Cronologia federada",
"navigation_bar.logout": "Sair", "navigation_bar.logout": "Sair",
"navigation_bar.moderation": "Moderação", "navigation_bar.moderation": "Moderação",
"navigation_bar.more": "Mais", "navigation_bar.more": "Mais",
@ -799,6 +801,7 @@
"report_notification.categories.violation": "Violação de regra", "report_notification.categories.violation": "Violação de regra",
"report_notification.categories.violation_sentence": "violação de regra", "report_notification.categories.violation_sentence": "violação de regra",
"report_notification.open": "Abrir denúncia", "report_notification.open": "Abrir denúncia",
"search.clear": "Limpar pesquisa",
"search.no_recent_searches": "Nenhuma pesquisa recente", "search.no_recent_searches": "Nenhuma pesquisa recente",
"search.placeholder": "Pesquisar", "search.placeholder": "Pesquisar",
"search.quick_action.account_search": "Perfis com correspondência a {x}", "search.quick_action.account_search": "Perfis com correspondência a {x}",

View File

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

View File

@ -779,6 +779,7 @@
"report_notification.categories.violation": "Порушення правил", "report_notification.categories.violation": "Порушення правил",
"report_notification.categories.violation_sentence": "порушення правил", "report_notification.categories.violation_sentence": "порушення правил",
"report_notification.open": "Відкрити скаргу", "report_notification.open": "Відкрити скаргу",
"search.clear": "Очистити пошук",
"search.no_recent_searches": "Немає останніх пошуків", "search.no_recent_searches": "Немає останніх пошуків",
"search.placeholder": "Пошук", "search.placeholder": "Пошук",
"search.quick_action.account_search": "Збіг профілів {x}", "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": "Vi phạm nội quy",
"report_notification.categories.violation_sentence": "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", "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.no_recent_searches": "Gần đây chưa tìm gì",
"search.placeholder": "Tìm kiếm", "search.placeholder": "Tìm kiếm",
"search.quick_action.account_search": "Người tên {x}", "search.quick_action.account_search": "Người tên {x}",

View File

@ -804,6 +804,7 @@
"report_notification.categories.violation": "違反規則", "report_notification.categories.violation": "違反規則",
"report_notification.categories.violation_sentence": "違反規則", "report_notification.categories.violation_sentence": "違反規則",
"report_notification.open": "開啟檢舉報告", "report_notification.open": "開啟檢舉報告",
"search.clear": "清除搜尋紀錄",
"search.no_recent_searches": "尚無最近的搜尋紀錄", "search.no_recent_searches": "尚無最近的搜尋紀錄",
"search.placeholder": "搜尋", "search.placeholder": "搜尋",
"search.quick_action.account_search": "符合的個人檔案 {x}", "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 { time {
margin-inline-start: 5px; margin-inline-start: 5px;
vertical-align: baseline; 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 type { RenderOptions } from '@testing-library/react';
import { render as rtlRender } from '@testing-library/react'; import { render as rtlRender } from '@testing-library/react';
import { IdentityContext } from './identity_context'; import { IdentityContext } from '@/mastodon/identity_context';
beforeAll(() => { beforeAll(() => {
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => { global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {

View File

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

View File

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

View File

@ -21,6 +21,7 @@ class Instance < ApplicationRecord
belongs_to :unavailable_domain belongs_to :unavailable_domain
has_many :accounts, dependent: nil has_many :accounts, dependent: nil
has_many :moderation_notes, class_name: 'InstanceModerationNote', dependent: :destroy
end end
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) } 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 self.discard_column = :deleted_at
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy 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 } 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 :for_locale, ->(locale) { where(language: I18n::Locale::Tag.tag(locale).to_a.first) }
scope :by_language_length, -> { order(Arel.sql('LENGTH(LANGUAGE)').desc) } 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 end

View File

@ -223,6 +223,12 @@ class User < ApplicationRecord
end end
end end
def email_domain
Mail::Address.new(email).domain
rescue Mail::Field::ParseError
nil
end
def update_sign_in!(new_sign_in: false) def update_sign_in!(new_sign_in: false)
old_current = current_sign_in_at old_current = current_sign_in_at
new_current = Time.now.utc 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 # frozen_string_literal: true
class OauthMetadataPresenter < ActiveModelSerializers::Model class OAuthMetadataPresenter < ActiveModelSerializers::Model
include RoutingHelper include RoutingHelper
attributes :issuer, :authorization_endpoint, :token_endpoint, attributes :issuer, :authorization_endpoint, :token_endpoint,

View File

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

View File

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

View File

@ -25,7 +25,7 @@
%td.accounts-table__extra %td.accounts-table__extra
- if account.local? - if account.local?
- if account.user_email - 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 - else
\- \-
%br/ %br/

View File

@ -22,10 +22,10 @@
%td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= account.user_email %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) %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 %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) - if can?(:create, :email_domain_block)
%tr %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? - if account.user_unconfirmed_email.present?
%tr %tr
%th= t('admin.accounts.unconfirmed_email') %th= t('admin.accounts.unconfirmed_email')

View File

@ -6,7 +6,7 @@
= date_range(@time_period) = date_range(@time_period)
- if @instance.persisted? - 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 - else
%p %p
= t('admin.instances.unknown_instance') = t('admin.instances.unknown_instance')
@ -55,6 +55,24 @@
= render partial: 'admin/action_logs/action_log', collection: @action_logs = 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' = 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/ %hr.spacer/
%h3= t('admin.instances.availability.title') %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' = image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
.report-notes__item__header .report-notes__item__header
%span.username %span.username
= link_to report_note.account.username, admin_account_path(report_note.account_id) = 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 } %a.timestamp{ href: "##{dom_id(report_note)}" }
= l report_note.created_at.to_date %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 .report-notes__item__content
= linkify(report_note.content) = linkify(report_note.content)
@ -14,5 +15,7 @@
.report-notes__item__actions .report-notes__item__actions
- if report_note.is_a?(AccountModerationNote) - if report_note.is_a?(AccountModerationNote)
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_account_moderation_note_path(report_note), method: :delete = 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 - else
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete = 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, = f.input :language,
collection: ui_languages, collection: ui_languages,
include_blank: false, 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 .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/> = 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/chewy/strategy/bypass_with_warning'
require_relative '../lib/rails/engine_extensions' require_relative '../lib/rails/engine_extensions'
require_relative '../lib/action_dispatch/remote_ip_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/database_tasks_extensions'
require_relative '../lib/active_record/batches' require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions' require_relative '../lib/simple_navigation/item_extensions'

View File

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

View File

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

View File

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

View File

@ -578,6 +578,13 @@ en:
all: All all: All
limited: Limited limited: Limited
title: Moderation 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 private_comment: Private comment
public_comment: Public comment public_comment: Public comment
purge: Purge purge: Purge

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class AddLastUsedAtToOauthAccessTokens < ActiveRecord::Migration[6.1] class AddLastUsedAtToOAuthAccessTokens < ActiveRecord::Migration[6.1]
def change def change
safety_assured do safety_assured do
change_table(:oauth_access_tokens, bulk: true) do |t| 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') require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class OptimizeNullIndexOauthAccessTokensRefreshToken < ActiveRecord::Migration[5.2] class OptimizeNullIndexOAuthAccessTokensRefreshToken < ActiveRecord::Migration[5.2]
include Mastodon::MigrationHelpers include Mastodon::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!

View File

@ -2,7 +2,7 @@
require Rails.root.join('lib', 'mastodon', 'migration_helpers') require Rails.root.join('lib', 'mastodon', 'migration_helpers')
class OptimizeNullIndexOauthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2] class OptimizeNullIndexOAuthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2]
include Mastodon::MigrationHelpers include Mastodon::MigrationHelpers
disable_ddl_transaction! 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.boolean "hide_collections"
t.integer "avatar_storage_schema_version" t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version" t.integer "header_storage_schema_version"
t.datetime "sensitized_at", precision: nil
t.integer "suspension_origin" t.integer "suspension_origin"
t.datetime "sensitized_at", precision: nil
t.boolean "trendable" t.boolean "trendable"
t.datetime "reviewed_at", precision: nil t.datetime "reviewed_at", precision: nil
t.datetime "requested_review_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" t.index ["user_id"], name: "index_identities_on_user_id"
end 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| create_table "invites", force: :cascade do |t|
t.bigint "user_id", null: false t.bigint "user_id", null: false
t.string "code", default: "", null: false t.string "code", default: "", null: false
@ -595,12 +604,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
end end
create_table "ip_blocks", force: :cascade do |t| 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 "created_at", precision: nil, null: false
t.datetime "updated_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 t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
end 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 "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
add_foreign_key "generated_annual_reports", "accounts" add_foreign_key "generated_annual_reports", "accounts"
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade 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 "invites", "users", on_delete: :cascade
add_foreign_key "list_accounts", "accounts", on_delete: :cascade add_foreign_key "list_accounts", "accounts", on_delete: :cascade
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade

View File

@ -251,8 +251,7 @@ export default tseslint.config([
devDependencies: [ devDependencies: [
'eslint.config.mjs', 'eslint.config.mjs',
'app/javascript/mastodon/performance.js', 'app/javascript/mastodon/performance.js',
'app/javascript/mastodon/test_setup.js', 'app/javascript/testing/**/*',
'app/javascript/mastodon/test_helpers.tsx',
'app/javascript/**/__tests__/**', 'app/javascript/**/__tests__/**',
'app/javascript/**/*.stories.ts', 'app/javascript/**/*.stories.ts',
'app/javascript/**/*.stories.tsx', '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' 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') } let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') }
describe 'GET #new' do describe 'GET #new' do

View File

@ -2,7 +2,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Oauth::AuthorizedApplicationsController do RSpec.describe OAuth::AuthorizedApplicationsController do
render_views render_views
describe 'GET #index' do 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 end
describe 'Validations' do 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(: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(: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) } it { is_expected.to validate_length_of(:website).is_at_most(described_class::APP_WEBSITE_LIMIT) }
end 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 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' require 'rails_helper'
RSpec.describe Instance do RSpec.describe Instance do
describe 'Scopes' do before { described_class.refresh }
before { described_class.refresh }
describe 'Scopes' do
describe '#searchable' do describe '#searchable' do
let(:expected_domain) { 'host.example' } let(:expected_domain) { 'host.example' }
let(:blocked_domain) { 'other.example' } let(:blocked_domain) { 'other.example' }

View File

@ -166,6 +166,34 @@ RSpec.describe User do
end end
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 describe '#update_sign_in!' do
context 'with an existing user' 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 } 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 RSpec.describe 'Admin Instances' do
describe 'GET /admin/instances/:id' 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 it 'returns http success' do
get admin_instance_path(id: 'unknown.example') get admin_instance_path(id: 'unknown.example')
@ -14,5 +14,14 @@ RSpec.describe 'Admin Instances' do
.to have_http_status(200) .to have_http_status(200)
end end
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
end end

View File

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

View File

@ -35,7 +35,7 @@ RSpec.describe 'Admin::AccountModerationNotes' do
end end
def delete_note 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') click_on I18n.t('admin.reports.notes.delete')
end end
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", "vite.config.mts",
"vitest.config.mts", "vitest.config.mts",
"config/vite", "config/vite",
"app/javascript/mastodon", "app/javascript",
"app/javascript/entrypoints", ".storybook/*"
"app/javascript/types",
".storybook/*.ts",
".storybook/*.tsx"
] ]
} }