mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(app,api): kick reasons, blocked counts and locale fallbacks (#3334)
This commit is contained in:
1 parent
a5c1362f4a
commit
4db13b0375
157 files changed
+3080
-1995
No files matched your search
@@ -78,6 +78,26 @@ describe('Admin guild routes', () => {
|
||||
expect(result.guild.name).toBe(renamed);
|
||||
expect(result.guild.features).toContain('VERIFIED');
|
||||
});
|
||||
test('PATCH /admin/guilds/{guild_id} returns the summary fields only when the body sets content warning fields', async () => {
|
||||
const admin = await createTestAccount(harness);
|
||||
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:update:settings']);
|
||||
const guild = await createGuild(harness, admin.token, `Warning Guild ${Date.now()}`);
|
||||
const result = await createBuilder<AdminGuildUpdate>(harness, `${admin.token}`)
|
||||
.patch(`/admin/guilds/${guild.id}`)
|
||||
.body({nsfw: true, content_warning_level: 1, content_warning_text: 'Graphic content'})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(Object.keys(result.guild).sort()).toEqual([
|
||||
'banner',
|
||||
'features',
|
||||
'icon',
|
||||
'id',
|
||||
'member_count',
|
||||
'name',
|
||||
'nsfw_level',
|
||||
'owner_id',
|
||||
]);
|
||||
});
|
||||
test('PATCH /admin/guilds/{guild_id} requires the ACL selected by every supplied field', async () => {
|
||||
const admin = await createTestAccount(harness);
|
||||
await setUserACLs(harness, admin, ['admin:authenticate', 'guild:update:settings']);
|
||||
|
||||
@@ -358,7 +358,7 @@ export function AuthController(app: HonoApp) {
|
||||
OpenAPI({
|
||||
operationId: 'revert_email_change',
|
||||
summary: 'Revert email change',
|
||||
responseSchema: AuthLoginResponse,
|
||||
responseSchema: AuthTokenWithUserIdResponse,
|
||||
statusCode: 200,
|
||||
security: [],
|
||||
tags: ['Auth'],
|
||||
|
||||
@@ -197,7 +197,7 @@ export class AuthRequestService {
|
||||
if ('registration_pending_approval' in result) {
|
||||
return result;
|
||||
}
|
||||
return await this.toAuthLoginResponse(result);
|
||||
return await this.toAuthTokenResponse(result);
|
||||
}
|
||||
|
||||
async login({
|
||||
@@ -257,13 +257,13 @@ export class AuthRequestService {
|
||||
};
|
||||
}
|
||||
|
||||
async revertEmailChange({data, request}: AuthRevertEmailChangeRequest): Promise<AuthLoginResponse> {
|
||||
async revertEmailChange({data, request}: AuthRevertEmailChangeRequest): Promise<AuthTokenWithUserIdResponse> {
|
||||
const result = await AuthEmailRevert.revertEmailChange(this.apiContext, {
|
||||
token: data.token,
|
||||
password: data.password,
|
||||
request,
|
||||
});
|
||||
return await this.toAuthLoginResponse(result);
|
||||
return await this.toAuthTokenResponse(result);
|
||||
}
|
||||
|
||||
getAuthSessions(userId: UserID, currentSessionIdHash?: Uint8Array): Promise<AuthSessionsResponse> {
|
||||
|
||||
@@ -198,6 +198,7 @@ describe('Email revert flow', () => {
|
||||
.execute();
|
||||
expect(revertResp.token.length).toBeGreaterThan(0);
|
||||
expect(addJob).toHaveBeenCalledWith('syncStripeCustomerEmail', {userId: account.userId});
|
||||
expect(Object.keys(revertResp).sort()).toEqual(['token', 'user', 'user_id']);
|
||||
await createBuilder(harness, account.token).get('/users/@me').expect(401).execute();
|
||||
const user = await createBuilder<UserPrivateResponse>(harness, revertResp.token).get('/users/@me').execute();
|
||||
expect(user.email).toBe(account.email);
|
||||
|
||||
@@ -85,6 +85,7 @@ describe('Auth registration', () => {
|
||||
});
|
||||
expect(reg.token.length).toBeGreaterThan(0);
|
||||
expect(reg.user_id.length).toBeGreaterThan(0);
|
||||
expect(Object.keys(reg).sort()).toEqual(['token', 'user', 'user_id']);
|
||||
});
|
||||
it('grants wildcard admin ACL to first accepted local dev registration', async () => {
|
||||
await getInstanceConfigRepository().updateCaptchaConfig({enabled: false});
|
||||
|
||||
@@ -84,6 +84,7 @@ describe('Auth registration policy', () => {
|
||||
.execute();
|
||||
expect(registration.registration_pending_approval).toBe(true);
|
||||
expect(registration.user_id.length).toBeGreaterThan(0);
|
||||
expect(Object.keys(registration).sort()).toEqual(['registration_pending_approval', 'user_id']);
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/auth/login')
|
||||
.body({email: body.email, password: body.password})
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type ContentI18nKey,
|
||||
} from '@app/api/content_i18n/ContentI18nMessages';
|
||||
import {identityLocale} from '@fluxer/i18n/src/normalization/IdentityLocale';
|
||||
import {compileMessage} from '@fluxer/i18n/src/runtime/CompileMessage';
|
||||
import {createStaticI18n} from '@fluxer/i18n/src/runtime/CreateStaticI18n';
|
||||
import type {MessageArgsForTemplate} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
|
||||
import {validateMessageTemplateVariables} from '@fluxer/i18n/src/runtime/MessageCatalogTypes';
|
||||
@@ -30,7 +31,7 @@ const contentI18n = createStaticI18n<ContentI18nKey, string, ContentI18nRuntimeV
|
||||
validateVariables: (_key, template, variables) => validateMessageTemplateVariables(template, variables),
|
||||
},
|
||||
(template, variables, mf) => {
|
||||
return String(mf.compile(template)(variables));
|
||||
return String(compileMessage(mf, template)(variables));
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Success",
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthLoginResponse"}}}
|
||||
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AuthTokenWithUserIdResponse"}}}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request - The request was malformed or contained invalid data",
|
||||
@@ -11364,6 +11364,13 @@
|
||||
"required": true,
|
||||
"schema": {"description": "The ID of the harvest request", "$ref": "#/components/schemas/SnowflakeType"},
|
||||
"description": "The ID of the harvest request"
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {"description": "The signed download token from the harvest download URL", "type": "string"},
|
||||
"description": "The signed download token from the harvest download URL"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -32650,31 +32657,6 @@
|
||||
"AuthRegisterResponse": {
|
||||
"anyOf": [
|
||||
{"$ref": "#/components/schemas/AuthTokenWithUserIdResponse"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mfa": {
|
||||
"type": "boolean",
|
||||
"const": true,
|
||||
"description": "Indicates MFA is required to complete authentication"
|
||||
},
|
||||
"ticket": {"type": "string", "description": "MFA ticket to use when completing MFA verification"},
|
||||
"allowed_methods": {
|
||||
"maxItems": 10,
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "List of allowed MFA methods"
|
||||
},
|
||||
"totp": {"type": "boolean", "description": "Whether TOTP authenticator MFA is available"},
|
||||
"webauthn": {"type": "boolean", "description": "Whether WebAuthn security key MFA is available"},
|
||||
"backup_codes": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the account has at least one unconsumed backup code"
|
||||
}
|
||||
},
|
||||
"required": ["mfa", "ticket", "allowed_methods", "totp", "webauthn", "backup_codes"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{"$ref": "#/components/schemas/AuthRegistrationPendingApprovalResponse"}
|
||||
]
|
||||
},
|
||||
@@ -35490,7 +35472,7 @@
|
||||
},
|
||||
"provider": {
|
||||
"description": "The provider of the embed (e.g., YouTube, Twitter)",
|
||||
"anyOf": [{"$ref": "#/components/schemas/EmbedAuthorResponse"}, {"type": "null"}]
|
||||
"anyOf": [{"$ref": "#/components/schemas/EmbedProviderResponse"}, {"type": "null"}]
|
||||
},
|
||||
"video": {
|
||||
"description": "The video of the embed",
|
||||
@@ -35618,7 +35600,7 @@
|
||||
},
|
||||
"provider": {
|
||||
"description": "The provider of the embed (e.g., YouTube, Twitter)",
|
||||
"anyOf": [{"$ref": "#/components/schemas/EmbedAuthorResponse"}, {"type": "null"}]
|
||||
"anyOf": [{"$ref": "#/components/schemas/EmbedProviderResponse"}, {"type": "null"}]
|
||||
},
|
||||
"video": {
|
||||
"description": "The video of the embed",
|
||||
@@ -35677,20 +35659,12 @@
|
||||
"required": ["url", "flags"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EmbedAuthorResponse": {
|
||||
"EmbedProviderResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "The name of the author"},
|
||||
"name": {"type": "string", "description": "The name of the provider"},
|
||||
"url": {
|
||||
"description": "The URL of the author",
|
||||
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
|
||||
},
|
||||
"icon_url": {
|
||||
"description": "The URL of the author icon",
|
||||
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
|
||||
},
|
||||
"proxy_icon_url": {
|
||||
"description": "The proxied URL of the author icon",
|
||||
"description": "The URL of the provider",
|
||||
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
|
||||
}
|
||||
},
|
||||
@@ -35723,6 +35697,26 @@
|
||||
"required": ["text"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EmbedAuthorResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string", "description": "The name of the author"},
|
||||
"url": {
|
||||
"description": "The URL of the author",
|
||||
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
|
||||
},
|
||||
"icon_url": {
|
||||
"description": "The URL of the author icon",
|
||||
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
|
||||
},
|
||||
"proxy_icon_url": {
|
||||
"description": "The proxied URL of the author icon",
|
||||
"anyOf": [{"type": "string", "format": "uri"}, {"type": "null"}]
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"EmbedMediaFlags": {
|
||||
"type": "integer",
|
||||
"minimum": 0,
|
||||
@@ -38281,7 +38275,7 @@
|
||||
"user_tag": {
|
||||
"description": "Fluxer tag of the user being reported",
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"minLength": 1,
|
||||
"maxLength": 37
|
||||
}
|
||||
},
|
||||
@@ -38333,7 +38327,7 @@
|
||||
"reported_user_tag": {
|
||||
"description": "Fluxer tag of the user who sent the message",
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"minLength": 1,
|
||||
"maxLength": 37
|
||||
}
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ import type {IEmailDnsValidationService} from '@app/api/infrastructure/IEmailDns
|
||||
import type {IGatewayService} from '@app/api/infrastructure/IGatewayService';
|
||||
import type {ISnowflakeService} from '@app/api/infrastructure/ISnowflakeService';
|
||||
import type {IStorageService} from '@app/api/infrastructure/IStorageService';
|
||||
import {usesUniqueUsernames} from '@app/api/instance/AccountIdentityModeCache';
|
||||
import type {IInviteRepository} from '@app/api/invite/IInviteRepository';
|
||||
import {Logger} from '@app/api/Logger';
|
||||
import type {Attachment} from '@app/api/models/Attachment';
|
||||
@@ -61,6 +62,7 @@ import {ReportStatus, ReportType} from '@app/api/report/IReportRepository';
|
||||
import type {IReportSearchService} from '@app/api/search/IReportSearchService';
|
||||
import type {IUserRepository} from '@app/api/user/IUserRepository';
|
||||
import {isUnderEnforcement} from '@app/api/user/ProfileVisibility';
|
||||
import {findPersonByLoginHandle, type ParsedLoginHandle, parseLoginHandle} from '@app/api/user/UniqueUsernames';
|
||||
import {isAccountClosed} from '@app/api/user/UserHelpers';
|
||||
import type {IWebhookRepository} from '@app/api/webhook/IWebhookRepository';
|
||||
import {buildHashedAssetKey} from '@app/api/worker/utils/AssetArchiveHelpers';
|
||||
@@ -1149,7 +1151,7 @@ export class ReportService {
|
||||
if (!parsed) {
|
||||
throw new InvalidDsaReportTargetError();
|
||||
}
|
||||
const user = await this.userRepository.findByUsernameDiscriminator(parsed.username, parsed.discriminator);
|
||||
const user = await this.lookupTaggedUser(parsed);
|
||||
if (!user) {
|
||||
throw new UnknownUserError();
|
||||
}
|
||||
@@ -1161,10 +1163,17 @@ export class ReportService {
|
||||
if (!parsed || !userId) {
|
||||
return false;
|
||||
}
|
||||
const user = await this.userRepository.findByUsernameDiscriminator(parsed.username, parsed.discriminator);
|
||||
const user = await this.lookupTaggedUser(parsed);
|
||||
return user?.id === userId;
|
||||
}
|
||||
|
||||
private async lookupTaggedUser(parsed: ParsedLoginHandle): Promise<User | null> {
|
||||
if (parsed.discriminator === null) {
|
||||
return findPersonByLoginHandle(this.userRepository, parsed);
|
||||
}
|
||||
return this.userRepository.findByUsernameDiscriminator(parsed.username, parsed.discriminator);
|
||||
}
|
||||
|
||||
private async isChannelReadableByEveryone(channel: Channel): Promise<boolean> {
|
||||
if (!channel.guildId) {
|
||||
return false;
|
||||
@@ -1211,17 +1220,18 @@ export class ReportService {
|
||||
return email.trim().toLowerCase();
|
||||
}
|
||||
|
||||
private parseFluxerTag(tag: string): {
|
||||
username: string;
|
||||
discriminator: number;
|
||||
} | null {
|
||||
private parseFluxerTag(tag: string): ParsedLoginHandle | null {
|
||||
const trimmed = tag.trim();
|
||||
const match = /^(.+)#(\d{4})$/.exec(trimmed);
|
||||
if (!match) return null;
|
||||
return {
|
||||
username: match[1],
|
||||
discriminator: Number.parseInt(match[2], 10),
|
||||
};
|
||||
if (match) {
|
||||
return {
|
||||
username: match[1],
|
||||
discriminator: Number.parseInt(match[2], 10),
|
||||
};
|
||||
}
|
||||
if (!usesUniqueUsernames()) return null;
|
||||
const handle = parseLoginHandle(trimmed);
|
||||
return handle?.discriminator === null ? handle : null;
|
||||
}
|
||||
|
||||
private extractChannelAndMessageFromLink(link: string): {
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
type ReportFlowScreenDef,
|
||||
} from '@app/api/report/flows/ReportFlowScreens';
|
||||
import {getLegacyCategory, isReportReasonKey, type ReportReasonKey} from '@app/api/report/flows/ReportReasonCatalog';
|
||||
import {type LocaleCode, Locales} from '@fluxer/constants/src/Locales';
|
||||
import type {LocaleCode} from '@fluxer/constants/src/Locales';
|
||||
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
|
||||
import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError';
|
||||
import {InvalidReportFlowAnswersError} from '@fluxer/errors/src/domains/moderation/InvalidReportFlowAnswersError';
|
||||
@@ -113,7 +113,6 @@ const REPORT_FLOW_CHECKED_INSTANCES: ReadonlyArray<ReportFlowInstance> = [
|
||||
{selfHosted: true, guidelinesLinked: true},
|
||||
];
|
||||
const ADMIN_LOCALE = 'en-US';
|
||||
const NORWEGIAN_LANGUAGE_CODES: ReadonlySet<string> = new Set(['nb', 'nn']);
|
||||
|
||||
export function getReportFlowScreenKind(screen: ReportFlowScreenDef): ReportFlowScreenKind {
|
||||
if (screen.checklist) {
|
||||
@@ -578,12 +577,7 @@ export function getReportFlowVariant(target: ReportFlowTargetType, surface: Repo
|
||||
}
|
||||
|
||||
export function resolveReportFlowLocale(raw: string | null | undefined): LocaleCode {
|
||||
if (!raw) {
|
||||
return parseAcceptLanguage(null);
|
||||
}
|
||||
const tag = raw.trim().replaceAll('_', '-');
|
||||
const language = tag.split(/[-,;]/)[0].toLowerCase();
|
||||
return parseAcceptLanguage(NORWEGIAN_LANGUAGE_CODES.has(language) ? Locales.NO : tag);
|
||||
return parseAcceptLanguage(raw);
|
||||
}
|
||||
|
||||
function renderCopy(key: ReportFlowCopyKey, locale: string): string {
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {
|
||||
clearTestEmails,
|
||||
createTestAccount,
|
||||
createUniqueEmail,
|
||||
findLastTestEmail,
|
||||
listTestEmails,
|
||||
} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {createReportID} from '@app/api/BrandedTypes';
|
||||
import {getConfig} from '@app/api/Config';
|
||||
import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons';
|
||||
import {ReportRepository} from '@app/api/report/ReportRepository';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@app/api/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
|
||||
import {AccountIdentityModes, TagStyles} from '@fluxer/constants/src/AccountIdentityConstants';
|
||||
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
interface ReportResponse {
|
||||
report_id: string;
|
||||
}
|
||||
|
||||
interface Me {
|
||||
username: string;
|
||||
discriminator: string;
|
||||
}
|
||||
|
||||
async function issueTicket(harness: ApiTestHarness): Promise<string> {
|
||||
await clearTestEmails(harness);
|
||||
const email = createUniqueEmail('dsa-tag');
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/reports/dsa/email/send')
|
||||
.body({email})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
const code = findLastTestEmail(await listTestEmails(harness), 'dsa_report_verification')?.metadata.code;
|
||||
const {ticket} = await createBuilderWithoutAuth<{ticket: string}>(harness)
|
||||
.post('/reports/dsa/email/verify')
|
||||
.body({email, code})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
return ticket;
|
||||
}
|
||||
|
||||
function submitUserReport(harness: ApiTestHarness, ticket: string, userTag: string) {
|
||||
return createBuilderWithoutAuth<ReportResponse>(harness).post('/reports/dsa').body({
|
||||
ticket,
|
||||
report_type: 'user',
|
||||
category: 'harassment',
|
||||
user_tag: userTag,
|
||||
reporter_full_legal_name: 'John Doe',
|
||||
reporter_country_of_residence: 'DE',
|
||||
});
|
||||
}
|
||||
|
||||
async function reportedUserIdOf(reportId: string): Promise<string | undefined> {
|
||||
const report = await new ReportRepository().getReport(createReportID(BigInt(reportId)));
|
||||
return report?.reportedUserId?.toString();
|
||||
}
|
||||
|
||||
async function readMe(harness: ApiTestHarness, token: string): Promise<Me> {
|
||||
return createBuilder<Me>(harness, token).get('/users/@me').expect(HTTP_STATUS.OK).execute();
|
||||
}
|
||||
|
||||
describe('DSA user lookup by tag', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
test('an instance with random tags finds the user by username#1234 and rejects a bare username', async () => {
|
||||
const target = await createTestAccount(harness);
|
||||
const me = await readMe(harness, target.token);
|
||||
const ticket = await issueTicket(harness);
|
||||
await submitUserReport(harness, ticket, me.username)
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_DSA_REPORT_TARGET)
|
||||
.execute();
|
||||
const result = await submitUserReport(harness, ticket, `${me.username}#${me.discriminator}`)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(await reportedUserIdOf(result.report_id)).toBe(target.userId);
|
||||
});
|
||||
|
||||
test('an instance without tags finds the user by bare username and by username#0000', async () => {
|
||||
const config = getConfig();
|
||||
const originalSelfHosted = config.instance.selfHosted;
|
||||
config.instance.selfHosted = true;
|
||||
try {
|
||||
await getInstanceConfigRepository().setAccountIdentityMode(AccountIdentityModes.EMAIL, 'setup', TagStyles.NONE);
|
||||
const target = await createTestAccount(harness);
|
||||
const me = await readMe(harness, target.token);
|
||||
expect(me.discriminator).toBe('0000');
|
||||
for (const userTag of [me.username, ` ${me.username.toUpperCase()} `, `${me.username}#0000`]) {
|
||||
const result = await submitUserReport(harness, await issueTicket(harness), userTag)
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
expect(await reportedUserIdOf(result.report_id)).toBe(target.userId);
|
||||
}
|
||||
await submitUserReport(harness, await issueTicket(harness), 'nobody_has_this_name')
|
||||
.expect(HTTP_STATUS.NOT_FOUND, APIErrorCodes.UNKNOWN_USER)
|
||||
.execute();
|
||||
} finally {
|
||||
config.instance.selfHosted = originalSelfHosted;
|
||||
getInstanceConfigRepository().clearCacheForTesting();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1192,7 +1192,15 @@ describe('report flow rendering', () => {
|
||||
expect(resolveReportFlowLocale('nb')).toBe('no');
|
||||
expect(resolveReportFlowLocale('nb_NO')).toBe('no');
|
||||
expect(resolveReportFlowLocale('nn')).toBe('no');
|
||||
expect(resolveReportFlowLocale('zh-Hant')).toBe('zh-CN');
|
||||
expect(resolveReportFlowLocale('nn-NO')).toBe('no');
|
||||
expect(resolveReportFlowLocale('zh-Hant')).toBe('zh-TW');
|
||||
expect(resolveReportFlowLocale('zh_Hant_TW')).toBe('zh-TW');
|
||||
expect(resolveReportFlowLocale('zh-HK')).toBe('zh-TW');
|
||||
expect(resolveReportFlowLocale('zh-Hans')).toBe('zh-CN');
|
||||
expect(resolveReportFlowLocale('zh')).toBe('zh-CN');
|
||||
expect(resolveReportFlowLocale('fr-CA')).toBe('fr');
|
||||
expect(resolveReportFlowLocale('de-DE')).toBe('de');
|
||||
expect(resolveReportFlowLocale(' de ')).toBe('de');
|
||||
expect(resolveReportFlowLocale('sv')).toBe('sv-SE');
|
||||
expect(resolveReportFlowLocale('pt')).toBe('pt-BR');
|
||||
expect(resolveReportFlowLocale('pt_BR')).toBe('pt-BR');
|
||||
@@ -1200,6 +1208,7 @@ describe('report flow rendering', () => {
|
||||
expect(resolveReportFlowLocale('xx-YY')).toBe('en-US');
|
||||
expect(resolveReportFlowLocale(undefined)).toBe('en-US');
|
||||
expect(getReportFlowResponse('message', 'in_app', 'nb').locale).toBe('no');
|
||||
expect(getReportFlowResponse('message', 'in_app', 'zh-Hant').locale).toBe('zh-TW');
|
||||
expect(getReportFlowResponse('message', 'in_app', 'xx-YY').locale).toBe('en-US');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -423,7 +423,7 @@ export class RepositoryBackedMessageResponseDataService extends MessageResponseD
|
||||
thumbnail: this.mapEmbedMedia(embed.thumbnail, message),
|
||||
footer: this.mapEmbedFooter(embed.footer),
|
||||
fields: embed.fields.length > 0 ? embed.fields.map((field) => this.mapEmbedField(field)) : null,
|
||||
provider: this.mapEmbedAuthor(embed.provider),
|
||||
provider: this.mapEmbedProvider(embed.provider),
|
||||
video: this.mapEmbedMedia(embed.video, message),
|
||||
audio: this.mapEmbedMedia(embed.audio, message),
|
||||
html: embed.html,
|
||||
@@ -434,14 +434,21 @@ export class RepositoryBackedMessageResponseDataService extends MessageResponseD
|
||||
};
|
||||
}
|
||||
|
||||
private mapEmbedAuthor(author: EmbedAuthor | EmbedProvider | null) {
|
||||
private mapEmbedAuthor(author: EmbedAuthor | null) {
|
||||
if (!author?.name) return null;
|
||||
const iconUrl = 'iconUrl' in author ? author.iconUrl : null;
|
||||
return {
|
||||
name: author.name,
|
||||
url: signOwnUrl(author.url),
|
||||
icon_url: signOwnUrl(iconUrl),
|
||||
proxy_icon_url: mediaProxyUrl(iconUrl),
|
||||
icon_url: signOwnUrl(author.iconUrl),
|
||||
proxy_icon_url: mediaProxyUrl(author.iconUrl),
|
||||
};
|
||||
}
|
||||
|
||||
private mapEmbedProvider(provider: EmbedProvider | null) {
|
||||
if (!provider?.name) return null;
|
||||
return {
|
||||
name: provider.name,
|
||||
url: signOwnUrl(provider.url),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {MessageListResponse} from '@fluxer/schema/src/domains/message/MessageRes
|
||||
import {
|
||||
HarvestArchiveResponse,
|
||||
HarvestCreationResponseSchema,
|
||||
HarvestDownloadQuery,
|
||||
HarvestDownloadUrlResponse,
|
||||
HarvestStatusResponseSchema,
|
||||
HarvestStatusResponseSchemaNullable,
|
||||
@@ -282,6 +283,9 @@ export function UserContentController(app: HonoApp) {
|
||||
'/harvest-downloads/:harvestId',
|
||||
RateLimitMiddleware(RateLimitConfigs.USER_HARVEST_DOWNLOAD_FILE),
|
||||
Validator('param', HarvestIdParam),
|
||||
Validator('query', HarvestDownloadQuery, (result, ctx) =>
|
||||
result.success ? undefined : ctx.text('Not Found', 404),
|
||||
),
|
||||
OpenAPI({
|
||||
operationId: 'download_data_harvest_archive',
|
||||
summary: 'Download data harvest archive',
|
||||
@@ -295,10 +299,7 @@ export function UserContentController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const {harvestId} = ctx.req.valid('param');
|
||||
const token = ctx.req.query('token');
|
||||
if (!token) {
|
||||
return ctx.text('Not Found', 404);
|
||||
}
|
||||
const {token} = ctx.req.valid('query');
|
||||
try {
|
||||
const result = await ctx.get('userContentRequestService').streamHarvestDownload({
|
||||
harvestId,
|
||||
|
||||
@@ -163,6 +163,30 @@ describe('Harvest download streaming', () => {
|
||||
const {harvestId} = await completedHarvest(harness);
|
||||
const response = await get(harness, `/harvest-downloads/${harvestId}`);
|
||||
expect(response.status).toBe(404);
|
||||
expect(await response.text()).toBe('Not Found');
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
['an empty token', '?token='],
|
||||
['a token longer than a download URL', `?token=${'a'.repeat(2049)}`],
|
||||
['a token given twice', '?token=a&token=b'],
|
||||
])('answers not found for %s', async (_label, query) => {
|
||||
await withStreamingHarvestDownloads(async () => {
|
||||
const {harvestId} = await completedHarvest(harness);
|
||||
const response = await get(harness, `/harvest-downloads/${harvestId}${query}`);
|
||||
expect(response.status).toBe(404);
|
||||
expect(await response.text()).toBe('Not Found');
|
||||
});
|
||||
});
|
||||
|
||||
test('serves the archive when the link has other query parameters', async () => {
|
||||
await withStreamingHarvestDownloads(async () => {
|
||||
const {account, harvestId} = await completedHarvest(harness);
|
||||
const download = await fetchHarvestDownload(harness, account.token, harvestId);
|
||||
const response = await get(harness, `${downloadPath(download.download_url)}&source=email`);
|
||||
expect(response.status).toBe(200);
|
||||
expect(new Uint8Array(await response.arrayBuffer())).toEqual(ZIP_BYTES);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user