mirror of
https://github.com/mastodon/mastodon.git
synced 2025-07-15 08:48:15 +00:00
Merge branch 'main' into mute-prefill
This commit is contained in:
commit
ff7e62f3c7
|
@ -23,5 +23,6 @@ RSpec/SpecFilePathFormat:
|
|||
ActivityPub: activitypub
|
||||
DeepL: deepl
|
||||
FetchOEmbedService: fetch_oembed_service
|
||||
OAuth: oauth
|
||||
OEmbedController: oembed_controller
|
||||
OStatus: ostatus
|
||||
|
|
|
@ -11,7 +11,21 @@ const config: StorybookConfig = {
|
|||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
staticDirs: ['./static'],
|
||||
staticDirs: [
|
||||
'./static',
|
||||
// We need to manually specify the assets because of the symlink in public/sw.js
|
||||
...[
|
||||
'avatars',
|
||||
'emoji',
|
||||
'headers',
|
||||
'sounds',
|
||||
'badge.png',
|
||||
'loading.gif',
|
||||
'loading.png',
|
||||
'oops.gif',
|
||||
'oops.png',
|
||||
].map((path) => ({ from: `../public/${path}`, to: `/${path}` })),
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
|
|
@ -2,16 +2,19 @@ import { useEffect, useState } from 'react';
|
|||
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { MemoryRouter, Route } from 'react-router';
|
||||
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import type { Preview } from '@storybook/react-vite';
|
||||
import { http, passthrough } from 'msw';
|
||||
import { initialize, mswLoader } from 'msw-storybook-addon';
|
||||
import { action } from 'storybook/actions';
|
||||
|
||||
import type { LocaleData } from '@/mastodon/locales';
|
||||
import { reducerWithInitialState, rootReducer } from '@/mastodon/reducers';
|
||||
import { defaultMiddleware } from '@/mastodon/store/store';
|
||||
import { mockHandlers, unhandledRequestHandler } from '@/testing/api';
|
||||
|
||||
// If you want to run the dark theme during development,
|
||||
// you can change the below to `/application.scss`
|
||||
|
@ -22,7 +25,9 @@ const localeFiles = import.meta.glob('@/mastodon/locales/*.json', {
|
|||
});
|
||||
|
||||
// Initialize MSW
|
||||
initialize();
|
||||
initialize({
|
||||
onUnhandledRequest: unhandledRequestHandler,
|
||||
});
|
||||
|
||||
const preview: Preview = {
|
||||
// Auto-generate docs: https://storybook.js.org/docs/writing-docs/autodocs
|
||||
|
@ -94,6 +99,21 @@ const preview: Preview = {
|
|||
</IntlProvider>
|
||||
);
|
||||
},
|
||||
(Story) => (
|
||||
<MemoryRouter>
|
||||
<Story />
|
||||
<Route
|
||||
path='*'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
render={({ location }) => {
|
||||
if (location.pathname !== '/') {
|
||||
action(`route change to ${location.pathname}`)(location);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
),
|
||||
],
|
||||
loaders: [mswLoader],
|
||||
parameters: {
|
||||
|
@ -115,20 +135,10 @@ const preview: Preview = {
|
|||
|
||||
state: {},
|
||||
|
||||
// Force docs to use an iframe as it breaks MSW handlers.
|
||||
// See: https://github.com/mswjs/msw-storybook-addon/issues/83
|
||||
docs: {
|
||||
story: {
|
||||
inline: false,
|
||||
},
|
||||
},
|
||||
docs: {},
|
||||
|
||||
msw: {
|
||||
handlers: [
|
||||
http.get('/index.json', passthrough),
|
||||
http.get('/packs-dev/*', passthrough),
|
||||
http.get('/sounds/*', passthrough),
|
||||
],
|
||||
handlers: mockHandlers,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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
|
|
@ -14,6 +14,9 @@ module Admin
|
|||
|
||||
def show
|
||||
authorize :instance, :show?
|
||||
|
||||
@instance_moderation_note = @instance.moderation_notes.new
|
||||
@instance_moderation_notes = @instance.moderation_notes.includes(:account).chronological
|
||||
@time_period = (6.days.ago.to_date...Time.now.utc.to_date)
|
||||
@action_logs = Admin::ActionLogFilter.new(target_domain: @instance.domain).results.limit(LOGS_LIMIT)
|
||||
end
|
||||
|
@ -52,7 +55,8 @@ module Admin
|
|||
private
|
||||
|
||||
def set_instance
|
||||
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(params[:id]&.strip))
|
||||
domain = params[:id]&.strip
|
||||
@instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain(domain))
|
||||
end
|
||||
|
||||
def set_instances
|
||||
|
|
|
@ -17,6 +17,9 @@ module Admin
|
|||
|
||||
def edit
|
||||
authorize @rule, :update?
|
||||
|
||||
missing_languages = RuleTranslation.languages - @rule.translations.pluck(:language)
|
||||
missing_languages.each { |lang| @rule.translations.build(language: lang) }
|
||||
end
|
||||
|
||||
def create
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
class OAuth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
||||
skip_before_action :authenticate_resource_owner!
|
||||
|
||||
before_action :store_current_location
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||
class OAuth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
|
||||
skip_before_action :authenticate_resource_owner!
|
||||
|
||||
before_action :store_current_location
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::TokensController < Doorkeeper::TokensController
|
||||
class OAuth::TokensController < Doorkeeper::TokensController
|
||||
def revoke
|
||||
unsubscribe_for_token if token.present? && authorized? && token.accessible?
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Oauth::UserinfoController < Api::BaseController
|
||||
class OAuth::UserinfoController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :profile }, only: [:show]
|
||||
before_action :require_user!
|
||||
|
||||
def show
|
||||
@account = current_account
|
||||
render json: @account, serializer: OauthUserinfoSerializer
|
||||
render json: @account, serializer: OAuthUserinfoSerializer
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WellKnown
|
||||
class OauthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
||||
class OAuthMetadataController < ActionController::Base # rubocop:disable Rails/ApplicationController
|
||||
include CacheConcern
|
||||
|
||||
# Prevent `active_model_serializer`'s `ActionController::Serialization` from calling `current_user`
|
||||
|
@ -13,8 +13,8 @@ module WellKnown
|
|||
# new OAuth scopes are added), we don't use expires_in to cache upstream,
|
||||
# instead just caching in the rails cache:
|
||||
render_with_cache(
|
||||
json: ::OauthMetadataPresenter.new,
|
||||
serializer: ::OauthMetadataSerializer,
|
||||
json: ::OAuthMetadataPresenter.new,
|
||||
serializer: ::OAuthMetadataSerializer,
|
||||
content_type: 'application/json',
|
||||
expires_in: 15.minutes
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { render, fireEvent, screen } from 'mastodon/test_helpers';
|
||||
import { render, fireEvent, screen } from '@/testing/rendering';
|
||||
|
||||
import { Button } from '../button';
|
||||
|
||||
|
|
120
app/javascript/mastodon/components/account/account.stories.tsx
Normal file
120
app/javascript/mastodon/components/account/account.stories.tsx
Normal 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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
import { render, fireEvent, screen } from 'mastodon/test_helpers';
|
||||
import { render, fireEvent, screen } from '@/testing/rendering';
|
||||
|
||||
import Column from '../column';
|
||||
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Porušení pravidla",
|
||||
"report_notification.categories.violation_sentence": "porušení pravidel",
|
||||
"report_notification.open": "Otevřít hlášení",
|
||||
"search.clear": "Vymazat hledání",
|
||||
"search.no_recent_searches": "Žádná nedávná vyhledávání",
|
||||
"search.placeholder": "Hledat",
|
||||
"search.quick_action.account_search": "Profily odpovídající {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Regelverstoß",
|
||||
"report_notification.categories.violation_sentence": "Regelverletzung",
|
||||
"report_notification.open": "Meldung öffnen",
|
||||
"search.clear": "Suchanfrage löschen",
|
||||
"search.no_recent_searches": "Keine früheren Suchanfragen",
|
||||
"search.placeholder": "Suchen",
|
||||
"search.quick_action.account_search": "Profile passend zu {x}",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"about.blocks": "Reguligitaj serviloj",
|
||||
"about.contact": "Kontakto:",
|
||||
"about.default_locale": "기본",
|
||||
"about.disclaimer": "Mastodon estas libera, malfermitkoda programo kaj varmarko de la firmao Mastodon gGmbH.",
|
||||
"about.domain_blocks.no_reason_available": "Kialo ne disponeblas",
|
||||
"about.domain_blocks.preamble": "Mastodon ĝenerale rajtigas vidi la enhavojn de uzantoj el aliaj serviloj en la fediverso, kaj komuniki kun ili. Jen la limigoj deciditaj de tiu ĉi servilo mem.",
|
||||
|
@ -8,6 +9,7 @@
|
|||
"about.domain_blocks.silenced.title": "Limigita",
|
||||
"about.domain_blocks.suspended.explanation": "Neniuj datumoj el tiu servilo estos prilaboritaj, konservitaj, aŭ interŝanĝitaj, do neeblas interagi aŭ komuniki kun uzantoj de tiu servilo.",
|
||||
"about.domain_blocks.suspended.title": "Suspendita",
|
||||
"about.language_label": "Lingvo",
|
||||
"about.not_available": "Ĉi tiu informo ne estas disponebla ĉe ĉi tiu servilo.",
|
||||
"about.powered_by": "Malcentrigita socia retejo pere de {mastodon}",
|
||||
"about.rules": "Reguloj de la servilo",
|
||||
|
@ -328,9 +330,13 @@
|
|||
"errors.unexpected_crash.copy_stacktrace": "Kopii stakspuron en tondujo",
|
||||
"errors.unexpected_crash.report_issue": "Raporti problemon",
|
||||
"explore.suggested_follows": "Homoj",
|
||||
"explore.title": "Popularaĵoj",
|
||||
"explore.trending_links": "Novaĵoj",
|
||||
"explore.trending_statuses": "Afiŝoj",
|
||||
"explore.trending_tags": "Kradvortoj",
|
||||
"featured_carousel.next": "Antaŭen",
|
||||
"featured_carousel.post": "Afiŝi",
|
||||
"featured_carousel.previous": "Malantaŭen",
|
||||
"filter_modal.added.context_mismatch_explanation": "Ĉi tiu filtrilkategorio ne kongruas kun la kunteksto en kiu vi akcesis ĉi tiun afiŝon. Se vi volas ke la afiŝo estas ankaŭ filtrita en ĉi tiu kunteksto, vi devus redakti la filtrilon.",
|
||||
"filter_modal.added.context_mismatch_title": "Ne kongruas la kunteksto!",
|
||||
"filter_modal.added.expired_explanation": "Ĉi tiu filtrilkategorio eksvalidiĝis, vu bezonos ŝanĝi la eksvaliddaton por ĝi.",
|
||||
|
@ -547,6 +553,7 @@
|
|||
"navigation_bar.lists": "Listoj",
|
||||
"navigation_bar.logout": "Elsaluti",
|
||||
"navigation_bar.moderation": "Modereco",
|
||||
"navigation_bar.more": "Pli",
|
||||
"navigation_bar.mutes": "Silentigitaj uzantoj",
|
||||
"navigation_bar.opened_in_classic_interface": "Afiŝoj, kontoj, kaj aliaj specifaj paĝoj kiuj estas malfermititaj defaulta en la klasika reta interfaco.",
|
||||
"navigation_bar.preferences": "Preferoj",
|
||||
|
@ -777,6 +784,7 @@
|
|||
"report_notification.categories.violation": "Malobservo de la regulo",
|
||||
"report_notification.categories.violation_sentence": "malobservo de la regulo",
|
||||
"report_notification.open": "Malfermi la raporton",
|
||||
"search.clear": "검색어 지우기",
|
||||
"search.no_recent_searches": "Neniuj lastaj serĉoj",
|
||||
"search.placeholder": "Serĉi",
|
||||
"search.quick_action.account_search": "Profiloj kiuj kongruas kun {x}",
|
||||
|
@ -872,7 +880,9 @@
|
|||
"subscribed_languages.save": "Konservi ŝanĝojn",
|
||||
"subscribed_languages.target": "Ŝanĝu abonitajn lingvojn por {target}",
|
||||
"tabs_bar.home": "Hejmo",
|
||||
"tabs_bar.menu": "Menuo",
|
||||
"tabs_bar.notifications": "Sciigoj",
|
||||
"tabs_bar.search": "Serĉi",
|
||||
"terms_of_service.effective_as_of": "Ĝi ekvalidas de {date}",
|
||||
"terms_of_service.title": "Kondiĉoj de uzado",
|
||||
"terms_of_service.upcoming_changes_on": "Venontaj ŝanĝoj el {date}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Violación de regla",
|
||||
"report_notification.categories.violation_sentence": "violación de regla",
|
||||
"report_notification.open": "Abrir denuncia",
|
||||
"search.clear": "Limpiar búsqueda",
|
||||
"search.no_recent_searches": "Sin búsquedas recientes",
|
||||
"search.placeholder": "Buscar",
|
||||
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Infracción de regla",
|
||||
"report_notification.categories.violation_sentence": "infracción de regla",
|
||||
"report_notification.open": "Abrir denuncia",
|
||||
"search.clear": "Limpiar búsqueda",
|
||||
"search.no_recent_searches": "Sin búsquedas recientes",
|
||||
"search.placeholder": "Buscar",
|
||||
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Infracción de regla",
|
||||
"report_notification.categories.violation_sentence": "infracción de regla",
|
||||
"report_notification.open": "Abrir informe",
|
||||
"search.clear": "Limpiar búsqueda",
|
||||
"search.no_recent_searches": "No hay búsquedas recientes",
|
||||
"search.placeholder": "Buscar",
|
||||
"search.quick_action.account_search": "Perfiles que coinciden con {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Brotin regla",
|
||||
"report_notification.categories.violation_sentence": "brot á reglu",
|
||||
"report_notification.open": "Opna melding",
|
||||
"search.clear": "Nullstilla leiting",
|
||||
"search.no_recent_searches": "Ongar nýggjar leitingar",
|
||||
"search.placeholder": "Leita",
|
||||
"search.quick_action.account_search": "Vangar, ið samsvara {x}",
|
||||
|
|
|
@ -563,6 +563,8 @@
|
|||
"navigation_bar.follows_and_followers": "Seguindo e seguidoras",
|
||||
"navigation_bar.import_export": "Importar e exportar",
|
||||
"navigation_bar.lists": "Listaxes",
|
||||
"navigation_bar.live_feed_local": "En directo (local)",
|
||||
"navigation_bar.live_feed_public": "En directo (federada)",
|
||||
"navigation_bar.logout": "Pechar sesión",
|
||||
"navigation_bar.moderation": "Moderación",
|
||||
"navigation_bar.more": "Máis",
|
||||
|
@ -802,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Faltou ás regras",
|
||||
"report_notification.categories.violation_sentence": "violación das regras",
|
||||
"report_notification.open": "Abrir a denuncia",
|
||||
"search.clear": "Limpar a busca",
|
||||
"search.no_recent_searches": "Non hai buscas recentes",
|
||||
"search.placeholder": "Procurar",
|
||||
"search.quick_action.account_search": "Perfís coincidentes {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "הפרת כלל",
|
||||
"report_notification.categories.violation_sentence": "הפרת כלל",
|
||||
"report_notification.open": "פתח דו\"ח",
|
||||
"search.clear": "ניקוי חיפוש",
|
||||
"search.no_recent_searches": "לא נמצאו חיפושים אחרונים",
|
||||
"search.placeholder": "חיפוש",
|
||||
"search.quick_action.account_search": "פרופילים המכילים {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Szabálysértés",
|
||||
"report_notification.categories.violation_sentence": "szabálysértés",
|
||||
"report_notification.open": "Bejelentés megnyitása",
|
||||
"search.clear": "Keresés törlése",
|
||||
"search.no_recent_searches": "Nincsenek keresési előzmények",
|
||||
"search.placeholder": "Keresés",
|
||||
"search.quick_action.account_search": "Profilok a következő keresésre: {x}",
|
||||
|
|
|
@ -563,6 +563,8 @@
|
|||
"navigation_bar.follows_and_followers": "팔로우와 팔로워",
|
||||
"navigation_bar.import_export": "가져오기 & 내보내기",
|
||||
"navigation_bar.lists": "리스트",
|
||||
"navigation_bar.live_feed_local": "라이브 피드 (로컬)",
|
||||
"navigation_bar.live_feed_public": "라이브 피드 (공개)",
|
||||
"navigation_bar.logout": "로그아웃",
|
||||
"navigation_bar.moderation": "중재",
|
||||
"navigation_bar.more": "더 보기",
|
||||
|
@ -802,6 +804,7 @@
|
|||
"report_notification.categories.violation": "규칙 위반",
|
||||
"report_notification.categories.violation_sentence": "규칙 위반",
|
||||
"report_notification.open": "신고 열기",
|
||||
"search.clear": "검색 초기화",
|
||||
"search.no_recent_searches": "최근 검색 기록이 없습니다",
|
||||
"search.placeholder": "검색",
|
||||
"search.quick_action.account_search": "{x}에 맞는 프로필",
|
||||
|
|
|
@ -563,6 +563,8 @@
|
|||
"navigation_bar.follows_and_followers": "Volgers en gevolgde accounts",
|
||||
"navigation_bar.import_export": "Importeren en exporteren",
|
||||
"navigation_bar.lists": "Lijsten",
|
||||
"navigation_bar.live_feed_local": "Openbare tijdlijn (deze server)",
|
||||
"navigation_bar.live_feed_public": "Openbare tijdlijn (alles)",
|
||||
"navigation_bar.logout": "Uitloggen",
|
||||
"navigation_bar.moderation": "Moderatie",
|
||||
"navigation_bar.more": "Meer",
|
||||
|
@ -802,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Overtreden regel(s)",
|
||||
"report_notification.categories.violation_sentence": "serverregel overtreden",
|
||||
"report_notification.open": "Rapportage openen",
|
||||
"search.clear": "Zoekopdracht wissen",
|
||||
"search.no_recent_searches": "Geen recente zoekopdrachten",
|
||||
"search.placeholder": "Zoeken",
|
||||
"search.quick_action.account_search": "Accounts die overeenkomen met {x}",
|
||||
|
|
|
@ -563,6 +563,8 @@
|
|||
"navigation_bar.follows_and_followers": "Seguindo e seguidores",
|
||||
"navigation_bar.import_export": "Importar e exportar",
|
||||
"navigation_bar.lists": "Listas",
|
||||
"navigation_bar.live_feed_local": "Cronologia local",
|
||||
"navigation_bar.live_feed_public": "Cronologia federada",
|
||||
"navigation_bar.logout": "Sair",
|
||||
"navigation_bar.moderation": "Moderação",
|
||||
"navigation_bar.more": "Mais",
|
||||
|
@ -799,6 +801,7 @@
|
|||
"report_notification.categories.violation": "Violação de regra",
|
||||
"report_notification.categories.violation_sentence": "violação de regra",
|
||||
"report_notification.open": "Abrir denúncia",
|
||||
"search.clear": "Limpar pesquisa",
|
||||
"search.no_recent_searches": "Nenhuma pesquisa recente",
|
||||
"search.placeholder": "Pesquisar",
|
||||
"search.quick_action.account_search": "Perfis com correspondência a {x}",
|
||||
|
|
|
@ -563,6 +563,8 @@
|
|||
"navigation_bar.follows_and_followers": "Подписки и подписчики",
|
||||
"navigation_bar.import_export": "Импорт и экспорт",
|
||||
"navigation_bar.lists": "Списки",
|
||||
"navigation_bar.live_feed_local": "Живая лента (локальная)",
|
||||
"navigation_bar.live_feed_public": "Живая лента (глобальная)",
|
||||
"navigation_bar.logout": "Выйти",
|
||||
"navigation_bar.moderation": "Модерирование",
|
||||
"navigation_bar.more": "Ещё",
|
||||
|
@ -578,9 +580,9 @@
|
|||
"navigation_panel.expand_lists": "Развернуть меню списков",
|
||||
"not_signed_in_indicator.not_signed_in": "Эта страница доступна только авторизованным пользователям.",
|
||||
"notification.admin.report": "{name} пожаловался (-лась) на {target}",
|
||||
"notification.admin.report_account": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target} по причине «{category}»",
|
||||
"notification.admin.report_account": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target}, выбрав категорию «{category}»",
|
||||
"notification.admin.report_account_other": "{name} пожаловался (-лась) на {count, plural, one {# пост} few {# поста} other {# постов}} пользователя {target}",
|
||||
"notification.admin.report_statuses": "{name} пожаловался (-лась) на {target} по причине «{category}»",
|
||||
"notification.admin.report_statuses": "{name} пожаловался (-лась) на {target}, выбрав категорию «{category}»",
|
||||
"notification.admin.report_statuses_other": "{name} пожаловался (-лась) на {target}",
|
||||
"notification.admin.sign_up": "{name} зарегистрировался (-лась) на сервере",
|
||||
"notification.admin.sign_up.name_and_others": "{name} и ещё {count, plural, one {# пользователь} few {# пользователя} other {# пользователей}} зарегистрировались на сервере",
|
||||
|
@ -602,13 +604,13 @@
|
|||
"notification.mentioned_you": "{name} упомянул(а) вас",
|
||||
"notification.moderation-warning.learn_more": "Узнать больше",
|
||||
"notification.moderation_warning": "Модераторы вынесли вам предупреждение",
|
||||
"notification.moderation_warning.action_delete_statuses": "Некоторые из ваших публикаций были удалены.",
|
||||
"notification.moderation_warning.action_delete_statuses": "Некоторые ваши посты были удалены.",
|
||||
"notification.moderation_warning.action_disable": "Ваша учётная запись была отключена.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые из ваших сообщений были отмечены как деликатные.",
|
||||
"notification.moderation_warning.action_none": "Ваша учётная запись получила предупреждение от модерации.",
|
||||
"notification.moderation_warning.action_sensitive": "С этого момента ваши сообщения будут помечены как деликатные.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Некоторые ваши посты получили отметку деликатного содержания.",
|
||||
"notification.moderation_warning.action_none": "Модераторы вынесли вам предупреждение.",
|
||||
"notification.moderation_warning.action_sensitive": "С этого момента ваши посты будут иметь отметку деликатного содержания.",
|
||||
"notification.moderation_warning.action_silence": "Ваша учётная запись была ограничена.",
|
||||
"notification.moderation_warning.action_suspend": "Действие вашей учётной записи приостановлено.",
|
||||
"notification.moderation_warning.action_suspend": "Ваша учётная запись была заблокирована.",
|
||||
"notification.own_poll": "Ваш опрос завершился",
|
||||
"notification.poll": "Опрос, в котором вы приняли участие, завершился",
|
||||
"notification.reblog": "{name} продвинул(а) ваш пост",
|
||||
|
@ -792,16 +794,17 @@
|
|||
"report.thanks.title_actionable": "Спасибо, что сообщили о проблеме, мы рассмотрим вашу жалобу.",
|
||||
"report.unfollow": "Отписаться от @{name}",
|
||||
"report.unfollow_explanation": "Вы подписаны на этого пользователя. Отпишитесь от пользователя, чтобы перестать видеть посты этого человека в домашней ленте.",
|
||||
"report_notification.attached_statuses": "{count, plural, one {{count} сообщение} few {{count} сообщения} many {{count} сообщений} other {{count} сообщений}} вложено",
|
||||
"report_notification.attached_statuses": "{count, plural, one {{count} пост прикреплён} few {{count} поста прикреплено} other {{count} постов прикреплено}}",
|
||||
"report_notification.categories.legal": "Нарушение закона",
|
||||
"report_notification.categories.legal_sentence": "запрещённый контент",
|
||||
"report_notification.categories.legal_sentence": "Нарушение закона",
|
||||
"report_notification.categories.other": "Другое",
|
||||
"report_notification.categories.other_sentence": "другое",
|
||||
"report_notification.categories.other_sentence": "Другое",
|
||||
"report_notification.categories.spam": "Спам",
|
||||
"report_notification.categories.spam_sentence": "спам",
|
||||
"report_notification.categories.spam_sentence": "Спам",
|
||||
"report_notification.categories.violation": "Нарушение правил",
|
||||
"report_notification.categories.violation_sentence": "нарушение правила",
|
||||
"report_notification.open": "Открыть жалобу",
|
||||
"report_notification.categories.violation_sentence": "Нарушение правил",
|
||||
"report_notification.open": "Перейти к жалобе",
|
||||
"search.clear": "Очистить поисковый запрос",
|
||||
"search.no_recent_searches": "Недавние запросы отсутствуют",
|
||||
"search.placeholder": "Поиск",
|
||||
"search.quick_action.account_search": "Профили, соответствующие {x}",
|
||||
|
@ -868,6 +871,7 @@
|
|||
"status.mute_conversation": "Игнорировать обсуждение",
|
||||
"status.open": "Открыть пост",
|
||||
"status.pin": "Закрепить в профиле",
|
||||
"status.quote_error.filtered": "Скрыто одним из ваших фильтров",
|
||||
"status.quote_error.removed": "Этот пост был удалён его автором.",
|
||||
"status.read_more": "Читать далее",
|
||||
"status.reblog": "Продвинуть",
|
||||
|
|
|
@ -779,6 +779,7 @@
|
|||
"report_notification.categories.violation": "Порушення правил",
|
||||
"report_notification.categories.violation_sentence": "порушення правил",
|
||||
"report_notification.open": "Відкрити скаргу",
|
||||
"search.clear": "Очистити пошук",
|
||||
"search.no_recent_searches": "Немає останніх пошуків",
|
||||
"search.placeholder": "Пошук",
|
||||
"search.quick_action.account_search": "Збіг профілів {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "Vi phạm nội quy",
|
||||
"report_notification.categories.violation_sentence": "vi phạm nội quy",
|
||||
"report_notification.open": "Mở báo cáo",
|
||||
"search.clear": "Xóa tìm kiếm",
|
||||
"search.no_recent_searches": "Gần đây chưa tìm gì",
|
||||
"search.placeholder": "Tìm kiếm",
|
||||
"search.quick_action.account_search": "Người tên {x}",
|
||||
|
|
|
@ -804,6 +804,7 @@
|
|||
"report_notification.categories.violation": "違反規則",
|
||||
"report_notification.categories.violation_sentence": "違反規則",
|
||||
"report_notification.open": "開啟檢舉報告",
|
||||
"search.clear": "清除搜尋紀錄",
|
||||
"search.no_recent_searches": "尚無最近的搜尋紀錄",
|
||||
"search.placeholder": "搜尋",
|
||||
"search.quick_action.account_search": "符合的個人檔案 {x}",
|
||||
|
|
|
@ -1632,6 +1632,17 @@ a.sparkline {
|
|||
}
|
||||
}
|
||||
|
||||
a.timestamp {
|
||||
color: $darker-text-color;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
time {
|
||||
margin-inline-start: 5px;
|
||||
vertical-align: baseline;
|
||||
|
|
53
app/javascript/testing/api.ts
Normal file
53
app/javascript/testing/api.ts
Normal 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.`,
|
||||
);
|
||||
}
|
||||
};
|
70
app/javascript/testing/factories.ts
Normal file
70
app/javascript/testing/factories.ts
Normal 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,
|
||||
});
|
|
@ -5,7 +5,7 @@ import { MemoryRouter } from 'react-router';
|
|||
import type { RenderOptions } from '@testing-library/react';
|
||||
import { render as rtlRender } from '@testing-library/react';
|
||||
|
||||
import { IdentityContext } from './identity_context';
|
||||
import { IdentityContext } from '@/mastodon/identity_context';
|
||||
|
||||
beforeAll(() => {
|
||||
global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
|
|
@ -163,6 +163,7 @@ class Account < ApplicationRecord
|
|||
after_update_commit :trigger_update_webhooks
|
||||
|
||||
delegate :email,
|
||||
:email_domain,
|
||||
:unconfirmed_email,
|
||||
:current_sign_in_at,
|
||||
:created_at,
|
||||
|
|
|
@ -18,6 +18,7 @@ module Account::Associations
|
|||
has_many :favourites
|
||||
has_many :featured_tags, -> { includes(:tag) }
|
||||
has_many :list_accounts
|
||||
has_many :instance_moderation_notes
|
||||
has_many :media_attachments
|
||||
has_many :mentions
|
||||
has_many :migrations, class_name: 'AccountMigration'
|
||||
|
|
|
@ -21,6 +21,7 @@ class Instance < ApplicationRecord
|
|||
belongs_to :unavailable_domain
|
||||
|
||||
has_many :accounts, dependent: nil
|
||||
has_many :moderation_notes, class_name: 'InstanceModerationNote', dependent: :destroy
|
||||
end
|
||||
|
||||
scope :searchable, -> { where.not(domain: DomainBlock.select(:domain)) }
|
||||
|
|
27
app/models/instance_moderation_note.rb
Normal file
27
app/models/instance_moderation_note.rb
Normal 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
|
|
@ -20,7 +20,7 @@ class Rule < ApplicationRecord
|
|||
self.discard_column = :deleted_at
|
||||
|
||||
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
|
||||
accepts_nested_attributes_for :translations, reject_if: :all_blank, allow_destroy: true
|
||||
accepts_nested_attributes_for :translations, reject_if: ->(attributes) { attributes['text'].blank? }, allow_destroy: true
|
||||
|
||||
validates :text, presence: true, length: { maximum: TEXT_SIZE_LIMIT }
|
||||
|
||||
|
|
|
@ -20,4 +20,8 @@ class RuleTranslation < ApplicationRecord
|
|||
|
||||
scope :for_locale, ->(locale) { where(language: I18n::Locale::Tag.tag(locale).to_a.first) }
|
||||
scope :by_language_length, -> { order(Arel.sql('LENGTH(LANGUAGE)').desc) }
|
||||
|
||||
def self.languages
|
||||
RuleTranslation.joins(:rule).merge(Rule.kept).select(:language).distinct.pluck(:language).sort
|
||||
end
|
||||
end
|
||||
|
|
|
@ -223,6 +223,12 @@ class User < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def email_domain
|
||||
Mail::Address.new(email).domain
|
||||
rescue Mail::Field::ParseError
|
||||
nil
|
||||
end
|
||||
|
||||
def update_sign_in!(new_sign_in: false)
|
||||
old_current = current_sign_in_at
|
||||
new_current = Time.now.utc
|
||||
|
|
17
app/policies/instance_moderation_note_policy.rb
Normal file
17
app/policies/instance_moderation_note_policy.rb
Normal 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
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OauthMetadataPresenter < ActiveModelSerializers::Model
|
||||
class OAuthMetadataPresenter < ActiveModelSerializers::Model
|
||||
include RoutingHelper
|
||||
|
||||
attributes :issuer, :authorization_endpoint, :token_endpoint,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OauthMetadataSerializer < ActiveModel::Serializer
|
||||
class OAuthMetadataSerializer < ActiveModel::Serializer
|
||||
attributes :issuer, :authorization_endpoint, :token_endpoint,
|
||||
:revocation_endpoint, :userinfo_endpoint, :scopes_supported,
|
||||
:response_types_supported, :response_modes_supported,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OauthUserinfoSerializer < ActiveModel::Serializer
|
||||
class OAuthUserinfoSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :iss, :sub, :name, :preferred_username, :profile, :picture
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
%td.accounts-table__extra
|
||||
- if account.local?
|
||||
- if account.user_email
|
||||
= link_to account.user_email.split('@').last, admin_accounts_path(email: "%@#{account.user_email.split('@').last}"), title: account.user_email
|
||||
= link_to account.user_email_domain, admin_accounts_path(email: "%@#{account.user_email_domain}"), title: account.user_email
|
||||
- else
|
||||
\-
|
||||
%br/
|
||||
|
|
|
@ -22,10 +22,10 @@
|
|||
%td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= account.user_email
|
||||
%td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(account.id) if can?(:change_email, account.user)
|
||||
%tr
|
||||
%td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{account.user_email.split('@').last}")
|
||||
%td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{account.user_email_domain}")
|
||||
- if can?(:create, :email_domain_block)
|
||||
%tr
|
||||
%td= table_link_to 'hide_source', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: account.user_email.split('@').last)
|
||||
%td= table_link_to 'hide_source', t('admin.accounts.add_email_domain_block'), new_admin_email_domain_block_path(_domain: account.user_email_domain)
|
||||
- if account.user_unconfirmed_email.present?
|
||||
%tr
|
||||
%th= t('admin.accounts.unconfirmed_email')
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
= date_range(@time_period)
|
||||
|
||||
- if @instance.persisted?
|
||||
= render 'dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first
|
||||
= render 'admin/instances/dashboard', instance_domain: @instance.domain, period_end_at: @time_period.last, period_start_at: @time_period.first
|
||||
- else
|
||||
%p
|
||||
= t('admin.instances.unknown_instance')
|
||||
|
@ -55,6 +55,24 @@
|
|||
= render partial: 'admin/action_logs/action_log', collection: @action_logs
|
||||
= link_to t('admin.instances.audit_log.view_all'), admin_action_logs_path(target_domain: @instance.domain), class: 'button'
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
- if @instance.domain.present?
|
||||
%h3#instance-notes= t('admin.instances.moderation_notes.title')
|
||||
%p= t('admin.instances.moderation_notes.description_html')
|
||||
.report-notes
|
||||
= render partial: 'admin/report_notes/report_note', collection: @instance_moderation_notes
|
||||
|
||||
= simple_form_for @instance_moderation_note, url: admin_instance_moderation_notes_path(instance_id: @instance.domain) do |form|
|
||||
= render 'shared/error_messages', object: @instance_moderation_note
|
||||
|
||||
.field-group
|
||||
= form.input :content, input_html: { placeholder: t('admin.instances.moderation_notes.placeholder'), maxlength: InstanceModerationNote::CONTENT_SIZE_LIMIT, rows: 6, autofocus: @instance_moderation_note.errors.any? }
|
||||
|
||||
.actions
|
||||
= form.button :button, t('admin.instances.moderation_notes.create'), type: :submit
|
||||
|
||||
- if @instance.persisted?
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t('admin.instances.availability.title')
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
.report-notes__item
|
||||
.report-notes__item{ id: dom_id(report_note) }
|
||||
= image_tag report_note.account.avatar.url, class: 'report-notes__item__avatar'
|
||||
|
||||
.report-notes__item__header
|
||||
%span.username
|
||||
= link_to report_note.account.username, admin_account_path(report_note.account_id)
|
||||
%time.relative-formatted{ datetime: report_note.created_at.iso8601, title: report_note.created_at }
|
||||
= l report_note.created_at.to_date
|
||||
%a.timestamp{ href: "##{dom_id(report_note)}" }
|
||||
%time.relative-formatted{ datetime: report_note.created_at.iso8601, title: report_note.created_at }
|
||||
= l report_note.created_at.to_date
|
||||
|
||||
.report-notes__item__content
|
||||
= linkify(report_note.content)
|
||||
|
@ -14,5 +15,7 @@
|
|||
.report-notes__item__actions
|
||||
- if report_note.is_a?(AccountModerationNote)
|
||||
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_account_moderation_note_path(report_note), method: :delete
|
||||
- elsif report_note.is_a?(InstanceModerationNote)
|
||||
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_instance_moderation_note_path(instance_id: report_note.domain, id: report_note.id), method: :delete
|
||||
- else
|
||||
= table_link_to 'delete', t('admin.reports.notes.delete'), admin_report_note_path(report_note), method: :delete
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
= f.input :language,
|
||||
collection: ui_languages,
|
||||
include_blank: false,
|
||||
label_method: ->(locale) { native_locale_name(locale) }
|
||||
label_method: ->(locale) { "#{native_locale_name(locale)} (#{standard_locale_name(locale)})" }
|
||||
|
||||
.fields-row__column.fields-group
|
||||
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
|
||||
|
|
|
@ -47,7 +47,6 @@ require_relative '../lib/chewy/strategy/mastodon'
|
|||
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
||||
require_relative '../lib/rails/engine_extensions'
|
||||
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
||||
require_relative '../lib/stoplight/redis_data_store_extensions'
|
||||
require_relative '../lib/active_record/database_tasks_extensions'
|
||||
require_relative '../lib/active_record/batches'
|
||||
require_relative '../lib/simple_navigation/item_extensions'
|
||||
|
|
|
@ -20,6 +20,7 @@ ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|||
inflect.acronym 'DeepL'
|
||||
inflect.acronym 'DSL'
|
||||
inflect.acronym 'JsonLd'
|
||||
inflect.acronym 'OAuth'
|
||||
inflect.acronym 'OEmbed'
|
||||
inflect.acronym 'OStatus'
|
||||
inflect.acronym 'PubSubHubbub'
|
||||
|
|
|
@ -2,117 +2,117 @@
|
|||
ru:
|
||||
devise:
|
||||
confirmations:
|
||||
confirmed: Ваш адрес e-mail был успешно подтвержден.
|
||||
send_instructions: Вы получите e-mail с инструкцией по подтверждению вашего адреса e-mail в течение нескольких минут.
|
||||
send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail с инструкцией по подтверждению вашего адреса в течение нескольких минут.
|
||||
confirmed: Ваш адрес электронной почты успешно подтверждён.
|
||||
send_instructions: В течение нескольких минут вы получите письмо с инструкциями по подтверждению адреса электронной почты. Если письмо не приходит, проверьте папку «Спам».
|
||||
send_paranoid_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по его подтверждению. Если письмо не приходит, проверьте папку «Спам».
|
||||
failure:
|
||||
already_authenticated: Вы уже вошли.
|
||||
already_authenticated: Вы уже авторизованы.
|
||||
inactive: Ваша учётная запись ещё не активирована.
|
||||
invalid: Неверно введены %{authentication_keys} или пароль.
|
||||
last_attempt: У Вас есть последняя попытка, после чего вход будет заблокирован.
|
||||
invalid: "%{authentication_keys} или пароль введены неверно."
|
||||
last_attempt: У вас осталась последняя попытка ввода пароля до блокировки учётной записи.
|
||||
locked: Ваша учётная запись заблокирована.
|
||||
not_found_in_database: Неверно введены %{authentication_keys} или пароль.
|
||||
omniauth_user_creation_failure: Ошибка создания учетной записи с этим идентификатором.
|
||||
pending: Ваша заявка на вступление всё ещё рассматривается.
|
||||
timeout: Ваша сессия истекла. Пожалуйста, войдите снова, чтобы продолжить.
|
||||
not_found_in_database: "%{authentication_keys} или пароль введены неверно."
|
||||
omniauth_user_creation_failure: Не удалось создать учётную запись с помощью выбранного способа идентификации.
|
||||
pending: Ваша заявка на регистрацию всё ещё рассматривается.
|
||||
timeout: Ваш сеанс закончился. Пожалуйста, войдите снова.
|
||||
unauthenticated: Вам необходимо войти или зарегистрироваться.
|
||||
unconfirmed: Вам необходимо подтвердить ваш адрес e-mail для продолжения.
|
||||
unconfirmed: Вы должны подтвердить свой адрес электронной почты.
|
||||
mailer:
|
||||
confirmation_instructions:
|
||||
action: Подтвердить e-mail адрес
|
||||
action: Подтвердить
|
||||
action_with_app: Подтвердить и вернуться в %{app}
|
||||
explanation: Вы создали учётную запись на сайте %{host}, используя этот e-mail адрес. Остался лишь один шаг для активации. Если это были не вы, просто игнорируйте письмо.
|
||||
explanation_when_pending: Вы подали заявку на %{host}, используя этот адрес e-mail. Как только вы его подтвердите, мы начнём изучать вашу заявку. До тех пор вы не сможете войти на сайт. Если ваша заявка будет отклонена, все данные будут автоматически удалены, от вас не потребуется никаких дополнительных действий. Если это были не вы, пожалуйста, проигнорируйте данное письмо.
|
||||
extra_html: Пожалуйста, ознакомьтесь <a href="%{terms_path}">правилами узла</a> and <a href="%{policy_path}">условиями пользования Сервисом</a>.
|
||||
subject: 'Mastodon: Инструкция по подтверждению на узле %{instance}'
|
||||
title: Подтвердите e-mail адрес
|
||||
explanation: Вы создали учётную запись на сайте %{host}, используя этот адрес электронной почты. Остался лишь один шаг для её активации. Если это сделали не вы, просто проигнорируйте это письмо.
|
||||
explanation_when_pending: Вы подали заявку, чтобы создать учётную запись на сайте %{host}, используя этот адрес электронной почты. После того как вы его подтвердите, мы начнём рассматривать вашу заявку. До тех пор вы сможете войти на сайт только для того, чтобы редактировать данные своей учётной записи или удалить её. Если ваша заявка будет отклонена, все данные будут автоматически удалены, от вас не потребуется никаких дополнительных действий. Если заявку подали не вы, пожалуйста, проигнорируйте это письмо.
|
||||
extra_html: Пожалуйста, также ознакомьтесь с <a href="%{terms_path}">правилами сервера</a> и <a href="%{policy_path}">политикой конфиденциальности</a>.
|
||||
subject: 'Mastodon: Инструкции по подтверждению учётной записи на %{instance}'
|
||||
title: Подтверждение адреса электронной почты
|
||||
email_changed:
|
||||
explanation: 'E-mail адрес вашей учётной записи будет изменён на:'
|
||||
extra: Если вы не меняли e-mail адрес, возможно кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором узла, если вы уже потеряли доступ к ней.
|
||||
subject: 'Mastodon: Изменён e-mail адрес'
|
||||
title: Новый адрес e-mail
|
||||
explanation: 'Ваш адрес электронной почты будет изменён на:'
|
||||
extra: Если вы не меняли адрес электронной почты, возможно, кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором сервера, если вы уже потеряли к ней доступ.
|
||||
subject: 'Mastodon: Адрес электронной почты изменён'
|
||||
title: Адрес электронной почты изменён
|
||||
password_change:
|
||||
explanation: Пароль Вашей учётной записи был изменён.
|
||||
extra: Если вы не меняли пароль, возможно кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором узла, если вы уже потеряли доступ к ней.
|
||||
subject: 'Mastodon: Пароль изменен'
|
||||
explanation: Ваш пароль был изменён.
|
||||
extra: Если вы не меняли пароль, возможно, кто-то получил доступ к вашей учётной записи. Пожалуйста, немедленно смените пароль или свяжитесь с администратором сервера, если вы уже потеряли к ней доступ.
|
||||
subject: 'Mastodon: Пароль изменён'
|
||||
title: Пароль изменён
|
||||
reconfirmation_instructions:
|
||||
explanation: Для завершения смены e-mail, нажмите кнопку ниже.
|
||||
extra: Если вы не изменяли e-mail, пожалуйста, игнорируйте это письмо. Новый адрес не будет привязан к учётной записи, пока вы не перейдёте по ссылке ниже.
|
||||
subject: 'Mastodon: Подтвердите свой новый e-mail на %{instance}'
|
||||
title: Подтвердите e-mail адрес
|
||||
explanation: Чтобы завершить изменение адреса электронной почты, подтвердите новый адрес.
|
||||
extra: Если запрос инициировали не вы, пожалуйста, проигнорируйте это письмо. Новый адрес не будет привязан к учётной записи, пока вы не перейдёте по ссылке выше.
|
||||
subject: 'Mastodon: Подтвердите новый адрес электронной почты на %{instance}'
|
||||
title: Подтверждение адреса электронной почты
|
||||
reset_password_instructions:
|
||||
action: Смена пароля
|
||||
action: Сменить пароль
|
||||
explanation: Вы запросили новый пароль для вашей учётной записи.
|
||||
extra: Если это сделали не вы, пожалуйста, игнорируйте письмо. Ваш пароль не будет изменён, пока вы не перейдёте по ссылке выше и не создадите новый пароль.
|
||||
subject: 'Mastodon: Инструкция по сбросу пароля'
|
||||
title: Сброс пароля
|
||||
extra: Если вы не запрашивали изменение пароля, проигнорируйте это письмо. Ваш пароль не будет изменён, пока вы не перейдёте по ссылке и не введёте новый пароль.
|
||||
subject: 'Mastodon: Инструкции по восстановлению пароля'
|
||||
title: Восстановление пароля
|
||||
two_factor_disabled:
|
||||
explanation: Вход в систему теперь возможен только с использованием адреса электронной почты и пароля.
|
||||
subject: 'Mastodon: Двухфакторная авторизация отключена'
|
||||
subtitle: Двухфакторная аутентификация для вашей учетной записи была отключена.
|
||||
title: 2ФА отключена
|
||||
explanation: Теперь вход возможен с использованием одних лишь адреса электронной почты и пароля.
|
||||
subject: 'Mastodon: Двухфакторная аутентификация отключена'
|
||||
subtitle: Двухфакторная аутентификация отключена для вашей учетной записи.
|
||||
title: 2FA отключена
|
||||
two_factor_enabled:
|
||||
explanation: Для входа в систему потребуется токен, сгенерированный сопряженным приложением TOTP.
|
||||
subject: 'Mastodon: Настроена двухфакторная авторизация'
|
||||
subtitle: Для вашей учетной записи была включена двухфакторная аутентификация.
|
||||
title: 2ФА включена
|
||||
explanation: Для входа потребуется одноразовый код, сгенерированный сопряжённым приложением TOTP.
|
||||
subject: 'Mastodon: Двухфакторная аутентификация включена'
|
||||
subtitle: Двухфакторная аутентификация включена для вашей учётной записи.
|
||||
title: 2FA включена
|
||||
two_factor_recovery_codes_changed:
|
||||
explanation: Предыдущие резервные коды были аннулированы и созданы новые.
|
||||
subject: 'Mastodon: Резервные коды двуфакторной авторизации обновлены'
|
||||
subtitle: Предыдущие коды восстановления были аннулированы и сгенерированы новые.
|
||||
title: Коды восстановления 2FA изменены
|
||||
explanation: Прежние резервные коды были аннулированы, и были созданы новые.
|
||||
subject: 'Mastodon: Резервные коды двухфакторной аутентификации пересозданы'
|
||||
subtitle: Прежние резервные коды были аннулированы, и были созданы новые.
|
||||
title: Резервные коды 2FA изменены
|
||||
unlock_instructions:
|
||||
subject: 'Mastodon: Инструкция по разблокировке'
|
||||
subject: 'Mastodon: Инструкции по снятию блокировки учётной записи'
|
||||
webauthn_credential:
|
||||
added:
|
||||
explanation: Следующий ключ безопасности был добавлен в вашу учётную запись
|
||||
subject: 'Мастодон: Новый ключ безопасности'
|
||||
title: Был добавлен новый ключ безопасности
|
||||
explanation: Новый электронный ключ добавлен для вашей учётной записи
|
||||
subject: 'Mastodon: Новый электронный ключ'
|
||||
title: Добавлен новый электронный ключ
|
||||
deleted:
|
||||
explanation: Следующий ключ безопасности был удален из вашей учётной записи
|
||||
subject: 'Мастодон: Ключ Безопасности удален'
|
||||
title: Один из ваших защитных ключей был удален
|
||||
explanation: Один из ваших электронных ключей удалён и больше не сможет быть использован для входа в вашу учётную запись
|
||||
subject: 'Mastodon: Электронный ключ удалён'
|
||||
title: Один из ваших электронных ключей удалён
|
||||
webauthn_disabled:
|
||||
explanation: Аутентификация с помощью ключей безопасности была отключена для вашей учетной записи.
|
||||
extra: Теперь вход в систему возможен только с использованием токена, сгенерированного сопряженным приложением TOTP.
|
||||
subject: 'Мастодон: Аутентификация с ключами безопасности отключена'
|
||||
title: Ключи безопасности отключены
|
||||
explanation: Аутентификация по электронным ключам деактивирована для вашей учетной записи.
|
||||
extra: Теперь вход возможен с использованием только лишь одноразового кода, сгенерированного сопряжённым приложением TOTP.
|
||||
subject: 'Mastodon: Аутентификация по электронным ключам деактивирована'
|
||||
title: Вход по электронным ключам деактивирован
|
||||
webauthn_enabled:
|
||||
explanation: Для вашей учетной записи включена аутентификация по ключу безопасности.
|
||||
extra: Теперь ваш ключ безопасности можно использовать для входа в систему.
|
||||
subject: 'Мастодон: Включена аутентификация по ключу безопасности'
|
||||
title: Ключи безопасности включены
|
||||
explanation: Аутентификация по электронным ключам активирована для вашей учетной записи.
|
||||
extra: Теперь ваш электронный ключ можно использовать для входа.
|
||||
subject: 'Mastodon: Аутентификация по электронным ключам активирована'
|
||||
title: Вход по электронным ключам активирован
|
||||
omniauth_callbacks:
|
||||
failure: Не получилось аутентифицировать вас с помощью %{kind} по следующей причине - "%{reason}".
|
||||
success: Аутентификация с помощью учётной записи %{kind} прошла успешно.
|
||||
failure: Вы не можете войти под учётной записью %{kind}, так как «%{reason}».
|
||||
success: Вход выполнен под учётной записью %{kind}.
|
||||
passwords:
|
||||
no_token: Вы можете получить доступ к этой странице, только перейдя по ссылке в e-mail для сброса пароля. Если вы действительно перешли по такой ссылке, пожалуйста, удостоверьтесь, что ссылка была введена полностью и без изменений.
|
||||
send_instructions: Вы получите e-mail с инструкцией по сбросу пароля в течение нескольких минут.
|
||||
send_paranoid_instructions: Если Ваш адрес e-mail есть в нашей базе данных, вы получите e-mail со ссылкой для сброса пароля в течение нескольких минут.
|
||||
updated: Ваш пароль был успешно изменен. Вход выполнен.
|
||||
updated_not_active: Ваш пароль был успешно изменен.
|
||||
no_token: Доступ к этой странице возможен только по ссылке из письма о восстановлении пароля. Если вы перешли по такой ссылке, пожалуйста, убедитесь, что вы скопировали всю ссылку целиком.
|
||||
send_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по восстановлению пароля. Если письмо не приходит, проверьте папку «Спам».
|
||||
send_paranoid_instructions: Если на ваш адрес электронной почты зарегистрирована учётная запись, то в течение нескольких минут вы получите письмо с инструкциями по восстановлению пароля. Если письмо не приходит, проверьте папку «Спам».
|
||||
updated: Ваш пароль изменён. Теперь вы авторизованы.
|
||||
updated_not_active: Ваш пароль изменён.
|
||||
registrations:
|
||||
destroyed: До свидания! Ваша учётная запись была успешно удалена. Мы надеемся скоро увидеть вас снова.
|
||||
update_needs_confirmation: Данные учётной записи обновлены, но нам необходимо подтвердить ваш новый e-mail адрес. Проверьте почту и перейдите по ссылке из письма. Если оно не приходит, проверьте папку «спам».
|
||||
updated: Ваша учётная запись успешно обновлена.
|
||||
destroyed: До свидания! Ваша учётная запись удалена. Надеемся увидеть вас снова.
|
||||
update_needs_confirmation: Вы успешно обновили данные своей учётной записи, но необходимо подтвердить новый адрес электронной почты. Пожалуйста, проверьте свой почтовый ящик и перейдите по ссылке, чтобы закончить процедуру проверки нового адреса. Если письмо не приходит, проверьте папку «Спам».
|
||||
updated: Ваша учётная запись обновлена.
|
||||
sessions:
|
||||
already_signed_out: Выход прошел успешно.
|
||||
signed_in: Вход прошел успешно.
|
||||
signed_out: Выход прошел успешно.
|
||||
already_signed_out: Выход выполнен.
|
||||
signed_in: Вход выполнен.
|
||||
signed_out: Выход выполнен.
|
||||
unlocks:
|
||||
send_instructions: Вы получите e-mail с инструкцией по разблокировке вашей учётной записи в течение нескольких минут.
|
||||
send_paranoid_instructions: Если ваша учётная запись существует, вы получите e-mail с инструкцией по её разблокировке в течение нескольких минут.
|
||||
unlocked: Ваша учётная запись был успешно разблокирована. Пожалуйста, войдите для продолжения.
|
||||
send_instructions: В течение нескольких минут вы получите письмо с инструкциями по разблокировке учётной записи. Если письмо не приходит, проверьте папку «Спам».
|
||||
send_paranoid_instructions: Если ваша учётная запись существует, то в течение нескольких минут вы получите письмо с инструкциями по её разблокировке. Если письмо не приходит, проверьте папку «Спам».
|
||||
unlocked: Ваша учётная запись разблокирована. Теперь вы можете войти.
|
||||
errors:
|
||||
messages:
|
||||
already_confirmed: уже подтвержден, пожалуйста, попробуйте войти
|
||||
confirmation_period_expired: не был подтвержден в течение %{period}, пожалуйста, запросите новый
|
||||
expired: истек, пожалуйста, запросите новый
|
||||
already_confirmed: уже подтверждён. Пожалуйста, попробуйте войти
|
||||
confirmation_period_expired: не был подтверждён в течение %{period}. Пожалуйста, повторите запрос на подтверждение
|
||||
expired: истёк. Пожалуйста, запросите новый код
|
||||
not_found: не найден
|
||||
not_locked: не был заблокирован
|
||||
not_locked: не заблокирован
|
||||
not_saved:
|
||||
few: "%{count} ошибки помешали сохранению этого %{resource}:"
|
||||
many: "%{count} ошибок помешали сохранению этого %{resource}:"
|
||||
one: '1 ошибка помешала сохранению этого %{resource}:'
|
||||
other: "%{count} ошибок помешали сохранению этого %{resource}:"
|
||||
few: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
|
||||
many: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
|
||||
one: "%{resource}: сохранение не удалось из-за %{count} ошибки:"
|
||||
other: "%{resource}: сохранение не удалось из-за %{count} ошибок:"
|
||||
|
|
|
@ -15,43 +15,43 @@ ru:
|
|||
fragment_present: не может содержать фрагмент.
|
||||
invalid_uri: должен быть правильным URI.
|
||||
relative_uri: должен быть абсолютным URI.
|
||||
secured_uri: нужен HTTPS/SSL URI.
|
||||
secured_uri: должен быть HTTPS/SSL URI.
|
||||
doorkeeper:
|
||||
applications:
|
||||
buttons:
|
||||
authorize: Авторизовать
|
||||
cancel: Отменить
|
||||
cancel: Отмена
|
||||
destroy: Удалить
|
||||
edit: Изменить
|
||||
submit: Принять
|
||||
edit: Редактировать
|
||||
submit: Готово
|
||||
confirmations:
|
||||
destroy: Вы уверены?
|
||||
edit:
|
||||
title: Изменить приложение
|
||||
title: Редактирование приложения
|
||||
form:
|
||||
error: Ой! Проверьте Вашу форму на возможные ошибки
|
||||
error: Упс! Проверьте форму на наличие ошибок
|
||||
help:
|
||||
native_redirect_uri: Используйте %{native_redirect_uri} для локального тестирования
|
||||
redirect_uri: Используйте по одной строке на URI
|
||||
redirect_uri: По одному URI на строку
|
||||
scopes: Разделяйте список разрешений пробелами. Оставьте незаполненным для использования разрешений по умолчанию.
|
||||
index:
|
||||
application: Приложение
|
||||
callback_url: URL-адреса обратного вызова
|
||||
callback_url: Callback-адрес URL
|
||||
delete: Удалить
|
||||
empty: У вас нет созданных приложений.
|
||||
empty: Вы ещё не создали ни одного приложения.
|
||||
name: Название
|
||||
new: Новое приложение
|
||||
new: Создать приложение
|
||||
scopes: Разрешения
|
||||
show: Показывать
|
||||
show: Открыть
|
||||
title: Ваши приложения
|
||||
new:
|
||||
title: Создание приложения
|
||||
title: Создать приложение
|
||||
show:
|
||||
actions: Действия
|
||||
application_id: Ключ клиента
|
||||
callback_urls: URL-адреса обратного вызова
|
||||
application_id: ID приложения
|
||||
callback_urls: Callback-адреса URL
|
||||
scopes: Разрешения
|
||||
secret: Секрет
|
||||
secret: Секретный ключ
|
||||
title: 'Приложение: %{name}'
|
||||
authorizations:
|
||||
buttons:
|
||||
|
@ -60,47 +60,47 @@ ru:
|
|||
error:
|
||||
title: Произошла ошибка
|
||||
new:
|
||||
prompt_html: "%{client_name} хочет получить доступ к вашему аккаунту. <strong>Принимайте запрос только в том случае, если узнаёте, откуда он, и доверяете источнику.</strong>"
|
||||
review_permissions: Просмотр разрешений
|
||||
prompt_html: Приложение %{client_name} запрашивает доступ к вашей учётной записи. <strong>Не принимайте этот запрос, если он исходит из незнакомого или недоверенного источника.</strong>
|
||||
review_permissions: Запрашиваемые разрешения
|
||||
title: Требуется авторизация
|
||||
show:
|
||||
title: Скопируйте этот код авторизации и вставьте его в приложении.
|
||||
title: Скопируйте этот код авторизации и вставьте его в приложение.
|
||||
authorized_applications:
|
||||
buttons:
|
||||
revoke: Отозвать авторизацию
|
||||
revoke: Отозвать доступ
|
||||
confirmations:
|
||||
revoke: Вы уверены?
|
||||
index:
|
||||
authorized_at: Доступ получен %{date}
|
||||
description_html: Это приложения, которые могут получить доступ к вашей учетной записи с помощью API. Если здесь есть приложения, которые вы не узнаете, или приложения, работающие неправильно, вы можете отозвать их доступ.
|
||||
last_used_at: Последнее использование %{date}
|
||||
description_html: Это приложения, которые могут получить доступ к вашей учётной записи с помощью API. Если здесь есть приложения, которые вы не узнаёте, или приложения, работающие неправильно, вы можете отозвать им доступ.
|
||||
last_used_at: В последний раз использовалось %{date}
|
||||
never_used: Не использовалось
|
||||
scopes: Разрешения
|
||||
superapp: Внутреннее
|
||||
title: Ваши авторизованные приложения
|
||||
superapp: Служебное приложение
|
||||
title: Связанные приложения
|
||||
errors:
|
||||
messages:
|
||||
access_denied: Владелец ресурса или сервер авторизации ответил отказом на Ваш запрос.
|
||||
credential_flow_not_configured: Поток с предоставлением клиенту пароля завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_from_credentials не был сконфигурирован.
|
||||
invalid_client: Клиентская аутентификация завершилась неудачей (неизвестный клиент, не включена клиентская аутентификация, или метод аутентификации не поддерживается.
|
||||
invalid_code_challenge_method: Метод проверки кода должен быть S256, простой не годится.
|
||||
invalid_grant: Предоставленный доступ некорректен, истек, отозван, не совпадает с URI перенаправления, использованным в запросе авторизации, или был выпущен для другого клиента.
|
||||
invalid_redirect_uri: Включенный URI перенаправления некорректен.
|
||||
access_denied: Владелец ресурса или сервер авторизации ответил отказом на ваш запрос.
|
||||
credential_flow_not_configured: Процесс Resource Owner Password Credentials завершился неудачей, поскольку параметр конфигурации Doorkeeper.configure.resource_owner_from_credentials не был задан.
|
||||
invalid_client: 'Не удалось аутентифицировать клиент по одной из следующих причин: неизвестный клиент; отсутствует аутентификация клиента; неподдерживаемый метод аутентификации.'
|
||||
invalid_code_challenge_method: Функция хеширования для механизма PKCE должна быть установлена в значение S256, метод PLAIN не поддерживается.
|
||||
invalid_grant: Предоставленное разрешение на авторизацию либо недействительно, либо истекло, либо отозвано, либо не соответствует использованному в запросе на авторизацию URI перенаправления, либо было выдано для другого клиента.
|
||||
invalid_redirect_uri: Предоставленный URI перенаправления недействителен.
|
||||
invalid_request:
|
||||
missing_param: 'Отсутствует обязательный параметр: %{value}.'
|
||||
request_not_authorized: Запрос должен быть авторизован. Обязательный параметр для авторизации запроса отсутствует или недействителен.
|
||||
unknown: В запросе отсутствует обязательный параметр, включено неподдерживаемое значение параметра или он имеет иной формат.
|
||||
invalid_resource_owner: Предоставленные данные владельца ресурса некорректны, или владелец ресурса не может быть найден
|
||||
invalid_scope: Запрошенное разрешение некорректно, неизвестно или неверно сформировано.
|
||||
unknown: В запросе отсутствует обязательный параметр либо присутствует неподдерживаемое значение параметра, или запрос является недействительным по какой-либо ещё причине.
|
||||
invalid_resource_owner: Предоставленные данные владельца ресурса недействительны, или владелец ресурса не найден
|
||||
invalid_scope: Запрошенное разрешение недействительно, неизвестно или имеет неправильный формат.
|
||||
invalid_token:
|
||||
expired: Токен доступа истек
|
||||
revoked: Токен доступа был отменен
|
||||
unknown: Токен доступа некорректен
|
||||
resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр Doorkeeper.configure.resource_owner_authenticator не был сконфигурирован.
|
||||
server_error: Сервер авторизации встретился с неожиданной ошибкой, не позволившей ему выполнить запрос.
|
||||
temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или профилактики.
|
||||
expired: Срок действия токена доступа истёк
|
||||
revoked: Токен доступа был отозван
|
||||
unknown: Токен доступа недействителен
|
||||
resource_owner_authenticator_not_configured: Поиск владельца ресурса завершился неудачей, поскольку параметр конфигурации Doorkeeper.configure.resource_owner_authenticator не был задан.
|
||||
server_error: На сервере авторизации произошла непредвиденная ошибка, не позволившая ему выполнить запрос.
|
||||
temporarily_unavailable: Сервер авторизации в данный момент не может выполнить запрос по причине временной перегрузки или технического обслуживания.
|
||||
unauthorized_client: Клиент не авторизован для выполнения этого запроса с использованием этого метода.
|
||||
unsupported_grant_type: Тип авторизации не поддерживается сервером авторизации.
|
||||
unsupported_grant_type: Тип разрешения на авторизацию не поддерживается сервером авторизации.
|
||||
unsupported_response_type: Сервер авторизации не поддерживает этот тип ответа.
|
||||
flash:
|
||||
applications:
|
||||
|
@ -112,33 +112,33 @@ ru:
|
|||
notice: Приложение обновлено.
|
||||
authorized_applications:
|
||||
destroy:
|
||||
notice: Авторизация приложения отозвана.
|
||||
notice: Приложению был отозван доступ.
|
||||
grouped_scopes:
|
||||
access:
|
||||
read: Доступ только для чтения
|
||||
read/write: Доступ на чтение и запись
|
||||
read/write: Доступ для чтения и записи
|
||||
write: Доступ только для записи
|
||||
title:
|
||||
accounts: Учётные записи
|
||||
admin/accounts: Управление учётными записями
|
||||
admin/accounts: Управление учётными записями пользователей
|
||||
admin/all: Все административные функции
|
||||
admin/reports: Управление отчётами
|
||||
all: Полный доступ к вашей учетной записи Mastodon
|
||||
admin/reports: Управление жалобами
|
||||
all: Полный доступ к вашей учётной записи Mastodon
|
||||
blocks: Блокировки
|
||||
bookmarks: Закладки
|
||||
conversations: Диалоги
|
||||
conversations: Беседы
|
||||
crypto: Сквозное шифрование
|
||||
favourites: Избранные
|
||||
favourites: Избранное
|
||||
filters: Фильтры
|
||||
follow: Подписки, заглушенные и заблокированные
|
||||
follow: Подписки, а также списки игнорируемых и заблокированных пользователей
|
||||
follows: Подписки
|
||||
lists: Списки
|
||||
media: Медиафайлы
|
||||
mutes: Игнорирует
|
||||
mutes: Игнорируемые пользователи
|
||||
notifications: Уведомления
|
||||
profile: Ваш профиль Mastodon
|
||||
push: Push-уведомления
|
||||
reports: Обращения
|
||||
reports: Жалобы
|
||||
search: Поиск
|
||||
statuses: Посты
|
||||
layouts:
|
||||
|
@ -150,49 +150,49 @@ ru:
|
|||
title: Требуется авторизация OAuth
|
||||
scopes:
|
||||
admin:read: читать все данные на сервере
|
||||
admin:read:accounts: читать конфиденциальную информацию всех учётных записей
|
||||
admin:read:canonical_email_blocks: чтение конфиденциальной информации всех канонических блоков электронной почты
|
||||
admin:read:domain_allows: чтение конфиденциальной информации для всего домена позволяет
|
||||
admin:read:domain_blocks: чтение конфиденциальной информации для всего домена позволяет
|
||||
admin:read:email_domain_blocks: читать конфиденциальную информацию обо всех блоках домена электронной почты
|
||||
admin:read:ip_blocks: читать конфиденциальную информацию обо всех IP-блоках
|
||||
admin:read:reports: читать конфиденциальную информацию о всех жалобах и учётных записях с жалобами
|
||||
admin:write: модифицировать все данные на сервере
|
||||
admin:write:accounts: производить модерацию учётных записей
|
||||
admin:write:canonical_email_blocks: выполнять действия по модерации канонических блоков электронной почты
|
||||
admin:write:domain_allows: производить модерацию учётных записей
|
||||
admin:write:domain_blocks: выполнять модерационные действия над блокировкой домена
|
||||
admin:write:email_domain_blocks: выполнять действия по модерации блоков домена электронной почты
|
||||
admin:write:ip_blocks: выполнять модерационные действия над блокировками IP
|
||||
admin:write:reports: производить модерацию жалоб
|
||||
crypto: использ. сквозное шифрование
|
||||
follow: управлять подписками и списком блокировок
|
||||
profile: данные вашего профиля только для чтения
|
||||
admin:read:accounts: читать конфиденциальные сведения обо всех учётных записях
|
||||
admin:read:canonical_email_blocks: читать конфиденциальные сведения обо всех блокировках по каноническому адресу электронной почты
|
||||
admin:read:domain_allows: читать конфиденциальные сведения обо всех разрешённых доменах
|
||||
admin:read:domain_blocks: читать конфиденциальные сведения обо всех заблокированных доменах
|
||||
admin:read:email_domain_blocks: читать конфиденциальные сведения обо всех блокировках по домену электронной почты
|
||||
admin:read:ip_blocks: читать конфиденциальные сведения обо всех блокировках по IP-адресу
|
||||
admin:read:reports: читать конфиденциальные сведения обо всех жалобах и учётных записях с жалобами
|
||||
admin:write: вносить изменения во все данные на сервере
|
||||
admin:write:accounts: осуществлять модерацию применительно к учётным записям
|
||||
admin:write:canonical_email_blocks: осуществлять модерацию применительно к блокировкам по каноническому адресу электронной почты
|
||||
admin:write:domain_allows: осуществлять модерацию применительно к разрешённым доменам
|
||||
admin:write:domain_blocks: осуществлять модерацию применительно к заблокированным доменам
|
||||
admin:write:email_domain_blocks: осуществлять модерацию применительно к блокировкам по домену электронной почты
|
||||
admin:write:ip_blocks: осуществлять модерацию применительно к блокировкам по IP-адресу
|
||||
admin:write:reports: осуществлять модерацию применительно к жалобам
|
||||
crypto: использовать сквозное шифрование
|
||||
follow: вносить изменения в отношения с другими пользователями
|
||||
profile: читать исключительно сведения о вашем профиле
|
||||
push: получать push-уведомления
|
||||
read: просматривать данные вашей учётной записи
|
||||
read:accounts: видеть информацию об учётных записях
|
||||
read:blocks: видеть ваши блокировки
|
||||
read:bookmarks: видеть ваши закладки
|
||||
read:favourites: видеть ваше избранное
|
||||
read:filters: видеть ваши фильтры
|
||||
read:follows: видеть ваши подписки
|
||||
read:lists: видеть ваши списки
|
||||
read:mutes: смотреть список игнорируемых
|
||||
read:notifications: получать уведомления
|
||||
read:reports: видеть ваши жалобы
|
||||
read: читать данные вашей учётной записи
|
||||
read:accounts: иметь доступ к информации об учётных записях
|
||||
read:blocks: иметь доступ к вашим блокировкам
|
||||
read:bookmarks: иметь доступ к вашим закладкам
|
||||
read:favourites: иметь доступ к списку постов, которые вы добавили в избранное
|
||||
read:filters: иметь доступ к вашим фильтрам
|
||||
read:follows: иметь доступ к вашим подпискам
|
||||
read:lists: иметь доступ к вашим спискам
|
||||
read:mutes: иметь доступ к списку пользователей, которых вы игнорируете
|
||||
read:notifications: иметь доступ к вашим уведомлениям
|
||||
read:reports: иметь доступ к вашим жалобам
|
||||
read:search: использовать поиск
|
||||
read:statuses: видеть все ваши посты
|
||||
write: изменять все данные вашей учётной записи
|
||||
write:accounts: редактировать ваш профиль
|
||||
read:statuses: иметь доступ ко всем постам
|
||||
write: вносить изменения во все данные вашей учётной записи
|
||||
write:accounts: вносить изменения в ваш профиль
|
||||
write:blocks: блокировать учётные записи и домены
|
||||
write:bookmarks: добавлять посты в закладки
|
||||
write:conversations: игнорировать и удалить разговоры
|
||||
write:favourites: добавить посты в избранное
|
||||
write:conversations: игнорировать и удалять беседы
|
||||
write:favourites: добавлять посты в избранное
|
||||
write:filters: создавать фильтры
|
||||
write:follows: подписываться на людей
|
||||
write:lists: создавать списки
|
||||
write:media: загружать медиафайлы
|
||||
write:mutes: игнорировать людей и обсуждения
|
||||
write:notifications: очищать список уведомлений
|
||||
write:reports: отправлять жалобы на других
|
||||
write:reports: отправлять жалобы на других пользователей
|
||||
write:statuses: публиковать посты
|
||||
|
|
|
@ -578,6 +578,13 @@ en:
|
|||
all: All
|
||||
limited: Limited
|
||||
title: Moderation
|
||||
moderation_notes:
|
||||
create: Add Moderation Note
|
||||
created_msg: Instance moderation note successfully created!
|
||||
description_html: View and leave notes for other moderators and your future self
|
||||
destroyed_msg: Instance moderation note successfully deleted!
|
||||
placeholder: Information about this instance, actions taken, or anything else that will help you moderate this instance in the future.
|
||||
title: Moderation Notes
|
||||
private_comment: Private comment
|
||||
public_comment: Public comment
|
||||
purge: Purge
|
||||
|
|
|
@ -91,6 +91,8 @@ namespace :admin do
|
|||
post :restart_delivery
|
||||
post :stop_delivery
|
||||
end
|
||||
|
||||
resources :moderation_notes, controller: 'instances/moderation_notes', only: [:create, :destroy]
|
||||
end
|
||||
|
||||
resources :rules, only: [:index, :new, :create, :edit, :update, :destroy] do
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSuperappToOauthApplications < ActiveRecord::Migration[5.0]
|
||||
class AddSuperappToOAuthApplications < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
add_column :oauth_applications, :superapp, :boolean, default: false, null: false
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddWebsiteToOauthApplication < ActiveRecord::Migration[5.0]
|
||||
class AddWebsiteToOAuthApplication < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
add_column :oauth_applications, :website, :string
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddLastUsedAtToOauthAccessTokens < ActiveRecord::Migration[6.1]
|
||||
class AddLastUsedAtToOAuthAccessTokens < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
safety_assured do
|
||||
change_table(:oauth_access_tokens, bulk: true) do |t|
|
||||
|
|
|
@ -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
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||
|
||||
class OptimizeNullIndexOauthAccessTokensRefreshToken < ActiveRecord::Migration[5.2]
|
||||
class OptimizeNullIndexOAuthAccessTokensRefreshToken < ActiveRecord::Migration[5.2]
|
||||
include Mastodon::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||
|
||||
class OptimizeNullIndexOauthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2]
|
||||
class OptimizeNullIndexOAuthAccessTokensResourceOwnerId < ActiveRecord::Migration[5.2]
|
||||
include Mastodon::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
|
20
db/schema.rb
20
db/schema.rb
|
@ -191,8 +191,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
|||
t.boolean "hide_collections"
|
||||
t.integer "avatar_storage_schema_version"
|
||||
t.integer "header_storage_schema_version"
|
||||
t.datetime "sensitized_at", precision: nil
|
||||
t.integer "suspension_origin"
|
||||
t.datetime "sensitized_at", precision: nil
|
||||
t.boolean "trendable"
|
||||
t.datetime "reviewed_at", precision: nil
|
||||
t.datetime "requested_review_at", precision: nil
|
||||
|
@ -580,6 +580,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
|||
t.index ["user_id"], name: "index_identities_on_user_id"
|
||||
end
|
||||
|
||||
create_table "instance_moderation_notes", force: :cascade do |t|
|
||||
t.string "domain", null: false
|
||||
t.bigint "account_id", null: false
|
||||
t.text "content"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["domain"], name: "index_instance_moderation_notes_on_domain"
|
||||
end
|
||||
|
||||
create_table "invites", force: :cascade do |t|
|
||||
t.bigint "user_id", null: false
|
||||
t.string "code", default: "", null: false
|
||||
|
@ -595,12 +604,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
|||
end
|
||||
|
||||
create_table "ip_blocks", force: :cascade do |t|
|
||||
t.inet "ip", default: "0.0.0.0", null: false
|
||||
t.integer "severity", default: 0, null: false
|
||||
t.datetime "expires_at", precision: nil
|
||||
t.text "comment", default: "", null: false
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.datetime "updated_at", precision: nil, null: false
|
||||
t.datetime "expires_at", precision: nil
|
||||
t.inet "ip", default: "0.0.0.0", null: false
|
||||
t.integer "severity", default: 0, null: false
|
||||
t.text "comment", default: "", null: false
|
||||
t.index ["ip"], name: "index_ip_blocks_on_ip", unique: true
|
||||
end
|
||||
|
||||
|
@ -1372,6 +1381,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_06_05_110215) do
|
|||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
||||
add_foreign_key "generated_annual_reports", "accounts"
|
||||
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
||||
add_foreign_key "instance_moderation_notes", "accounts", on_delete: :cascade
|
||||
add_foreign_key "invites", "users", on_delete: :cascade
|
||||
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
|
||||
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
|
||||
|
|
|
@ -251,8 +251,7 @@ export default tseslint.config([
|
|||
devDependencies: [
|
||||
'eslint.config.mjs',
|
||||
'app/javascript/mastodon/performance.js',
|
||||
'app/javascript/mastodon/test_setup.js',
|
||||
'app/javascript/mastodon/test_helpers.tsx',
|
||||
'app/javascript/testing/**/*',
|
||||
'app/javascript/**/__tests__/**',
|
||||
'app/javascript/**/*.stories.ts',
|
||||
'app/javascript/**/*.stories.tsx',
|
||||
|
|
|
@ -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)
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Oauth::AuthorizationsController do
|
||||
RSpec.describe OAuth::AuthorizationsController do
|
||||
let(:app) { Doorkeeper::Application.create!(name: 'test', redirect_uri: 'http://localhost/', scopes: 'read') }
|
||||
|
||||
describe 'GET #new' do
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Oauth::AuthorizedApplicationsController do
|
||||
RSpec.describe OAuth::AuthorizedApplicationsController do
|
||||
render_views
|
||||
|
||||
describe 'GET #index' do
|
||||
|
|
7
spec/fabricators/instance_moderation_note_fabricator.rb
Normal file
7
spec/fabricators/instance_moderation_note_fabricator.rb
Normal 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
|
42
spec/models/doorkeeper/access_grant_spec.rb
Normal file
42
spec/models/doorkeeper/access_grant_spec.rb
Normal 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
|
63
spec/models/doorkeeper/access_token_spec.rb
Normal file
63
spec/models/doorkeeper/access_token_spec.rb
Normal 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
|
|
@ -8,8 +8,45 @@ RSpec.describe Doorkeeper::Application do
|
|||
end
|
||||
|
||||
describe 'Validations' do
|
||||
subject { Fabricate :application }
|
||||
|
||||
it { is_expected.to validate_presence_of(:name) }
|
||||
it { is_expected.to validate_presence_of(:uid) }
|
||||
|
||||
it { is_expected.to validate_length_of(:name).is_at_most(described_class::APP_NAME_LIMIT) }
|
||||
it { is_expected.to validate_length_of(:redirect_uri).is_at_most(described_class::APP_REDIRECT_URI_LIMIT) }
|
||||
it { is_expected.to validate_length_of(:website).is_at_most(described_class::APP_WEBSITE_LIMIT) }
|
||||
end
|
||||
|
||||
describe '#redirect_uris' do
|
||||
subject { Fabricate.build(:application, redirect_uri:).redirect_uris }
|
||||
|
||||
context 'with single value' do
|
||||
let(:redirect_uri) { 'https://test.example/one' }
|
||||
|
||||
it { is_expected.to be_an(Array).and(eq(['https://test.example/one'])) }
|
||||
end
|
||||
|
||||
context 'with multiple values' do
|
||||
let(:redirect_uri) { "https://test.example/one\nhttps://test.example/two" }
|
||||
|
||||
it { is_expected.to be_an(Array).and(eq(['https://test.example/one', 'https://test.example/two'])) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#confirmation_redirect_uri' do
|
||||
subject { Fabricate.build(:application, redirect_uri:).confirmation_redirect_uri }
|
||||
|
||||
context 'with single value' do
|
||||
let(:redirect_uri) { 'https://test.example/one ' }
|
||||
|
||||
it { is_expected.to eq('https://test.example/one') }
|
||||
end
|
||||
|
||||
context 'with multiple values' do
|
||||
let(:redirect_uri) { "https://test.example/one \nhttps://test.example/two " }
|
||||
|
||||
it { is_expected.to eq('https://test.example/one') }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
37
spec/models/instance_moderation_note_spec.rb
Normal file
37
spec/models/instance_moderation_note_spec.rb
Normal 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
|
|
@ -3,9 +3,9 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Instance do
|
||||
describe 'Scopes' do
|
||||
before { described_class.refresh }
|
||||
before { described_class.refresh }
|
||||
|
||||
describe 'Scopes' do
|
||||
describe '#searchable' do
|
||||
let(:expected_domain) { 'host.example' }
|
||||
let(:blocked_domain) { 'other.example' }
|
||||
|
|
|
@ -166,6 +166,34 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#email_domain' do
|
||||
subject { described_class.new(email: email).email_domain }
|
||||
|
||||
context 'when value is nil' do
|
||||
let(:email) { nil }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when value is blank' do
|
||||
let(:email) { '' }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when value has valid domain' do
|
||||
let(:email) { 'user@host.example' }
|
||||
|
||||
it { is_expected.to eq('host.example') }
|
||||
end
|
||||
|
||||
context 'when value has no split' do
|
||||
let(:email) { 'user$host.example' }
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_sign_in!' do
|
||||
context 'with an existing user' do
|
||||
let!(:user) { Fabricate :user, last_sign_in_at: 10.days.ago, current_sign_in_at: 1.hour.ago, sign_in_count: 123 }
|
||||
|
|
16
spec/requests/admin/instances/moderation_notes_spec.rb
Normal file
16
spec/requests/admin/instances/moderation_notes_spec.rb
Normal 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
|
|
@ -4,9 +4,9 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe 'Admin Instances' do
|
||||
describe 'GET /admin/instances/:id' do
|
||||
context 'with an unknown domain' do
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
before { sign_in Fabricate(:admin_user) }
|
||||
|
||||
context 'with an unknown domain' do
|
||||
it 'returns http success' do
|
||||
get admin_instance_path(id: 'unknown.example')
|
||||
|
||||
|
@ -14,5 +14,14 @@ RSpec.describe 'Admin Instances' do
|
|||
.to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid domain' do
|
||||
it 'returns http success' do
|
||||
get admin_instance_path(id: ' ')
|
||||
|
||||
expect(response)
|
||||
.to have_http_status(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Oauth Userinfo Endpoint' do
|
||||
RSpec.describe 'OAuth Userinfo Endpoint' do
|
||||
include RoutingHelper
|
||||
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
|
|
@ -35,7 +35,7 @@ RSpec.describe 'Admin::AccountModerationNotes' do
|
|||
end
|
||||
|
||||
def delete_note
|
||||
within('.report-notes__item__actions') do
|
||||
within('.report-notes__item:first-child .report-notes__item__actions') do
|
||||
click_on I18n.t('admin.reports.notes.delete')
|
||||
end
|
||||
end
|
||||
|
|
51
spec/system/admin/instance/moderation_notes_spec.rb
Normal file
51
spec/system/admin/instance/moderation_notes_spec.rb
Normal 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
|
|
@ -29,10 +29,7 @@
|
|||
"vite.config.mts",
|
||||
"vitest.config.mts",
|
||||
"config/vite",
|
||||
"app/javascript/mastodon",
|
||||
"app/javascript/entrypoints",
|
||||
"app/javascript/types",
|
||||
".storybook/*.ts",
|
||||
".storybook/*.tsx"
|
||||
"app/javascript",
|
||||
".storybook/*"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user