mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
fix(api): send correct staff emails and allow suppressing them (#3048)
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import type {I18nResult, TemplateCompiler} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
import {renderTemplate} from '@fluxer/i18n/src/runtime/RenderTemplate';
|
||||
import {getEffectiveStaticLocale, hasStaticLocale} from '@fluxer/i18n/src/runtime/StaticLocale';
|
||||
import type MessageFormat from '@messageformat/core';
|
||||
import type {default as MessageFormat, MessageFormatOptions} from '@messageformat/core';
|
||||
|
||||
export type StaticLocaleMessages<TKey extends string, TValue> = Partial<Record<TKey, TValue>>;
|
||||
|
||||
@@ -14,6 +14,7 @@ export interface StaticI18nConfig<TKey extends string, TValue, TVariables> {
|
||||
normalizeLocale?: (locale: string) => string;
|
||||
onWarning?: (message: string) => void;
|
||||
validateVariables?: (key: TKey, template: TValue, variables: TVariables) => string | null;
|
||||
messageFormatOptions?: MessageFormatOptions<'string'>;
|
||||
}
|
||||
|
||||
interface StaticI18nModule<TKey extends string, TValue, TVariables> {
|
||||
@@ -70,6 +71,7 @@ export function createStaticI18n<TKey extends string, TValue, TVariables>(
|
||||
variables,
|
||||
compile,
|
||||
messageFormatCache,
|
||||
messageFormatOptions: config.messageFormatOptions,
|
||||
});
|
||||
},
|
||||
hasLocale(locale: string): boolean {
|
||||
|
||||
@@ -109,6 +109,54 @@ export function extractMessageTemplateVariables(template: string): Set<string> {
|
||||
return variables;
|
||||
}
|
||||
|
||||
function collectMessageTemplatePlaceholders(tokens: ReadonlyArray<Token>, placeholders: Set<string>): void {
|
||||
for (const token of tokens) {
|
||||
switch (token.type) {
|
||||
case 'argument': {
|
||||
placeholders.add(token.arg);
|
||||
break;
|
||||
}
|
||||
case 'function': {
|
||||
const param = (token.param ?? [])
|
||||
.map((paramToken) => (paramToken.type === 'content' ? paramToken.value : ''))
|
||||
.join('')
|
||||
.trim();
|
||||
placeholders.add(`${token.arg},${token.key},${param}`);
|
||||
if (token.param) {
|
||||
collectMessageTemplatePlaceholders(token.param, placeholders);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'select': {
|
||||
const keys = token.cases.map((selectCase) => selectCase.key).sort();
|
||||
placeholders.add(`${token.arg},select[${keys.join(',')}]`);
|
||||
for (const selectCase of token.cases) {
|
||||
collectMessageTemplatePlaceholders(selectCase.tokens, placeholders);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'plural':
|
||||
case 'selectordinal': {
|
||||
placeholders.add(`${token.arg},plural`);
|
||||
for (const selectCase of token.cases) {
|
||||
collectMessageTemplatePlaceholders(selectCase.tokens, placeholders);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'content':
|
||||
case 'octothorpe': {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function extractMessageTemplatePlaceholders(template: string): Array<string> {
|
||||
const placeholders = new Set<string>();
|
||||
collectMessageTemplatePlaceholders(parse(template), placeholders);
|
||||
return [...placeholders].sort();
|
||||
}
|
||||
|
||||
export function validateMessageTemplateVariables(
|
||||
template: string,
|
||||
variables: Record<string, unknown> | undefined,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import type {I18nResult, TemplateCompiler} from '@fluxer/i18n/src/runtime/I18nTypes';
|
||||
import MessageFormat from '@messageformat/core';
|
||||
import MessageFormat, {type MessageFormatOptions} from '@messageformat/core';
|
||||
|
||||
interface TemplateRenderOptions<TKey extends string, TValue, TVariables> {
|
||||
key: TKey;
|
||||
@@ -10,6 +10,7 @@ interface TemplateRenderOptions<TKey extends string, TValue, TVariables> {
|
||||
variables: TVariables;
|
||||
compile: TemplateCompiler<TValue, TVariables>;
|
||||
messageFormatCache: Map<string, MessageFormat>;
|
||||
messageFormatOptions?: MessageFormatOptions<'string'>;
|
||||
}
|
||||
|
||||
export function renderTemplate<TKey extends string, TValue, TVariables>({
|
||||
@@ -19,11 +20,12 @@ export function renderTemplate<TKey extends string, TValue, TVariables>({
|
||||
variables,
|
||||
compile,
|
||||
messageFormatCache,
|
||||
messageFormatOptions,
|
||||
}: TemplateRenderOptions<TKey, TValue, TVariables>): I18nResult<TKey, TValue> {
|
||||
try {
|
||||
let messageFormat = messageFormatCache.get(locale);
|
||||
if (!messageFormat) {
|
||||
messageFormat = new MessageFormat(locale);
|
||||
messageFormat = new MessageFormat(locale, messageFormatOptions);
|
||||
messageFormatCache.set(locale, messageFormat);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type {MessageArgsForTemplate, MessageVariablesForTemplate} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
|
||||
import {
|
||||
defineStaticLocaleMessages,
|
||||
extractMessageTemplatePlaceholders,
|
||||
extractMessageTemplateVariables,
|
||||
validateMessageTemplateVariables,
|
||||
} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
|
||||
@@ -19,6 +20,21 @@ describe('message catalog type helpers', () => {
|
||||
expect([...extractMessageTemplateVariables(template)].sort()).toEqual(['count', 'userName']);
|
||||
expect(validateMessageTemplateVariables(template, {count: 2})).toBe('Missing required i18n variable: userName');
|
||||
});
|
||||
it('extracts placeholder signatures including formats and select cases', () => {
|
||||
const template =
|
||||
'{when, date, full} {when, time, short }{reason, select, null {} other {{reason}}} {count, plural, one {# {name}} other {#}}';
|
||||
expect(extractMessageTemplatePlaceholders(template)).toEqual([
|
||||
'count,plural',
|
||||
'name',
|
||||
'reason',
|
||||
'reason,select[null,other]',
|
||||
'when,date,full',
|
||||
'when,time,short',
|
||||
]);
|
||||
expect(extractMessageTemplatePlaceholders('{count, plural, one {#} few {#} other {#}}')).toEqual(
|
||||
extractMessageTemplatePlaceholders('{count, plural, one {#} other {#}}'),
|
||||
);
|
||||
});
|
||||
it('types placeholders from literal templates', () => {
|
||||
type Variables =
|
||||
MessageVariablesForTemplate<'Hello {userName}, you have {count, plural, one {one invite} other {# invites}}.'>;
|
||||
|
||||
@@ -227,6 +227,7 @@ export type ListReportsQuery = z.infer<typeof ListReportsQuery>;
|
||||
export const UpdateReportRequest = z.object({
|
||||
status: z.literal('resolved').describe('The status to move the report to'),
|
||||
public_comment: createStringType(0, 512).optional().describe('Public comment to include with the resolution'),
|
||||
notify_reporter: z.boolean().default(true).describe('Whether to notify the reporter by system DM and email'),
|
||||
});
|
||||
|
||||
export type UpdateReportRequest = z.infer<typeof UpdateReportRequest>;
|
||||
|
||||
@@ -318,7 +318,11 @@ export const TempBanUserRequest = z.object({
|
||||
.min(0)
|
||||
.max(8760)
|
||||
.describe('Duration of the ban in hours. Use 0 for a permanent ban (until manually unbanned).'),
|
||||
reason: createStringType(0, 512).optional().describe('Reason for the temporary ban'),
|
||||
reason: createStringType(0, 512).optional().describe('Reason shown to the user in the ban email'),
|
||||
notify_user: z
|
||||
.boolean()
|
||||
.default(true)
|
||||
.describe('Whether to email the user about a temporary ban. Permanent bans (duration_hours 0) are never emailed'),
|
||||
});
|
||||
|
||||
export type TempBanUserRequest = z.infer<typeof TempBanUserRequest>;
|
||||
@@ -346,6 +350,7 @@ export const ScheduleAccountDeletionRequest = z.object({
|
||||
.describe(
|
||||
'pending_deletion_at of the deletion this request replaces. Required when a deletion is already scheduled for the account',
|
||||
),
|
||||
notify_user: z.boolean().default(true).describe('Whether to email the user about the scheduled deletion'),
|
||||
});
|
||||
|
||||
export type ScheduleAccountDeletionRequest = z.infer<typeof ScheduleAccountDeletionRequest>;
|
||||
@@ -400,6 +405,7 @@ export const DisableForSuspiciousActivityRequest = z.object({
|
||||
'Bitmask of suspicious activity flags that triggered the disable',
|
||||
'SuspiciousActivityFlags',
|
||||
),
|
||||
notify_user: z.boolean().default(true).describe('Whether to email the user that the account was disabled'),
|
||||
});
|
||||
|
||||
export type DisableForSuspiciousActivityRequest = z.infer<typeof DisableForSuspiciousActivityRequest>;
|
||||
@@ -653,6 +659,15 @@ export const AdminUserDeletionCancelRequest = z.object({
|
||||
|
||||
export type AdminUserDeletionCancelRequest = z.infer<typeof AdminUserDeletionCancelRequest>;
|
||||
|
||||
export const AdminUserUnbanRequest = z.object({
|
||||
notify_user: z.boolean().default(true).describe('Whether to email the user that the suspension was lifted'),
|
||||
public_reason: createStringType(0, 512)
|
||||
.optional()
|
||||
.describe('Reason shown to the user in the unban email. The audit log reason is never emailed'),
|
||||
});
|
||||
|
||||
export type AdminUserUnbanRequest = z.infer<typeof AdminUserUnbanRequest>;
|
||||
|
||||
export const AdminUserBanNoteRequest = z.object({
|
||||
ban_audit_log_id: SnowflakeType.describe('Audit log entry of the current ban that the note refers to'),
|
||||
note: createStringType(1, 512).describe('Note to append to the ban. Recorded as the reason of a new audit log entry'),
|
||||
|
||||
Reference in New Issue
Block a user