diff --git a/fluxer_api/package.json b/fluxer_api/package.json index f5ff8f31f..ade0d74e1 100644 --- a/fluxer_api/package.json +++ b/fluxer_api/package.json @@ -70,7 +70,6 @@ "jose": "catalog:", "livekit-server-sdk": "catalog:", "lodash": "catalog:", - "luxon": "catalog:", "maxmind": "catalog:", "mime": "catalog:", "nats": "catalog:", @@ -89,7 +88,6 @@ "devDependencies": { "@types/archiver": "catalog:", "@types/lodash": "catalog:", - "@types/luxon": "catalog:", "@typescript/native-preview": "catalog:", "esbuild": "catalog:", "msw": "catalog:", diff --git a/fluxer_api/src/api/Tables.ts b/fluxer_api/src/api/Tables.ts index 85764dabc..df4182cc8 100644 --- a/fluxer_api/src/api/Tables.ts +++ b/fluxer_api/src/api/Tables.ts @@ -305,8 +305,6 @@ import { type RelationshipRow, SAVED_MESSAGE_COLUMNS, type SavedMessageRow, - SCHEDULED_MESSAGE_COLUMNS, - type ScheduledMessageRow, USER_BY_EMAIL_COLUMNS, USER_BY_LAST_ACTIVE_IP_COLUMNS, USER_BY_LAST_ACTIVE_IP_TRUST_KEY_COLUMNS, @@ -684,11 +682,6 @@ export const SavedMessages = defineTable({ - name: 'scheduled_messages', - columns: SCHEDULED_MESSAGE_COLUMNS, - primaryKey: ['user_id', 'scheduled_message_id'], -}); export const PushSubscriptions = defineTable({ name: 'push_subscriptions', columns: PUSH_SUBSCRIPTION_COLUMNS, diff --git a/fluxer_api/src/api/ZodErrorMap.ts b/fluxer_api/src/api/ZodErrorMap.ts index 5ab4804ec..7c2ffb97a 100644 --- a/fluxer_api/src/api/ZodErrorMap.ts +++ b/fluxer_api/src/api/ZodErrorMap.ts @@ -66,8 +66,6 @@ function fluxerZodErrorMap(issue: FluxerZodErrorMapIssue): FluxerZodErrorMapResu const origin = 'origin' in issue ? String(issue.origin) : undefined; if (origin === 'string') { errorCode = ValidationErrorCodes.CONTENT_EXCEEDS_MAX_LENGTH; - } else if (origin === 'date') { - errorCode = ValidationErrorCodes.SCHEDULED_TIME_MUST_BE_FUTURE; } else { errorCode = ValidationErrorCodes.INVALID_FORMAT; } diff --git a/fluxer_api/src/api/channel/controllers/ScheduledMessageController.ts b/fluxer_api/src/api/channel/controllers/ScheduledMessageController.ts deleted file mode 100644 index 7f4037ef9..000000000 --- a/fluxer_api/src/api/channel/controllers/ScheduledMessageController.ts +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {ChannelIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; -import { - ScheduledMessageRequestSchema, - ScheduledMessageResponseSchema, -} from '@fluxer/schema/src/domains/message/ScheduledMessageSchemas'; -import {createChannelID} from '../../BrandedTypes'; -import {DefaultUserOnly, LoginRequired} from '../../middleware/AuthMiddleware'; -import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware'; -import {OpenAPI} from '../../middleware/ResponseTypeMiddleware'; -import {RateLimitConfigs} from '../../RateLimitConfig'; -import type {HonoApp} from '../../types/HonoEnv'; -import {Validator} from '../../Validator'; -import {parseScheduledMessageInput} from './ScheduledMessageParsing'; - -export function ScheduledMessageController(app: HonoApp) { - app.post( - '/channels/:channel_id/messages/schedule', - RateLimitMiddleware(RateLimitConfigs.CHANNEL_MESSAGE_CREATE), - LoginRequired, - DefaultUserOnly, - Validator('param', ChannelIdParam), - OpenAPI({ - operationId: 'schedule_message', - summary: 'Schedule a message to send later', - description: - 'Schedules a message to be sent at a specified time. Only available for regular user accounts. Requires permission to send messages in the target channel. Message is sent automatically at the scheduled time. Returns the scheduled message object with delivery time.', - responseSchema: ScheduledMessageResponseSchema, - requestSchema: ScheduledMessageRequestSchema, - requestFormSchema: ScheduledMessageRequestSchema, - statusCode: 201, - security: ['bearerToken', 'sessionToken'], - tags: 'Channels', - }), - async (ctx) => { - const user = ctx.get('user'); - const channelId = createChannelID(ctx.req.valid('param').channel_id); - const scheduledMessageService = ctx.get('scheduledMessageService'); - const {message, scheduledLocalAt, timezone} = await parseScheduledMessageInput({ - ctx, - user, - channelId, - }); - const scheduledMessage = await scheduledMessageService.createScheduledMessage({ - user, - channelId, - data: message, - scheduledLocalAt, - timezone, - }); - return ctx.json(scheduledMessage.toResponse(), 201); - }, - ); -} diff --git a/fluxer_api/src/api/channel/controllers/ScheduledMessageParsing.ts b/fluxer_api/src/api/channel/controllers/ScheduledMessageParsing.ts deleted file mode 100644 index dd5e0c545..000000000 --- a/fluxer_api/src/api/channel/controllers/ScheduledMessageParsing.ts +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; -import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import {MessageRequestSchema} from '@fluxer/schema/src/domains/message/MessageRequestSchemas'; -import {ScheduledMessageRequestSchema} from '@fluxer/schema/src/domains/message/ScheduledMessageSchemas'; -import type {Context} from 'hono'; -import {ms} from 'itty-time'; -import type {ChannelID} from '../../BrandedTypes'; -import type {User} from '../../models/User'; -import type {HonoEnv} from '../../types/HonoEnv'; -import {parseJsonPreservingLargeIntegers} from '../../utils/LosslessJsonParser'; -import type {MessageRequest} from '../MessageTypes'; -import {normalizeMessageRequestPayload} from '../services/message/MessageRequestCompatibility'; -import {parseMultipartMessageData} from '../services/message/MessageRequestParser'; - -type ScheduledMessageSchemaType = ScheduledMessageRequestSchema; - -function extractScheduleFields(data: ScheduledMessageSchemaType): { - scheduled_local_at: string; - timezone: string; - message: MessageRequest; -} { - const {scheduled_local_at, timezone, ...messageData} = data; - return { - scheduled_local_at, - timezone, - message: messageData as MessageRequest, - }; -} - -export async function parseScheduledMessageInput({ - ctx, - user, - channelId, -}: { - ctx: Context; - user: User; - channelId: ChannelID; -}): Promise<{ - message: MessageRequest; - scheduledLocalAt: string; - timezone: string; -}> { - const contentType = ctx.req.header('content-type') ?? ''; - const isMultipart = contentType.includes('multipart/form-data'); - if (isMultipart) { - let parsedPayload: unknown = null; - const message = (await parseMultipartMessageData(ctx, user, channelId, MessageRequestSchema, { - uploadExpiresAt: new Date(Date.now() + ms('32 days')), - onPayloadParsed(payload) { - parsedPayload = payload; - }, - })) as MessageRequest; - if (!parsedPayload) { - throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.FAILED_TO_PARSE_MULTIPART_PAYLOAD); - } - const validation = ScheduledMessageRequestSchema.safeParse(normalizeMessageRequestPayload(parsedPayload)); - if (!validation.success) { - throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.INVALID_SCHEDULED_MESSAGE_PAYLOAD); - } - const {scheduled_local_at, timezone} = extractScheduleFields(validation.data); - return {message, scheduledLocalAt: scheduled_local_at, timezone}; - } - let body: unknown; - try { - const raw = await ctx.req.text(); - body = raw.trim().length === 0 ? {} : parseJsonPreservingLargeIntegers(raw); - } catch { - throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.INVALID_SCHEDULED_MESSAGE_PAYLOAD); - } - const validation = ScheduledMessageRequestSchema.safeParse(normalizeMessageRequestPayload(body)); - if (!validation.success) { - throw InputValidationError.fromCode('scheduled_message', ValidationErrorCodes.INVALID_SCHEDULED_MESSAGE_PAYLOAD); - } - const {scheduled_local_at, timezone, message} = extractScheduleFields(validation.data); - return {message, scheduledLocalAt: scheduled_local_at, timezone}; -} diff --git a/fluxer_api/src/api/channel/controllers/index.ts b/fluxer_api/src/api/channel/controllers/index.ts index fbb1ac093..ee48d4df6 100644 --- a/fluxer_api/src/api/channel/controllers/index.ts +++ b/fluxer_api/src/api/channel/controllers/index.ts @@ -5,14 +5,12 @@ import {CallController} from './CallController'; import {ChannelController} from './ChannelController'; import {MessageController} from './MessageController'; import {MessageInteractionController} from './MessageInteractionController'; -import {ScheduledMessageController} from './ScheduledMessageController'; import {StreamController} from './StreamController'; export function registerChannelControllers(app: HonoApp) { ChannelController(app); MessageInteractionController(app); MessageController(app); - ScheduledMessageController(app); CallController(app); StreamController(app); } diff --git a/fluxer_api/src/api/channel/services/ScheduledMessageService.ts b/fluxer_api/src/api/channel/services/ScheduledMessageService.ts deleted file mode 100644 index 6df674a7e..000000000 --- a/fluxer_api/src/api/channel/services/ScheduledMessageService.ts +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {UserFlags} from '@fluxer/constants/src/UserConstants'; -import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; -import {FeatureTemporarilyDisabledError} from '@fluxer/errors/src/domains/core/FeatureTemporarilyDisabledError'; -import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; -import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService'; -import type {WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes'; -import {ms, seconds} from 'itty-time'; -import {DateTime, IANAZone} from 'luxon'; -import type {ChannelID, MessageID, UserID} from '../../BrandedTypes'; -import {createMessageID} from '../../BrandedTypes'; -import type {ISnowflakeService} from '../../infrastructure/ISnowflakeService'; -import type {ScheduledMessagePayload} from '../../models/ScheduledMessage'; -import {ScheduledMessage} from '../../models/ScheduledMessage'; -import type {User} from '../../models/User'; -import type {ScheduledMessageRepository} from '../../user/repositories/ScheduledMessageRepository'; -import type {WorkerTaskName} from '../../worker/WorkerLaneConfig'; -import type {MessageRequest} from '../MessageTypes'; -import type {ChannelService} from './ChannelService'; - -export const SCHEDULED_MESSAGE_TTL_SECONDS = seconds('31 days'); -const DEFAULT_TIMEZONE = 'UTC'; -const WORKER_TASK_NAME = 'sendScheduledMessage'; - -interface ScheduleParams { - user: User; - channelId: ChannelID; - data: MessageRequest; - scheduledLocalAt: string; - timezone?: string; -} - -interface UpdateScheduleParams extends ScheduleParams { - scheduledMessageId: MessageID; - existing?: ScheduledMessage; -} - -interface SendScheduledMessageWorkerPayload extends WorkerJobPayload { - userId: string; - scheduledMessageId: string; - expectedScheduledAt: string; -} - -export class ScheduledMessageService { - constructor( - private readonly channelService: ChannelService, - private readonly scheduledMessageRepository: ScheduledMessageRepository, - private readonly workerService: IWorkerService, - private readonly snowflakeService: ISnowflakeService, - ) {} - - async listScheduledMessages(userId: UserID): Promise> { - return await this.scheduledMessageRepository.listScheduledMessages(userId); - } - - async getScheduledMessage(userId: UserID, scheduledMessageId: MessageID): Promise { - return await this.scheduledMessageRepository.getScheduledMessage(userId, scheduledMessageId); - } - - async createScheduledMessage(params: ScheduleParams): Promise { - return await this.upsertScheduledMessage({ - ...params, - scheduledMessageId: await createSnowflake(this.snowflakeService), - }); - } - - async updateScheduledMessage(params: UpdateScheduleParams): Promise { - return await this.upsertScheduledMessage({ - ...params, - existing: - params.existing ?? (await this.getScheduledMessage(params.user.id, params.scheduledMessageId)) ?? undefined, - }); - } - - async cancelScheduledMessage(userId: UserID, scheduledMessageId: MessageID): Promise { - await this.scheduledMessageRepository.deleteScheduledMessage(userId, scheduledMessageId); - } - - async markInvalid(userId: UserID, scheduledMessageId: MessageID, reason: string): Promise { - await this.scheduledMessageRepository.markInvalid( - userId, - scheduledMessageId, - reason, - SCHEDULED_MESSAGE_TTL_SECONDS, - ); - } - - private async upsertScheduledMessage(params: UpdateScheduleParams): Promise { - const {user, channelId, data, scheduledLocalAt, timezone} = params; - if ((user.flags & UserFlags.STAFF) === 0n) { - throw new FeatureTemporarilyDisabledError(); - } - await this.channelService.messages.send.validateMessageCanBeSent({ - user, - channelId, - data, - }); - const scheduledAt = this.resolveScheduledAt(scheduledLocalAt, timezone); - const payload = toScheduledPayload(data); - const existing = params.existing; - const message = new ScheduledMessage({ - userId: user.id, - id: params.scheduledMessageId, - channelId, - scheduledAt, - scheduledLocalAt, - timezone: timezone ?? DEFAULT_TIMEZONE, - payload, - status: 'pending', - statusReason: null, - createdAt: existing?.createdAt, - invalidatedAt: null, - }); - await this.scheduledMessageRepository.upsertScheduledMessage(message, SCHEDULED_MESSAGE_TTL_SECONDS); - await this.scheduleWorker(message); - return message; - } - - private resolveScheduledAt(local: string, timezone?: string): Date { - const zone = timezone?.trim() || DEFAULT_TIMEZONE; - if (!IANAZone.isValidZone(zone)) { - throw InputValidationError.fromCode('timezone', ValidationErrorCodes.INVALID_TIMEZONE_IDENTIFIER); - } - const dt = DateTime.fromISO(local, {zone}); - if (!dt.isValid) { - throw InputValidationError.fromCode( - 'scheduled_local_at', - ValidationErrorCodes.INVALID_DATETIME_FOR_SCHEDULED_SEND, - ); - } - const scheduledAt = dt.toJSDate(); - const now = Date.now(); - const diffMs = scheduledAt.getTime() - now; - if (diffMs <= 0) { - throw InputValidationError.fromCode('scheduled_local_at', ValidationErrorCodes.SCHEDULED_TIME_MUST_BE_FUTURE); - } - if (diffMs > ms('30 days')) { - throw InputValidationError.fromCode('scheduled_local_at', ValidationErrorCodes.SCHEDULED_MESSAGES_MAX_30_DAYS); - } - return scheduledAt; - } - - private async scheduleWorker(message: ScheduledMessage): Promise { - const payload: SendScheduledMessageWorkerPayload = { - userId: message.userId.toString(), - scheduledMessageId: message.id.toString(), - expectedScheduledAt: message.scheduledAt.toISOString(), - }; - await this.workerService.addJob(WORKER_TASK_NAME, payload, {runAt: message.scheduledAt}); - } -} - -async function createSnowflake(snowflakeService: ISnowflakeService): Promise { - return createMessageID(await snowflakeService.generate()); -} - -function toScheduledPayload(data: MessageRequest): ScheduledMessagePayload { - return { - content: data.content ?? null, - embeds: data.embeds, - attachments: data.attachments, - message_reference: data.message_reference - ? { - message_id: data.message_reference.message_id.toString(), - channel_id: data.message_reference.channel_id?.toString(), - guild_id: data.message_reference.guild_id?.toString(), - type: data.message_reference.type, - } - : undefined, - allowed_mentions: data.allowed_mentions - ? { - parse: data.allowed_mentions.parse, - users: data.allowed_mentions.users?.map((id) => id.toString()), - roles: data.allowed_mentions.roles?.map((id) => id.toString()), - replied_user: data.allowed_mentions.replied_user, - } - : undefined, - flags: data.flags, - nonce: data.nonce, - favorite_meme_id: data.favorite_meme_id?.toString(), - sticker_ids: data.sticker_ids?.map((id) => id.toString()), - tts: data.tts, - }; -} diff --git a/fluxer_api/src/api/channel/tests/ScheduledMessageTestUtils.ts b/fluxer_api/src/api/channel/tests/ScheduledMessageTestUtils.ts deleted file mode 100644 index 24035741e..000000000 --- a/fluxer_api/src/api/channel/tests/ScheduledMessageTestUtils.ts +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas'; -import type {ScheduledMessageResponseSchema} from '@fluxer/schema/src/domains/message/ScheduledMessageSchemas'; -import type {z} from 'zod'; -import {ensureSessionStarted} from '../../message/tests/MessageTestUtils'; -import type {ApiTestHarness} from '../../test/ApiTestHarness'; -import {createBuilder, createBuilderWithoutAuth} from '../../test/TestRequestBuilder'; - -type ScheduledMessageResponse = z.infer; - -export async function createGuildChannel( - harness: ApiTestHarness, - token: string, - guildId: string, - name: string, -): Promise<{ - id: string; -}> { - const {response, json} = await createBuilder<{ - id: string; - }>(harness, token) - .post(`/guilds/${guildId}/channels`) - .body({name, type: 0}) - .executeWithResponse(); - if (response.status !== 200 && response.status !== 201) { - throw new Error(`Failed to create guild channel: ${response.status}`); - } - return json as { - id: string; - }; -} - -export async function grantStaffAccess(harness: ApiTestHarness, userId: string): Promise { - await createBuilderWithoutAuth(harness).patch(`/test/users/${userId}/flags`).body({flags: 1}).execute(); -} - -export async function scheduleMessage( - harness: ApiTestHarness, - channelId: string, - token: string, - content: string, - scheduledAt?: Date, -): Promise { - const scheduledTime = scheduledAt ?? new Date(Date.now() + 60 * 1000); - await ensureSessionStarted(harness, token); - const {json} = await createBuilder(harness, token) - .post(`/channels/${channelId}/messages/schedule`) - .body({ - content, - scheduled_local_at: scheduledTime.toISOString(), - timezone: 'UTC', - }) - .expect(201) - .executeWithResponse(); - return json as ScheduledMessageResponse; -} - -export async function updateScheduledMessage( - harness: ApiTestHarness, - scheduledMessageId: string, - token: string, - updates: { - content?: string; - scheduled_local_at?: string; - timezone?: string; - }, -): Promise { - const {json} = await createBuilder(harness, token) - .patch(`/users/@me/scheduled-messages/${scheduledMessageId}`) - .body(updates) - .expect(200) - .executeWithResponse(); - return json as ScheduledMessageResponse; -} - -export async function getScheduledMessages( - harness: ApiTestHarness, - token: string, -): Promise> { - const {json} = await createBuilder>(harness, token) - .get('/users/@me/scheduled-messages') - .expect(200) - .executeWithResponse(); - return json; -} - -export async function getScheduledMessage( - harness: ApiTestHarness, - scheduledMessageId: string, - token: string, - expectedStatus: 200 | 404 = 200, -): Promise { - const {response, json} = await createBuilder(harness, token) - .get(`/users/@me/scheduled-messages/${scheduledMessageId}`) - .expect(expectedStatus) - .executeWithResponse(); - if (response.status === 404) { - return null; - } - return json as ScheduledMessageResponse; -} - -export async function cancelScheduledMessage( - harness: ApiTestHarness, - scheduledMessageId: string, - token: string, -): Promise { - await createBuilder(harness, token) - .delete(`/users/@me/scheduled-messages/${scheduledMessageId}`) - .expect(204) - .execute(); -} - -export async function triggerScheduledMessageWorker( - harness: ApiTestHarness, - userId: string, - scheduledMessageId: string, -): Promise { - await createBuilder(harness, '') - .post(`/test/worker/send-scheduled-message/${userId}/${scheduledMessageId}`) - .expect(200) - .execute(); -} - -export async function getChannelMessages( - harness: ApiTestHarness, - channelId: string, - token: string, - limit = 20, -): Promise> { - const {json} = await createBuilder>(harness, token) - .get(`/channels/${channelId}/messages?limit=${limit}`) - .expect(200) - .executeWithResponse(); - return json; -} - -export function messageFromAuthorContains( - messages: Array, - authorId: string, - content: string, -): boolean { - return messages.some((msg) => msg.author.id === authorId && msg.content === content); -} - -export async function createChannelInvite( - harness: ApiTestHarness, - token: string, - channelId: string, -): Promise<{ - code: string; -}> { - const {json} = await createBuilder<{ - code: string; - }>(harness, token) - .post(`/channels/${channelId}/invites`) - .body({ - max_age: 86400, - }) - .expect(200) - .executeWithResponse(); - return json; -} - -export async function joinGuild(harness: ApiTestHarness, token: string, inviteCode: string): Promise { - await createBuilder(harness, token).post(`/invites/${inviteCode}`).body({}).expect(200).execute(); -} - -export async function removeGuildMember( - harness: ApiTestHarness, - token: string, - guildId: string, - userId: string, -): Promise { - await createBuilder(harness, token).delete(`/guilds/${guildId}/members/${userId}`).expect(204).execute(); -} diff --git a/fluxer_api/src/api/channel/tests/ScheduledMessageTraitGated.test.ts b/fluxer_api/src/api/channel/tests/ScheduledMessageTraitGated.test.ts deleted file mode 100644 index e4b9c0094..000000000 --- a/fluxer_api/src/api/channel/tests/ScheduledMessageTraitGated.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {beforeAll, beforeEach, describe, expect, it} from 'vitest'; -import {createTestAccount} from '../../auth/tests/AuthTestUtils'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; -import {ensureSessionStarted} from '../../message/tests/MessageTestUtils'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import {createBuilder} from '../../test/TestRequestBuilder'; -import {createGuildChannel, grantStaffAccess, scheduleMessage} from './ScheduledMessageTestUtils'; - -describe('Scheduled message staff gating', () => { - let harness: ApiTestHarness; - beforeAll(async () => { - harness = await createApiTestHarness(); - }); - beforeEach(async () => { - await harness.reset(); - }); - it('rejects scheduling message before staff flag granted', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'scheduled-flag'); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-channel'); - await ensureSessionStarted(harness, owner.token); - await createBuilder(harness, owner.token) - .post(`/channels/${channel.id}/messages/schedule`) - .body({ - content: 'trying to schedule', - scheduled_local_at: new Date(Date.now() + 60 * 1000).toISOString(), - timezone: 'UTC', - }) - .expect(403) - .execute(); - await grantStaffAccess(harness, owner.userId); - const scheduled = await scheduleMessage(harness, channel.id, owner.token, 'enabled now'); - expect(scheduled.id).toBeDefined(); - expect(scheduled.id).not.toBe(''); - }); -}); diff --git a/fluxer_api/src/api/channel/tests/ScheduledMessageValidation.test.ts b/fluxer_api/src/api/channel/tests/ScheduledMessageValidation.test.ts deleted file mode 100644 index dcb687179..000000000 --- a/fluxer_api/src/api/channel/tests/ScheduledMessageValidation.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {beforeAll, beforeEach, describe, expect, it} from 'vitest'; -import {createTestAccount} from '../../auth/tests/AuthTestUtils'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; -import {ensureSessionStarted} from '../../message/tests/MessageTestUtils'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import {generateFutureTimestamp, generatePastTimestamp, HTTP_STATUS, TEST_LIMITS} from '../../test/TestConstants'; -import {createBuilder} from '../../test/TestRequestBuilder'; -import {createGuildChannel, grantStaffAccess, scheduleMessage} from './ScheduledMessageTestUtils'; - -describe('Scheduled message validation', () => { - let harness: ApiTestHarness; - beforeAll(async () => { - harness = await createApiTestHarness(); - }); - beforeEach(async () => { - await harness.reset(); - }); - it('rejects scheduling message with past time', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'sched-validation-past'); - await grantStaffAccess(harness, owner.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'test'); - await ensureSessionStarted(harness, owner.token); - const pastTime = generatePastTimestamp(1); - const {json} = await createBuilder<{ - errors: Array<{ - path: string; - message: string; - code: string; - }>; - }>(harness, owner.token) - .post(`/channels/${channel.id}/messages/schedule`) - .body({ - content: 'should fail - past time', - scheduled_local_at: pastTime, - timezone: 'UTC', - }) - .expect(HTTP_STATUS.BAD_REQUEST) - .executeWithResponse(); - const errorJson = json as { - errors: Array<{ - path: string; - message: string; - code: string; - }>; - }; - const hasScheduledAtError = errorJson.errors.some((e) => e.path === 'scheduled_local_at'); - expect(hasScheduledAtError).toBe(true); - }); - it('rejects scheduling message exceeding 30 days', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'sched-validation-30day'); - await grantStaffAccess(harness, owner.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'test'); - await ensureSessionStarted(harness, owner.token); - const futureTime = new Date(Date.now() + TEST_LIMITS.SCHEDULED_MESSAGE_MAX_DELAY_MS).toISOString(); - const {json} = await createBuilder<{ - errors: Array<{ - path: string; - message: string; - code: string; - }>; - }>(harness, owner.token) - .post(`/channels/${channel.id}/messages/schedule`) - .body({ - content: 'should fail - exceeds 30 days', - scheduled_local_at: futureTime, - timezone: 'UTC', - }) - .expect(HTTP_STATUS.BAD_REQUEST) - .executeWithResponse(); - const errorJson = json as { - errors: Array<{ - path: string; - message: string; - code: string; - }>; - }; - const hasScheduledAtError = errorJson.errors.some((e) => e.path === 'scheduled_local_at'); - expect(hasScheduledAtError).toBe(true); - }); - it('rejects scheduling message with invalid timezone', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'sched-validation-tz'); - await grantStaffAccess(harness, owner.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'test'); - await ensureSessionStarted(harness, owner.token); - const futureTime = generateFutureTimestamp(5); - const {json} = await createBuilder<{ - errors: Array<{ - path: string; - message: string; - code: string; - }>; - }>(harness, owner.token) - .post(`/channels/${channel.id}/messages/schedule`) - .body({ - content: 'should fail - invalid timezone', - scheduled_local_at: futureTime, - timezone: 'Invalid/NotATimezone', - }) - .expect(HTTP_STATUS.BAD_REQUEST) - .executeWithResponse(); - const errorJson = json as { - errors: Array<{ - path: string; - message: string; - code: string; - }>; - }; - const hasTimezoneError = errorJson.errors.some((e) => e.path === 'timezone'); - expect(hasTimezoneError).toBe(true); - }); - it('accepts scheduling message at 30 day boundary', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'sched-validation-boundary'); - await grantStaffAccess(harness, owner.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'test'); - const futureTime = new Date(Date.now() + 29 * 24 * 60 * 60 * 1000 + 23 * 60 * 60 * 1000).toISOString(); - const scheduled = await scheduleMessage( - harness, - channel.id, - owner.token, - 'should succeed - within 30 days', - new Date(futureTime), - ); - expect(scheduled.status).toBe('pending'); - }); -}); diff --git a/fluxer_api/src/api/channel/tests/ScheduledMessageWorkerLifecycle.test.ts b/fluxer_api/src/api/channel/tests/ScheduledMessageWorkerLifecycle.test.ts deleted file mode 100644 index 4ae42481c..000000000 --- a/fluxer_api/src/api/channel/tests/ScheduledMessageWorkerLifecycle.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {beforeAll, beforeEach, describe, expect, it} from 'vitest'; -import {createTestAccount} from '../../auth/tests/AuthTestUtils'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import { - createChannelInvite, - createGuildChannel, - getChannelMessages, - getScheduledMessage, - grantStaffAccess, - joinGuild, - messageFromAuthorContains, - removeGuildMember, - scheduleMessage, - triggerScheduledMessageWorker, - updateScheduledMessage, -} from './ScheduledMessageTestUtils'; - -describe('Scheduled message worker lifecycle', () => { - let harness: ApiTestHarness; - beforeAll(async () => { - harness = await createApiTestHarness(); - }); - beforeEach(async () => { - await harness.reset(); - }); - it('delivers scheduled message when permissions remain', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'scheduled-messages'); - await grantStaffAccess(harness, owner.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled'); - const content = 'scheduled message goes through'; - const scheduled = await scheduleMessage(harness, channel.id, owner.token, content); - await triggerScheduledMessageWorker(harness, owner.userId, scheduled.id); - const fetched = await getScheduledMessage(harness, scheduled.id, owner.token, 404); - expect(fetched).toBeNull(); - const messages = await getChannelMessages(harness, channel.id, owner.token); - expect(messageFromAuthorContains(messages, owner.userId, content)).toBe(true); - }); - it('reschedules pending message before worker execution', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'scheduled-messages'); - await grantStaffAccess(harness, owner.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled'); - const content = 'scheduled message initial content'; - const scheduled = await scheduleMessage(harness, channel.id, owner.token, content); - const oldScheduledAt = new Date(scheduled.scheduled_at); - const updatedContent = 'scheduled message updated content'; - const newLocalTime = new Date(Date.now() + 5 * 60 * 1000); - const newLocalStr = newLocalTime.toISOString(); - const updated = await updateScheduledMessage(harness, scheduled.id, owner.token, { - content: updatedContent, - scheduled_local_at: newLocalStr, - timezone: 'America/Los_Angeles', - }); - expect(updated.status).toBe('pending'); - expect(updated.scheduled_local_at).toBe(newLocalStr); - expect(updated.timezone).toBe('America/Los_Angeles'); - const updatedScheduledAt = new Date(updated.scheduled_at); - expect(updatedScheduledAt.getTime()).toBeGreaterThan(oldScheduledAt.getTime()); - await triggerScheduledMessageWorker(harness, owner.userId, updated.id); - const respMessages = await getChannelMessages(harness, channel.id, owner.token); - expect(messageFromAuthorContains(respMessages, owner.userId, updatedContent)).toBe(true); - expect(messageFromAuthorContains(respMessages, owner.userId, content)).toBe(false); - const fetched = await getScheduledMessage(harness, updated.id, owner.token, 404); - expect(fetched).toBeNull(); - }); - it('marks scheduled message invalid when access lost', async () => { - const owner = await createTestAccount(harness); - const member = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'scheduled-messages'); - await grantStaffAccess(harness, member.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled'); - const invite = await createChannelInvite(harness, owner.token, channel.id); - await joinGuild(harness, member.token, invite.code); - const content = 'scheduled message invalidation'; - const scheduled = await scheduleMessage(harness, channel.id, member.token, content); - await removeGuildMember(harness, owner.token, guild.id, member.userId); - await triggerScheduledMessageWorker(harness, member.userId, scheduled.id); - const fetched = await getScheduledMessage(harness, scheduled.id, member.token); - expect(fetched).not.toBeNull(); - expect(fetched!.status).toBe('invalid'); - expect(fetched!.status_reason).not.toBeNull(); - expect(fetched!.status_reason).not.toBe(''); - const messages = await getChannelMessages(harness, channel.id, owner.token); - const hasContent = messages.some((msg) => msg.content === content); - expect(hasContent).toBe(false); - }); -}); diff --git a/fluxer_api/src/api/channel/tests/ScheduledMessagesListInvalidEntry.test.ts b/fluxer_api/src/api/channel/tests/ScheduledMessagesListInvalidEntry.test.ts deleted file mode 100644 index 50a8d794a..000000000 --- a/fluxer_api/src/api/channel/tests/ScheduledMessagesListInvalidEntry.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {beforeAll, beforeEach, describe, expect, it} from 'vitest'; -import {createTestAccount} from '../../auth/tests/AuthTestUtils'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import { - createChannelInvite, - createGuildChannel, - getScheduledMessages, - grantStaffAccess, - joinGuild, - removeGuildMember, - scheduleMessage, - triggerScheduledMessageWorker, -} from './ScheduledMessageTestUtils'; - -describe('Scheduled messages list invalid entry', () => { - let harness: ApiTestHarness; - beforeAll(async () => { - harness = await createApiTestHarness(); - }); - beforeEach(async () => { - await harness.reset(); - }); - it('shows invalid scheduled message in list', async () => { - const owner = await createTestAccount(harness); - const member = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'scheduled-invalid'); - await grantStaffAccess(harness, member.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-invalid'); - const invite = await createChannelInvite(harness, owner.token, channel.id); - await joinGuild(harness, member.token, invite.code); - const content = 'invalid scheduled'; - const scheduled = await scheduleMessage(harness, channel.id, member.token, content); - await removeGuildMember(harness, owner.token, guild.id, member.userId); - await triggerScheduledMessageWorker(harness, member.userId, scheduled.id); - const list = await getScheduledMessages(harness, member.token); - const entry = list.find((e) => e.id === scheduled.id); - expect(entry).toBeDefined(); - expect(entry!.status).toBe('invalid'); - expect(entry!.status_reason).not.toBeNull(); - expect(entry!.status_reason).not.toBe(''); - }); -}); diff --git a/fluxer_api/src/api/channel/tests/ScheduledMessagesListLifecycle.test.ts b/fluxer_api/src/api/channel/tests/ScheduledMessagesListLifecycle.test.ts deleted file mode 100644 index 27cae8736..000000000 --- a/fluxer_api/src/api/channel/tests/ScheduledMessagesListLifecycle.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {beforeAll, beforeEach, describe, expect, it} from 'vitest'; -import {createTestAccount} from '../../auth/tests/AuthTestUtils'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; -import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; -import { - cancelScheduledMessage, - createGuildChannel, - getScheduledMessages, - grantStaffAccess, - scheduleMessage, -} from './ScheduledMessageTestUtils'; - -describe('Scheduled messages list lifecycle', () => { - let harness: ApiTestHarness; - beforeAll(async () => { - harness = await createApiTestHarness(); - }); - beforeEach(async () => { - await harness.reset(); - }); - it('lists scheduled messages and removes after cancel', async () => { - const owner = await createTestAccount(harness); - const guild = await createGuild(harness, owner.token, 'scheduled-list'); - await grantStaffAccess(harness, owner.userId); - const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-list'); - const content = 'list scheduled'; - const scheduled = await scheduleMessage(harness, channel.id, owner.token, content); - const list = await getScheduledMessages(harness, owner.token); - const found = list.some((entry) => entry.id === scheduled.id); - expect(found).toBe(true); - await cancelScheduledMessage(harness, scheduled.id, owner.token); - const listAfterCancel = await getScheduledMessages(harness, owner.token); - const foundAfterCancel = listAfterCancel.some((entry) => entry.id === scheduled.id); - expect(foundAfterCancel).toBe(false); - }); -}); diff --git a/fluxer_api/src/api/database/types/UserTypes.ts b/fluxer_api/src/api/database/types/UserTypes.ts index bd86a6b4b..c711b7c87 100644 --- a/fluxer_api/src/api/database/types/UserTypes.ts +++ b/fluxer_api/src/api/database/types/UserTypes.ts @@ -343,34 +343,6 @@ export const SAVED_MESSAGE_COLUMNS = [ 'saved_at', ] as const satisfies ReadonlyArray; -export interface ScheduledMessageRow { - user_id: UserID; - scheduled_message_id: MessageID; - channel_id: ChannelID; - payload: string; - scheduled_at: Date; - scheduled_local_at: string; - timezone: string; - status: string; - status_reason: string | null; - created_at: Date; - invalidated_at: Date | null; -} - -export const SCHEDULED_MESSAGE_COLUMNS = [ - 'user_id', - 'scheduled_message_id', - 'channel_id', - 'payload', - 'scheduled_at', - 'scheduled_local_at', - 'timezone', - 'status', - 'status_reason', - 'created_at', - 'invalidated_at', -] as const satisfies ReadonlyArray; - export interface FavoriteMemeRow { user_id: UserID; meme_id: MemeID; diff --git a/fluxer_api/src/api/infrastructure/KVScheduledJobQueueService.ts b/fluxer_api/src/api/infrastructure/KVScheduledJobQueueService.ts deleted file mode 100644 index bba6b7d65..000000000 --- a/fluxer_api/src/api/infrastructure/KVScheduledJobQueueService.ts +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {createHash} from 'node:crypto'; -import type {IKVProvider} from '@pkgs/kv_client/src/IKVProvider'; -import type {WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes'; -import {Logger} from '../Logger'; - -export interface ParkedScheduledJob { - jobIdentity: string; - taskType: string; - payload: WorkerJobPayload; - runAtMs: number; - ledgerJobId: string | null; -} - -export interface ReadyScheduledJob { - job: ParkedScheduledJob; - member: string; -} - -const QUEUE_KEY = 'scheduled_job_queue'; -const SECONDARY_KEY_PREFIX = 'scheduled_job_queue:'; - -export function buildScheduledJobIdentity( - taskType: string, - payload: WorkerJobPayload, - runAtMs: number, - ledgerJobId: string | null, -): string { - if (ledgerJobId !== null) { - return ledgerJobId; - } - return createHash('sha256') - .update(`${taskType}|${runAtMs}|${JSON.stringify(payload)}`) - .digest('hex'); -} - -export class KVScheduledJobQueueService { - constructor(private readonly kvClient: IKVProvider) {} - - private getSecondaryKey(jobIdentity: string): string { - return `${SECONDARY_KEY_PREFIX}${jobIdentity}`; - } - - private serializeQueueItem(job: ParkedScheduledJob): string { - return JSON.stringify(job); - } - - private deserializeQueueItem(value: string): ParkedScheduledJob { - const parsed: unknown = JSON.parse(value); - if (typeof parsed !== 'object' || parsed === null) { - throw new Error('parked scheduled job must be a JSON object'); - } - const {jobIdentity, taskType, payload, runAtMs, ledgerJobId} = parsed as Record; - if (typeof jobIdentity !== 'string' || typeof taskType !== 'string' || typeof runAtMs !== 'number') { - throw new Error('parked scheduled job is missing required fields'); - } - if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) { - throw new Error('parked scheduled job payload must be a JSON object'); - } - return { - jobIdentity, - taskType, - payload: payload as WorkerJobPayload, - runAtMs, - ledgerJobId: typeof ledgerJobId === 'string' ? ledgerJobId : null, - }; - } - - async parkJob(job: ParkedScheduledJob, releaseAt: Date): Promise { - try { - const secondaryKey = this.getSecondaryKey(job.jobIdentity); - const value = this.serializeQueueItem(job); - await this.kvClient.removeBulkDeletion(QUEUE_KEY, secondaryKey); - await this.kvClient.scheduleBulkDeletion(QUEUE_KEY, secondaryKey, releaseAt.getTime(), value); - Logger.debug({jobIdentity: job.jobIdentity, taskType: job.taskType, releaseAt}, 'Parked scheduled job'); - } catch (error) { - Logger.error({error, jobIdentity: job.jobIdentity, taskType: job.taskType}, 'Failed to park scheduled job'); - throw error; - } - } - - async claimJob(jobIdentity: string, member = ''): Promise { - try { - return await this.kvClient.removeBulkDeletion(QUEUE_KEY, this.getSecondaryKey(jobIdentity), member); - } catch (error) { - Logger.error({error, jobIdentity}, 'Failed to claim parked scheduled job'); - throw error; - } - } - - async getReadyJobs(nowMs: number, limit: number): Promise> { - try { - const results = await this.kvClient.zrangebyscore(QUEUE_KEY, '-inf', nowMs, 'LIMIT', 0, limit); - const jobs: Array = []; - for (const result of results) { - try { - jobs.push({job: this.deserializeQueueItem(result), member: result}); - } catch (error) { - Logger.error({error, result}, 'Failed to parse parked scheduled job entry'); - } - } - return jobs; - } catch (error) { - Logger.error({error, nowMs, limit}, 'Failed to fetch ready parked scheduled jobs'); - throw error; - } - } - - async getQueueSize(): Promise { - try { - return await this.kvClient.zcard(QUEUE_KEY); - } catch (error) { - Logger.error({error}, 'Failed to get parked scheduled job queue size'); - throw error; - } - } -} diff --git a/fluxer_api/src/api/middleware/ServiceMiddleware.ts b/fluxer_api/src/api/middleware/ServiceMiddleware.ts index 30ec36710..e54837797 100644 --- a/fluxer_api/src/api/middleware/ServiceMiddleware.ts +++ b/fluxer_api/src/api/middleware/ServiceMiddleware.ts @@ -21,7 +21,6 @@ import {ChannelRepository} from '../channel/ChannelRepository'; import {ChannelRequestService} from '../channel/services/ChannelRequestService'; import {MessageRequestService} from '../channel/services/message/MessageRequestService'; import {createMessageResponseDataService} from '../channel/services/message/MessageResponseDataService'; -import {ScheduledMessageService} from '../channel/services/ScheduledMessageService'; import {StreamService} from '../channel/services/StreamService'; import {ConnectionRequestService} from '../connection/ConnectionRequestService'; import {ConnectionService} from '../connection/ConnectionService'; @@ -148,7 +147,6 @@ import { getReadStateRequestService, getReadStateService, getReportRepository, - getScheduledMessageRepository, getStorageService, getStreamPreviewService, getSweegoWebhookService, @@ -384,7 +382,6 @@ class RequestServices implements RequestScopedServices { private cachedUserChannelRequestService: UserChannelRequestService | undefined; private cachedUserContentRequestService: UserContentRequestService | undefined; private cachedUserRelationshipRequestService: UserRelationshipRequestService | undefined; - private cachedScheduledMessageService: ScheduledMessageService | undefined; private cachedWebhookService: WebhookService | undefined; private cachedWebhookRequestService: WebhookRequestService | undefined; @@ -1022,16 +1019,6 @@ class RequestServices implements RequestScopedServices { return this.cachedUserRelationshipRequestService; } - get scheduledMessageService(): ScheduledMessageService { - this.cachedScheduledMessageService ??= new ScheduledMessageService( - this.channelService, - getScheduledMessageRepository(), - getWorkerService(), - getSnowflakeService(), - ); - return this.cachedScheduledMessageService; - } - get webhookService(): WebhookService { this.cachedWebhookService ??= new WebhookService( getWebhookRepository(), diff --git a/fluxer_api/src/api/middleware/ServiceSingletons.ts b/fluxer_api/src/api/middleware/ServiceSingletons.ts index 7a3d54950..b8862d379 100644 --- a/fluxer_api/src/api/middleware/ServiceSingletons.ts +++ b/fluxer_api/src/api/middleware/ServiceSingletons.ts @@ -60,7 +60,6 @@ import type {IUnfurlerService} from '../infrastructure/IUnfurlerService'; import {KVAccountDeletionQueueService} from '../infrastructure/KVAccountDeletionQueueService'; import {KVActivityTracker} from '../infrastructure/KVActivityTracker'; import {KVBulkMessageDeletionQueueService} from '../infrastructure/KVBulkMessageDeletionQueueService'; -import {KVScheduledJobQueueService} from '../infrastructure/KVScheduledJobQueueService'; import {NatsUnfurlerService} from '../infrastructure/NatsUnfurlerService'; import {PremiumStateReconciliationQueueService} from '../infrastructure/PremiumStateReconciliationQueueService'; import {createDownloadsStorageService, createStorageService} from '../infrastructure/StorageServiceFactory'; @@ -88,7 +87,6 @@ import {EntranceSoundRepository} from '../user/entrance_sound/EntranceSoundRepos import {EntranceSoundService} from '../user/entrance_sound/EntranceSoundService'; import {EmailChangeRepository} from '../user/repositories/auth/EmailChangeRepository'; import {PasswordChangeRepository} from '../user/repositories/auth/PasswordChangeRepository'; -import {ScheduledMessageRepository} from '../user/repositories/ScheduledMessageRepository'; import {UserContactChangeLogRepository} from '../user/repositories/UserContactChangeLogRepository'; import {UserRepository} from '../user/repositories/UserRepository'; import {VisionarySlotRepository} from '../user/repositories/VisionarySlotRepository'; @@ -123,7 +121,6 @@ export const getApplicationRepository = singleton(() => new ApplicationRepositor export const getOAuth2TokenRepository = singleton(() => new OAuth2TokenRepository()); export const getPackRepository = singleton(() => new PackRepository()); export const getGuildDiscoveryRepository = singleton(() => new GuildDiscoveryRepository()); -export const getScheduledMessageRepository = singleton(() => new ScheduledMessageRepository()); export const getEmailChangeRepository = singleton(() => new EmailChangeRepository()); export const getPasswordChangeRepository = singleton(() => new PasswordChangeRepository()); const getUserContactChangeLogRepository = singleton(() => new UserContactChangeLogRepository()); @@ -230,18 +227,6 @@ export function getKVBulkMessageDeletionQueue(): KVBulkMessageDeletionQueueServi return bulkMessageDeletionQueue; } -let scheduledJobQueueClient: IKVProvider | null = null; -let scheduledJobQueue: KVScheduledJobQueueService | null = null; - -export function getKVScheduledJobQueue(): KVScheduledJobQueueService { - const kvClient = getKVClient(); - if (!scheduledJobQueue || scheduledJobQueueClient !== kvClient) { - scheduledJobQueue = new KVScheduledJobQueueService(kvClient); - scheduledJobQueueClient = kvClient; - } - return scheduledJobQueue; -} - let premiumStateQueueClient: IKVProvider | null = null; let premiumStateQueue: PremiumStateReconciliationQueueService | null = null; diff --git a/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts b/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts index 8d9cacfa1..099d14d3c 100644 --- a/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts +++ b/fluxer_api/src/api/middleware/tests/LazyServiceProvider.test.ts @@ -63,7 +63,6 @@ const REQUEST_SERVICE_VARIABLES: ReadonlyArray = [ 'reportRequestService', 'reportService', 'rpcService', - 'scheduledMessageService', 'searchService', 'singleCommunityService', 'snowflakeService', diff --git a/fluxer_api/src/api/models/ScheduledMessage.ts b/fluxer_api/src/api/models/ScheduledMessage.ts deleted file mode 100644 index b51fce5e7..000000000 --- a/fluxer_api/src/api/models/ScheduledMessage.ts +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {MessageReferenceTypeValue} from '@fluxer/constants/src/ChannelConstants'; -import type {AllowedMentionsRequest} from '@fluxer/schema/src/domains/message/SharedMessageSchemas'; -import type {ChannelID, MessageID, UserID} from '../BrandedTypes'; -import type {AttachmentRequestData} from '../channel/AttachmentDTOs'; -import type {MessageRequest} from '../channel/MessageTypes'; -import type {ScheduledMessageRow} from '../database/types/UserTypes'; -import {isJsonRecord, parseJsonWithGuard} from '../utils/JsonBoundaryUtils'; - -type ScheduledMessageStatus = 'pending' | 'invalid'; - -export interface ScheduledMessageReference { - message_id: string; - channel_id?: string; - guild_id?: string; - type?: MessageReferenceTypeValue; -} - -export interface ScheduledAllowedMentions { - parse?: AllowedMentionsRequest['parse']; - users?: Array; - roles?: Array; - replied_user?: boolean; -} - -export interface ScheduledMessagePayload { - content?: string | null; - embeds?: MessageRequest['embeds']; - attachments?: Array; - message_reference?: ScheduledMessageReference; - allowed_mentions?: ScheduledAllowedMentions; - flags?: number; - nonce?: string; - favorite_meme_id?: string; - sticker_ids?: Array; - tts?: boolean; -} - -interface ScheduledMessageResponse { - id: string; - channel_id: string; - scheduled_at: string; - scheduled_local_at: string; - timezone: string; - status: ScheduledMessageStatus; - status_reason: string | null; - payload: ScheduledMessagePayload; - created_at: string; - invalidated_at: string | null; -} - -export class ScheduledMessage { - readonly userId: UserID; - readonly id: MessageID; - readonly channelId: ChannelID; - readonly scheduledAt: Date; - readonly scheduledLocalAt: string; - readonly timezone: string; - readonly payload: ScheduledMessagePayload; - readonly status: ScheduledMessageStatus; - readonly statusReason: string | null; - readonly createdAt: Date; - readonly invalidatedAt: Date | null; - - constructor(params: { - userId: UserID; - id: MessageID; - channelId: ChannelID; - scheduledAt: Date; - scheduledLocalAt: string; - timezone: string; - payload: ScheduledMessagePayload; - status?: ScheduledMessageStatus; - statusReason?: string | null; - createdAt?: Date; - invalidatedAt?: Date | null; - }) { - this.userId = params.userId; - this.id = params.id; - this.channelId = params.channelId; - this.scheduledAt = params.scheduledAt; - this.scheduledLocalAt = params.scheduledLocalAt; - this.timezone = params.timezone; - this.payload = params.payload; - this.status = params.status ?? 'pending'; - this.statusReason = params.statusReason ?? null; - this.createdAt = params.createdAt ?? new Date(); - this.invalidatedAt = params.invalidatedAt ?? null; - } - - static fromRow(row: ScheduledMessageRow): ScheduledMessage { - return new ScheduledMessage({ - userId: row.user_id, - id: row.scheduled_message_id, - channelId: row.channel_id, - scheduledAt: row.scheduled_at, - scheduledLocalAt: row.scheduled_local_at, - timezone: row.timezone, - payload: parsePayload(row.payload), - status: toStatus(row.status), - statusReason: row.status_reason, - createdAt: row.created_at, - invalidatedAt: row.invalidated_at, - }); - } - - toRow(): ScheduledMessageRow { - return { - user_id: this.userId, - scheduled_message_id: this.id, - channel_id: this.channelId, - payload: JSON.stringify(this.payload), - scheduled_at: this.scheduledAt, - scheduled_local_at: this.scheduledLocalAt, - timezone: this.timezone, - status: this.status, - status_reason: this.statusReason, - created_at: this.createdAt, - invalidated_at: this.invalidatedAt, - }; - } - - toResponse(): ScheduledMessageResponse { - return { - id: this.id.toString(), - channel_id: this.channelId.toString(), - scheduled_at: this.scheduledAt.toISOString(), - scheduled_local_at: this.scheduledLocalAt, - timezone: this.timezone, - status: this.status, - status_reason: this.statusReason, - payload: this.payload, - created_at: this.createdAt.toISOString(), - invalidated_at: this.invalidatedAt?.toISOString() ?? null, - }; - } - - parseToMessageRequest(): MessageRequest { - return hydrateMessageRequest(this.payload); - } -} - -function parsePayload(payload: string): ScheduledMessagePayload { - return parseJsonWithGuard(payload, isScheduledMessagePayload) ?? {}; -} - -function isStringArray(value: unknown): value is Array { - return Array.isArray(value) && value.every((entry) => typeof entry === 'string'); -} - -function isScheduledMessageReference(value: unknown): value is ScheduledMessageReference { - if (!isJsonRecord(value)) return false; - return ( - typeof value.message_id === 'string' && - (value.channel_id === undefined || typeof value.channel_id === 'string') && - (value.guild_id === undefined || typeof value.guild_id === 'string') && - (value.type === undefined || typeof value.type === 'number') - ); -} - -function isScheduledAllowedMentions(value: unknown): value is ScheduledAllowedMentions { - if (!isJsonRecord(value)) return false; - return ( - (value.parse === undefined || isStringArray(value.parse)) && - (value.users === undefined || isStringArray(value.users)) && - (value.roles === undefined || isStringArray(value.roles)) && - (value.replied_user === undefined || typeof value.replied_user === 'boolean') - ); -} - -function isScheduledMessagePayload(value: unknown): value is ScheduledMessagePayload { - if (!isJsonRecord(value)) return false; - return ( - (value.content === undefined || value.content === null || typeof value.content === 'string') && - (value.embeds === undefined || Array.isArray(value.embeds)) && - (value.attachments === undefined || Array.isArray(value.attachments)) && - (value.message_reference === undefined || isScheduledMessageReference(value.message_reference)) && - (value.allowed_mentions === undefined || isScheduledAllowedMentions(value.allowed_mentions)) && - (value.flags === undefined || typeof value.flags === 'number') && - (value.nonce === undefined || typeof value.nonce === 'string') && - (value.favorite_meme_id === undefined || typeof value.favorite_meme_id === 'string') && - (value.sticker_ids === undefined || isStringArray(value.sticker_ids)) && - (value.tts === undefined || typeof value.tts === 'boolean') - ); -} - -function toStatus(value: string): ScheduledMessageStatus { - return value === 'invalid' ? 'invalid' : 'pending'; -} - -function hydrateMessageRequest(payload: ScheduledMessagePayload): MessageRequest { - const message: MessageRequest = { - content: payload.content, - embeds: payload.embeds ?? [], - attachments: payload.attachments, - message_reference: payload.message_reference - ? { - ...payload.message_reference, - message_id: BigInt(payload.message_reference.message_id), - channel_id: payload.message_reference.channel_id ? BigInt(payload.message_reference.channel_id) : undefined, - guild_id: payload.message_reference.guild_id ? BigInt(payload.message_reference.guild_id) : undefined, - } - : undefined, - allowed_mentions: payload.allowed_mentions - ? { - parse: payload.allowed_mentions.parse, - users: payload.allowed_mentions.users?.map((value) => BigInt(value)), - roles: payload.allowed_mentions.roles?.map((value) => BigInt(value)), - replied_user: payload.allowed_mentions.replied_user, - } - : undefined, - flags: payload.flags, - nonce: payload.nonce, - favorite_meme_id: payload.favorite_meme_id ? BigInt(payload.favorite_meme_id) : undefined, - sticker_ids: payload.sticker_ids?.map((value) => BigInt(value)), - tts: payload.tts, - }; - return message; -} diff --git a/fluxer_api/src/api/test/TestConstants.ts b/fluxer_api/src/api/test/TestConstants.ts index 21a8fba33..8bdc8e5bc 100644 --- a/fluxer_api/src/api/test/TestConstants.ts +++ b/fluxer_api/src/api/test/TestConstants.ts @@ -47,27 +47,10 @@ export const TEST_IDS = { NONEXISTENT_MESSAGE: '999999999999999999', NONEXISTENT_WEBHOOK: '999999999999999999', } as const; -export const TEST_LIMITS = { - SCHEDULED_MESSAGE_MAX_DAYS: 30, - SCHEDULED_MESSAGE_MIN_DELAY_MS: 5 * 60 * 1000, - SCHEDULED_MESSAGE_MAX_DELAY_MS: 31 * 24 * 60 * 60 * 1000, - MFA_TICKET_SHORT_TTL: 1, - MFA_TICKET_LONG_TTL: 300, - PASSWORD_RESET_TOKEN_LENGTH: 64, -} as const; - export function generateUniquePassword(): string { return `SecurePass-${Date.now()}!`; } -export function generateFutureTimestamp(minutesInFuture = 5): string { - return new Date(Date.now() + minutesInFuture * 60 * 1000).toISOString(); -} - -export function generatePastTimestamp(hoursInPast = 1): string { - return new Date(Date.now() - hoursInPast * 60 * 60 * 1000).toISOString(); -} - export function wait(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/fluxer_api/src/api/test/TestHarnessController.ts b/fluxer_api/src/api/test/TestHarnessController.ts index 26de3b795..85eb57492 100644 --- a/fluxer_api/src/api/test/TestHarnessController.ts +++ b/fluxer_api/src/api/test/TestHarnessController.ts @@ -81,14 +81,12 @@ import {IpAuthorizationTokens, OAuth2AccessTokensByUser} from '../Tables'; import type {HonoApp, HonoEnv} from '../types/HonoEnv'; import {UserSearchRepository} from '../user/repositories/account/crud/UserSearchRepository'; import {AuthSessionRepository} from '../user/repositories/auth/AuthSessionRepository'; -import {ScheduledMessageRepository} from '../user/repositories/ScheduledMessageRepository'; import {UserChannelRepository} from '../user/repositories/UserChannelRepository'; import {UserRepository} from '../user/repositories/UserRepository'; import {processUserDeletion} from '../user/services/UserDeletionService'; import {UserHarvestRepository} from '../user/UserHarvestRepository'; import {getExpiryBucket} from '../utils/AttachmentDecay'; import {parseReportedClientOs} from '../utils/SessionClientIdentity'; -import {ScheduledMessageExecutor} from '../worker/executors/ScheduledMessageExecutor'; import {processExpiredAttachments} from '../worker/tasks/ExpireAttachments'; import {processInactivityDeletionsCore} from '../worker/tasks/ProcessInactivityDeletions'; import {setWorkerDependencies} from '../worker/WorkerContext'; @@ -1754,48 +1752,6 @@ export function TestHarnessController(app: HonoApp) { await snowflakeService.shutdown(); } }); - app.post('/test/worker/send-scheduled-message/:userId/:scheduledMessageId', async (ctx) => { - ensureHarnessAccess(ctx); - const params = ctx.req.param() as { - userId?: string; - scheduledMessageId?: string; - }; - const userIdParam = params.userId; - const scheduledMessageIdParam = params.scheduledMessageId; - if (!userIdParam) { - throw new Error('Missing userId parameter'); - } - if (!scheduledMessageIdParam) { - throw new Error('Missing scheduledMessageId parameter'); - } - const userId = createUserID(BigInt(userIdParam)); - const scheduledMessageId = createMessageID(BigInt(scheduledMessageIdParam)); - const snowflakeService = getSnowflakeService(); - try { - const workerDeps = await initializeWorkerDepsWithHarnessEmail(ctx, snowflakeService); - setWorkerDependencies(workerDeps); - const scheduledMessageRepository = new ScheduledMessageRepository(); - const scheduledMessage = await scheduledMessageRepository.getScheduledMessage(userId, scheduledMessageId); - if (!scheduledMessage) { - return ctx.json({success: false, reason: 'scheduled message not found'}, 404); - } - const logger = { - debug: (message: string, extra?: object) => Logger.debug(extra ?? {}, message), - info: (message: string, extra?: object) => Logger.info(extra ?? {}, message), - warn: (message: string, extra?: object) => Logger.warn(extra ?? {}, message), - error: (message: string, extra?: object) => Logger.error(extra ?? {}, message), - }; - const executor = new ScheduledMessageExecutor(workerDeps, logger, scheduledMessageRepository); - await executor.execute({ - userId: userId.toString(), - scheduledMessageId: scheduledMessageId.toString(), - expectedScheduledAt: scheduledMessage.scheduledAt.toISOString(), - }); - return ctx.json({success: true}, 200); - } finally { - await snowflakeService.shutdown(); - } - }); app.post('/test/attachment-decay/rows', async (ctx) => { ensureHarnessAccess(ctx); const payload = (await ctx.req.json()) as { diff --git a/fluxer_api/src/api/types/HonoEnv.ts b/fluxer_api/src/api/types/HonoEnv.ts index cdf5427e8..f4bde28db 100644 --- a/fluxer_api/src/api/types/HonoEnv.ts +++ b/fluxer_api/src/api/types/HonoEnv.ts @@ -18,7 +18,6 @@ import type {IChannelRepository} from '../channel/IChannelRepository'; import type {ChannelRequestService} from '../channel/services/ChannelRequestService'; import type {ChannelService} from '../channel/services/ChannelService'; import type {MessageRequestService} from '../channel/services/message/MessageRequestService'; -import type {ScheduledMessageService} from '../channel/services/ScheduledMessageService'; import type {StreamPreviewService} from '../channel/services/StreamPreviewService'; import type {StreamService} from '../channel/services/StreamService'; import type {ConnectionRequestService} from '../connection/ConnectionRequestService'; @@ -177,7 +176,6 @@ export interface HonoEnv { webhookService: WebhookService; webhookRequestService: WebhookRequestService; workerService: IWorkerService; - scheduledMessageService: ScheduledMessageService; stripeService: StripeService; ageVerificationService: AgeVerificationService; applicationService: ApplicationService; diff --git a/fluxer_api/src/api/user/controllers/UserController.ts b/fluxer_api/src/api/user/controllers/UserController.ts index e5fe2289e..07486f153 100644 --- a/fluxer_api/src/api/user/controllers/UserController.ts +++ b/fluxer_api/src/api/user/controllers/UserController.ts @@ -8,7 +8,6 @@ import {UserAuthController} from './UserAuthController'; import {UserChannelController} from './UserChannelController'; import {UserContentController} from './UserContentController'; import {UserRelationshipController} from './UserRelationshipController'; -import {UserScheduledMessageController} from './UserScheduledMessageController'; export function UserController(app: HonoApp) { UserAccountController(app); @@ -16,7 +15,6 @@ export function UserController(app: HonoApp) { UserRelationshipController(app); UserChannelController(app); UserContentController(app); - UserScheduledMessageController(app); EntranceSoundController(app); EntranceSoundPlayController(app); } diff --git a/fluxer_api/src/api/user/controllers/UserScheduledMessageController.ts b/fluxer_api/src/api/user/controllers/UserScheduledMessageController.ts deleted file mode 100644 index 3e80e1f16..000000000 --- a/fluxer_api/src/api/user/controllers/UserScheduledMessageController.ts +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {UnknownMessageError} from '@fluxer/errors/src/domains/channel/UnknownMessageError'; -import {ScheduledMessageIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas'; -import { - ScheduledMessageRequestSchema, - ScheduledMessageResponseSchema, -} from '@fluxer/schema/src/domains/message/ScheduledMessageSchemas'; -import type {Context} from 'hono'; -import {z} from 'zod'; -import {createMessageID} from '../../BrandedTypes'; -import {parseScheduledMessageInput} from '../../channel/controllers/ScheduledMessageParsing'; -import {DefaultUserOnly, LoginRequired} from '../../middleware/AuthMiddleware'; -import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware'; -import {OpenAPI} from '../../middleware/ResponseTypeMiddleware'; -import {RateLimitConfigs} from '../../RateLimitConfig'; -import type {HonoApp, HonoEnv} from '../../types/HonoEnv'; -import {Validator} from '../../Validator'; - -export function UserScheduledMessageController(app: HonoApp) { - app.get( - '/users/@me/scheduled-messages', - RateLimitMiddleware(RateLimitConfigs.USER_SAVED_MESSAGES_READ), - LoginRequired, - DefaultUserOnly, - OpenAPI({ - operationId: 'list_scheduled_messages', - summary: 'List scheduled messages', - responseSchema: z.array(ScheduledMessageResponseSchema), - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Users'], - description: - 'Retrieves all scheduled messages for the current user. Returns list of messages that are scheduled to be sent at a future date and time.', - }), - async (ctx: Context) => { - const userId = ctx.get('user').id; - const scheduledMessageService = ctx.get('scheduledMessageService'); - const scheduledMessages = await scheduledMessageService.listScheduledMessages(userId); - return ctx.json( - scheduledMessages.map((message) => message.toResponse()), - 200, - ); - }, - ); - app.get( - '/users/@me/scheduled-messages/:scheduled_message_id', - RateLimitMiddleware(RateLimitConfigs.USER_SAVED_MESSAGES_READ), - LoginRequired, - DefaultUserOnly, - Validator('param', ScheduledMessageIdParam), - OpenAPI({ - operationId: 'get_scheduled_message', - summary: 'Get scheduled message', - responseSchema: ScheduledMessageResponseSchema, - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Users'], - description: - 'Retrieves details of a specific scheduled message by ID. Returns the message content, scheduled send time, and status.', - }), - async (ctx) => { - const userId = ctx.get('user').id; - const scheduledMessageId = createMessageID(ctx.req.valid('param').scheduled_message_id); - const scheduledMessageService = ctx.get('scheduledMessageService'); - const scheduledMessage = await scheduledMessageService.getScheduledMessage(userId, scheduledMessageId); - if (!scheduledMessage) { - throw new UnknownMessageError(); - } - return ctx.json(scheduledMessage.toResponse(), 200); - }, - ); - app.delete( - '/users/@me/scheduled-messages/:scheduled_message_id', - RateLimitMiddleware(RateLimitConfigs.USER_SAVED_MESSAGES_WRITE), - LoginRequired, - DefaultUserOnly, - Validator('param', ScheduledMessageIdParam), - OpenAPI({ - operationId: 'cancel_scheduled_message', - summary: 'Cancel scheduled message', - responseSchema: null, - statusCode: 204, - security: ['bearerToken', 'sessionToken'], - tags: ['Users'], - description: - 'Cancels and deletes a scheduled message before it is sent. The message will not be delivered if cancelled.', - }), - async (ctx) => { - const userId = ctx.get('user').id; - const scheduledMessageId = createMessageID(ctx.req.valid('param').scheduled_message_id); - const scheduledMessageService = ctx.get('scheduledMessageService'); - await scheduledMessageService.cancelScheduledMessage(userId, scheduledMessageId); - return ctx.body(null, 204); - }, - ); - app.patch( - '/users/@me/scheduled-messages/:scheduled_message_id', - RateLimitMiddleware(RateLimitConfigs.USER_SAVED_MESSAGES_WRITE), - LoginRequired, - DefaultUserOnly, - Validator('param', ScheduledMessageIdParam), - OpenAPI({ - operationId: 'update_scheduled_message', - summary: 'Update scheduled message', - responseSchema: ScheduledMessageResponseSchema, - requestSchema: ScheduledMessageRequestSchema, - requestFormSchema: ScheduledMessageRequestSchema, - statusCode: 200, - security: ['bearerToken', 'sessionToken'], - tags: ['Users'], - description: - 'Updates an existing scheduled message before it is sent. Can modify message content, scheduled time, and timezone. Returns updated scheduled message details.', - }), - async (ctx) => { - const user = ctx.get('user'); - const scheduledMessageService = ctx.get('scheduledMessageService'); - const scheduledMessageId = createMessageID(ctx.req.valid('param').scheduled_message_id); - const existingMessage = await scheduledMessageService.getScheduledMessage(user.id, scheduledMessageId); - if (!existingMessage) { - throw new UnknownMessageError(); - } - const channelId = existingMessage.channelId; - const {message, scheduledLocalAt, timezone} = await parseScheduledMessageInput({ - ctx, - user, - channelId, - }); - const scheduledMessage = await scheduledMessageService.updateScheduledMessage({ - user, - channelId, - data: message, - scheduledLocalAt, - timezone, - scheduledMessageId, - existing: existingMessage, - }); - return ctx.json(scheduledMessage.toResponse(), 200); - }, - ); -} diff --git a/fluxer_api/src/api/user/repositories/ScheduledMessageRepository.ts b/fluxer_api/src/api/user/repositories/ScheduledMessageRepository.ts deleted file mode 100644 index c8001e111..000000000 --- a/fluxer_api/src/api/user/repositories/ScheduledMessageRepository.ts +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {MessageID, UserID} from '../../BrandedTypes'; -import {deleteOneOrMany, fetchMany, fetchOne, upsertOne} from '../../database/CassandraQueryExecution'; -import {Db} from '../../database/CassandraTypes'; -import type {ScheduledMessageRow} from '../../database/types/UserTypes'; -import {ScheduledMessage} from '../../models/ScheduledMessage'; -import {ScheduledMessages} from '../../Tables'; - -export class ScheduledMessageRepository { - private readonly fetchCql = ScheduledMessages.selectCql({ - where: [ScheduledMessages.where.eq('user_id')], - }); - - async listScheduledMessages(userId: UserID, limit: number = 25): Promise> { - const rows = await fetchMany(this.fetchCql, { - user_id: userId, - }); - const messages = rows.map((row) => ScheduledMessage.fromRow(row)); - return messages.sort((a, b) => (b.id > a.id ? 1 : a.id > b.id ? -1 : 0)).slice(0, limit); - } - - async getScheduledMessage(userId: UserID, scheduledMessageId: MessageID): Promise { - const row = await fetchOne( - ScheduledMessages.selectCql({ - where: [ScheduledMessages.where.eq('user_id'), ScheduledMessages.where.eq('scheduled_message_id')], - }), - { - user_id: userId, - scheduled_message_id: scheduledMessageId, - }, - ); - return row ? ScheduledMessage.fromRow(row) : null; - } - - async upsertScheduledMessage(message: ScheduledMessage, ttlSeconds: number): Promise { - await upsertOne(ScheduledMessages.upsertAllWithTtl(message.toRow(), ttlSeconds)); - } - - async deleteScheduledMessage(userId: UserID, scheduledMessageId: MessageID): Promise { - await deleteOneOrMany( - ScheduledMessages.deleteByPk({ - user_id: userId, - scheduled_message_id: scheduledMessageId, - }), - ); - } - - async markInvalid(userId: UserID, scheduledMessageId: MessageID, reason: string, ttlSeconds: number): Promise { - await upsertOne( - ScheduledMessages.patchByPkWithTtl( - { - user_id: userId, - scheduled_message_id: scheduledMessageId, - }, - { - status: Db.set('invalid'), - status_reason: Db.set(reason), - invalidated_at: Db.set(new Date()), - }, - ttlSeconds, - ), - ); - } -} diff --git a/fluxer_api/src/api/webhook/tests/WebhookPermissions.test.ts b/fluxer_api/src/api/webhook/tests/WebhookPermissions.test.ts index 807c88217..351fdf6d5 100644 --- a/fluxer_api/src/api/webhook/tests/WebhookPermissions.test.ts +++ b/fluxer_api/src/api/webhook/tests/WebhookPermissions.test.ts @@ -2,8 +2,7 @@ import {beforeAll, beforeEach, describe, it} from 'vitest'; import {createTestAccount} from '../../auth/tests/AuthTestUtils'; -import {joinGuild} from '../../channel/tests/ScheduledMessageTestUtils'; -import {createGuild} from '../../guild/tests/GuildTestUtils'; +import {acceptInvite, createGuild} from '../../guild/tests/GuildTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; import {HTTP_STATUS} from '../../test/TestConstants'; import {createBuilder} from '../../test/TestRequestBuilder'; @@ -23,7 +22,7 @@ describe('Webhook permissions', () => { const guild = await createGuild(harness, owner.token, `Webhook Security ${Date.now()}`); const channelId = guild.system_channel_id!; const invite = await createChannelInvite(harness, owner.token, channelId); - await joinGuild(harness, member.token, invite.code); + await acceptInvite(harness, member.token, invite.code); const webhook = await createWebhook(harness, channelId, owner.token, 'Test Webhook'); await createBuilder(harness, member.token) .post(`/channels/${channelId}/webhooks`) diff --git a/fluxer_api/src/api/worker/WorkerDependencies.ts b/fluxer_api/src/api/worker/WorkerDependencies.ts index 9e7163f6b..f30ca8caf 100644 --- a/fluxer_api/src/api/worker/WorkerDependencies.ts +++ b/fluxer_api/src/api/worker/WorkerDependencies.ts @@ -40,7 +40,6 @@ import type {IVoiceRoomStore} from '../infrastructure/IVoiceRoomStore'; import type {KVAccountDeletionQueueService} from '../infrastructure/KVAccountDeletionQueueService'; import type {KVActivityTracker} from '../infrastructure/KVActivityTracker'; import type {KVBulkMessageDeletionQueueService} from '../infrastructure/KVBulkMessageDeletionQueueService'; -import type {KVScheduledJobQueueService} from '../infrastructure/KVScheduledJobQueueService'; import type {PremiumStateReconciliationQueueService} from '../infrastructure/PremiumStateReconciliationQueueService'; import type {UserCacheService} from '../infrastructure/UserCacheService'; import type {InstanceConfigRepository} from '../instance/InstanceConfigRepository'; @@ -87,7 +86,6 @@ import { getKVAccountDeletionQueue, getKVActivityTracker, getKVBulkMessageDeletionQueue, - getKVScheduledJobQueue, getLimitConfigService, getNcmecSubmissionService, getOAuth2TokenRepository, @@ -164,7 +162,6 @@ export interface WorkerDependencies { activityTracker: KVActivityTracker; deletionQueueService: KVAccountDeletionQueueService; bulkMessageDeletionQueueService: KVBulkMessageDeletionQueueService; - scheduledJobQueueService: KVScheduledJobQueueService; premiumStateReconciliationQueueService: PremiumStateReconciliationQueueService; deletionEligibilityService: UserDeletionEligibilityService; voiceRoomStore: IVoiceRoomStore; @@ -229,7 +226,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS const activityTracker = getKVActivityTracker(); const deletionQueueService = getKVAccountDeletionQueue(); const bulkMessageDeletionQueueService = getKVBulkMessageDeletionQueue(); - const scheduledJobQueueService = getKVScheduledJobQueue(); const premiumStateReconciliationQueueService = getPremiumStateReconciliationQueueService(); const deletionEligibilityService = new UserDeletionEligibilityService(kvClient); await ensureVoiceResourcesInitialized(); @@ -341,7 +337,6 @@ export async function initializeWorkerDependencies(snowflakeService: ISnowflakeS activityTracker, deletionQueueService, bulkMessageDeletionQueueService, - scheduledJobQueueService, premiumStateReconciliationQueueService, deletionEligibilityService, voiceRoomStore, diff --git a/fluxer_api/src/api/worker/WorkerLaneConfig.ts b/fluxer_api/src/api/worker/WorkerLaneConfig.ts index a821f7394..9fc4f6f0c 100644 --- a/fluxer_api/src/api/worker/WorkerLaneConfig.ts +++ b/fluxer_api/src/api/worker/WorkerLaneConfig.ts @@ -32,7 +32,6 @@ const LANE_CONFIG = { consumerName: 'workers_lifecycle', tasks: [ 'processStripeWebhook', - 'sendScheduledMessage', 'sendSystemDm', 'userProcessPendingDeletion', 'userProcessPendingDeletions', @@ -72,7 +71,6 @@ const LANE_CONFIG = { 'processInactivityDeletions', 'processPendingBulkMessageDeletions', 'processPremiumStateReconciliationQueue', - 'processScheduledJobQueue', 'prunePostgresKvTtl', 'refreshSearchIndex', 'syncDiscoveryIndex', @@ -214,13 +212,6 @@ function resolveWorkerLanes(config: WorkerLaneRuntimeConfig): Array lane.ackWaitMs)); - return Math.max(1, Math.floor(minimumAckWaitMs / SCHEDULED_JOB_DRAIN_INTERVALS_PER_ACK_WAIT / 1000)); -} - function resolveCronSchedulerEnabled(mode: APIWorkerMode, configuredValue: boolean | undefined): boolean { if (configuredValue !== undefined) { return configuredValue; @@ -253,10 +244,4 @@ export function findLaneForTask(taskName: string): APIWorkerLaneName | null { } export type {WorkerLaneDefinition}; -export { - resolveCronSchedulerEnabled, - resolveScheduledJobDrainIntervalSeconds, - resolveWorkerLanes, - validateLaneCompleteness, - WORKER_LANES, -}; +export {resolveCronSchedulerEnabled, resolveWorkerLanes, validateLaneCompleteness, WORKER_LANES}; diff --git a/fluxer_api/src/api/worker/WorkerMain.ts b/fluxer_api/src/api/worker/WorkerMain.ts index 7250bcf2e..ebe015d97 100644 --- a/fluxer_api/src/api/worker/WorkerMain.ts +++ b/fluxer_api/src/api/worker/WorkerMain.ts @@ -25,7 +25,6 @@ import {clearWorkerDependencies, setWorkerDependencies} from './WorkerContext'; import {initializeWorkerDependencies, shutdownWorkerDependencies, type WorkerDependencies} from './WorkerDependencies'; import { resolveCronSchedulerEnabled, - resolveScheduledJobDrainIntervalSeconds, resolveWorkerLanes, validateLaneCompleteness, type WorkerLaneDefinition, @@ -54,13 +53,6 @@ function registerCronJobs(cron: CronScheduler): void { cron.upsert('processPremiumStateReconciliationQueue', 'processPremiumStateReconciliationQueue', {}, '0 * * * * *', { ledger: false, }); - cron.upsert( - 'processScheduledJobQueue', - 'processScheduledJobQueue', - {}, - `*/${resolveScheduledJobDrainIntervalSeconds()} * * * * *`, - {ledger: false}, - ); if (!Config.instance.selfHosted) { cron.upsert('processExpiredPremiumSweep', 'processExpiredPremiumSweep', {}, '0 0 * * * *', {ledger: false}); } @@ -249,7 +241,6 @@ export async function startWorkerMain(): Promise { const runner = new WorkerRunner({ tasks: laneTasks, queue, - scheduledJobQueue: dependencies.scheduledJobQueueService, consumerName: lane.consumerName, laneName: lane.name, ledger: jobLedger, diff --git a/fluxer_api/src/api/worker/WorkerRunner.ts b/fluxer_api/src/api/worker/WorkerRunner.ts index d48e838c4..33ee95964 100644 --- a/fluxer_api/src/api/worker/WorkerRunner.ts +++ b/fluxer_api/src/api/worker/WorkerRunner.ts @@ -4,7 +4,6 @@ import {randomUUID} from 'node:crypto'; import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService'; import {JobCancelledError, type WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask'; import type {ConsumerMessages, JsMsg} from 'nats'; -import {buildScheduledJobIdentity, type ParkedScheduledJob} from '../infrastructure/KVScheduledJobQueueService'; import type {IJobLedgerRepository} from '../jobs/IJobLedgerRepository'; import {Logger} from '../Logger'; import {getWorkerService} from '../middleware/ServiceRegistry'; @@ -42,14 +41,9 @@ interface WorkerRunnerQueue { publishToDlq(taskType: string, originalPayload: Record, meta: WorkerRunnerDlqMeta): Promise; } -interface WorkerRunnerScheduledJobQueue { - parkJob(job: ParkedScheduledJob, releaseAt: Date): Promise; -} - interface WorkerRunnerOptions { tasks: Record; queue: WorkerRunnerQueue; - scheduledJobQueue: WorkerRunnerScheduledJobQueue; consumerName: string; laneName: string; ledger: IJobLedgerRepository; @@ -62,7 +56,6 @@ interface WorkerRunnerOptions { export class WorkerRunner { private readonly tasks: Record; private readonly queue: WorkerRunnerQueue; - private readonly scheduledJobQueue: WorkerRunnerScheduledJobQueue; private readonly consumerName: string; private readonly laneName: string; private readonly workerId: string; @@ -78,7 +71,6 @@ export class WorkerRunner { constructor(options: WorkerRunnerOptions) { this.tasks = options.tasks; this.queue = options.queue; - this.scheduledJobQueue = options.scheduledJobQueue; this.consumerName = options.consumerName; this.laneName = options.laneName; this.workerId = options.workerId ?? `worker-${options.laneName}-${randomUUID()}`; @@ -201,37 +193,11 @@ export class WorkerRunner { if (Number.isFinite(runAtMs)) { const delayMs = runAtMs - Date.now(); if (delayMs > 0) { - const deliveryCount = msg.info.deliveryCount; - const shouldPark = delayMs > this.ackWaitMs || deliveryCount >= this.maxDeliver - 1; - if (shouldPark) { - const ledgerJobIdValue = ledgerJobId === null ? null : ledgerJobId.toString(); - try { - await this.scheduledJobQueue.parkJob( - { - jobIdentity: buildScheduledJobIdentity(taskType, jobPayload, runAtMs, ledgerJobIdValue), - taskType, - payload: jobPayload, - runAtMs, - ledgerJobId: ledgerJobIdValue, - }, - new Date(runAtMs - this.ackWaitMs), - ); - msg.ack(); - Logger.debug( - {taskType, seq: msg.seq, runAt, deliveryCount}, - 'Parked scheduled job in the due queue to free ack slot', - ); - } catch (error) { - Logger.error({taskType, seq: msg.seq, err: error}, 'Failed to park scheduled job, falling back to NAK'); - msg.nak(Math.min(delayMs, this.ackWaitMs - 5000)); - } - } else { - Logger.debug( - {taskType, seq: msg.seq, runAt, delayMs}, - 'Job scheduled for future execution, redelivering with delay', - ); - msg.nak(delayMs); - } + Logger.debug( + {taskType, seq: msg.seq, runAt, delayMs}, + 'Job scheduled for future execution, redelivering with delay', + ); + msg.nak(delayMs); return false; } } diff --git a/fluxer_api/src/api/worker/WorkerTaskRegistry.ts b/fluxer_api/src/api/worker/WorkerTaskRegistry.ts index cc875676a..c509d7f95 100644 --- a/fluxer_api/src/api/worker/WorkerTaskRegistry.ts +++ b/fluxer_api/src/api/worker/WorkerTaskRegistry.ts @@ -30,13 +30,11 @@ import processExpiredPremiumSweep from './tasks/ProcessExpiredPremiumSweep'; import processInactivityDeletions from './tasks/ProcessInactivityDeletions'; import processPendingBulkMessageDeletions from './tasks/ProcessPendingBulkMessageDeletions'; import processPremiumStateReconciliationQueue from './tasks/ProcessPremiumStateReconciliationQueue'; -import processScheduledJobQueue from './tasks/ProcessScheduledJobQueue'; import processStripeWebhook from './tasks/ProcessStripeWebhook'; import prunePostgresKvTtl from './tasks/PrunePostgresKvTtl'; import reconcileUserPayments from './tasks/ReconcileUserPayments'; import refreshSearchIndex from './tasks/RefreshSearchIndex'; import revalidateUserConnections from './tasks/RevalidateUserConnections'; -import {sendScheduledMessage} from './tasks/SendScheduledMessage'; import {sendSystemDm} from './tasks/SendSystemDm'; import syncDiscoveryIndex from './tasks/SyncDiscoveryIndex'; import syncDisposableEmailDomains from './tasks/SyncDisposableEmailDomains'; @@ -76,12 +74,10 @@ export const workerTasks: Record = { processInactivityDeletions, processPendingBulkMessageDeletions, processPremiumStateReconciliationQueue, - processScheduledJobQueue, reconcileUserPayments, prunePostgresKvTtl, refreshSearchIndex, revalidateUserConnections, - sendScheduledMessage, sendSystemDm, syncFileShaBlocklists, syncUrlBlocklists, diff --git a/fluxer_api/src/api/worker/executors/ScheduledMessageExecutor.ts b/fluxer_api/src/api/worker/executors/ScheduledMessageExecutor.ts deleted file mode 100644 index a89fdcbeb..000000000 --- a/fluxer_api/src/api/worker/executors/ScheduledMessageExecutor.ts +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {MessageID, UserID} from '../../BrandedTypes'; -import {createMessageID, createUserID} from '../../BrandedTypes'; -import {SCHEDULED_MESSAGE_TTL_SECONDS} from '../../channel/services/ScheduledMessageService'; -import {createRequestCache} from '../../middleware/RequestCacheMiddleware'; -import {ScheduledMessageRepository} from '../../user/repositories/ScheduledMessageRepository'; -import type {WorkerDependencies} from '../WorkerDependencies'; - -interface WorkerLogger { - debug(message: string, extra?: object): void; - info(message: string, extra?: object): void; - warn(message: string, extra?: object): void; - error(message: string, extra?: object): void; -} - -export interface SendScheduledMessageParams { - userId: string; - scheduledMessageId: string; - expectedScheduledAt: string; -} - -export class ScheduledMessageExecutor { - constructor( - private readonly deps: WorkerDependencies, - private readonly logger: WorkerLogger, - private readonly scheduledMessageRepository: ScheduledMessageRepository = new ScheduledMessageRepository(), - ) {} - - async execute(params: SendScheduledMessageParams): Promise { - const userId = this.parseUserID(params.userId); - const scheduledMessageId = this.parseMessageID(params.scheduledMessageId); - if (!userId || !scheduledMessageId) { - this.logger.warn('Malformed scheduled message job payload', {payload: params}); - return; - } - const expectedScheduledAt = this.parseScheduledAt(params.expectedScheduledAt); - if (!expectedScheduledAt) { - this.logger.warn('Invalid expectedScheduledAt for scheduled message job', {payload: params}); - return; - } - const scheduledMessage = await this.scheduledMessageRepository.getScheduledMessage(userId, scheduledMessageId); - if (!scheduledMessage) { - this.logger.info('Scheduled message not found, skipping', {userId, scheduledMessageId}); - return; - } - if (scheduledMessage.status !== 'pending') { - this.logger.info('Scheduled message already processed', { - scheduledMessageId, - status: scheduledMessage.status, - }); - return; - } - if (scheduledMessage.scheduledAt.toISOString() !== expectedScheduledAt.toISOString()) { - this.logger.info('Scheduled message time mismatch, skipping stale job', { - scheduledMessageId, - expected: expectedScheduledAt.toISOString(), - actual: scheduledMessage.scheduledAt.toISOString(), - }); - return; - } - const user = await this.deps.userRepository.findUnique(userId); - if (!user) { - await this.markInvalid(userId, scheduledMessageId, 'User not found'); - return; - } - const messageRequest = scheduledMessage.parseToMessageRequest(); - const requestCache = createRequestCache(); - try { - await this.deps.channelService.messages.send.validateMessageCanBeSent({ - user, - channelId: scheduledMessage.channelId, - data: messageRequest, - }); - await this.deps.channelService.messages.send.sendMessage({ - user, - channelId: scheduledMessage.channelId, - data: messageRequest, - requestCache, - }); - await this.scheduledMessageRepository.deleteScheduledMessage(userId, scheduledMessageId); - this.logger.info('Scheduled message sent successfully', {scheduledMessageId, userId}); - } catch (error) { - const reason = error instanceof Error ? error.message : 'Failed to send scheduled message'; - this.logger.warn('Marking scheduled message invalid', {scheduledMessageId, userId, reason}); - await this.markInvalid(userId, scheduledMessageId, reason); - } finally { - requestCache.clear(); - } - } - - private async markInvalid(userId: UserID, scheduledMessageId: MessageID, reason: string): Promise { - try { - await this.scheduledMessageRepository.markInvalid( - userId, - scheduledMessageId, - reason, - SCHEDULED_MESSAGE_TTL_SECONDS, - ); - } catch (error) { - this.logger.error('Failed to mark scheduled message invalid', {error, scheduledMessageId}); - } - } - - private parseUserID(value: string): UserID | null { - try { - return createUserID(BigInt(value)); - } catch { - return null; - } - } - - private parseMessageID(value: string): MessageID | null { - try { - return createMessageID(BigInt(value)); - } catch { - return null; - } - } - - private parseScheduledAt(value: string): Date | null { - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; - } -} diff --git a/fluxer_api/src/api/worker/tasks/ProcessScheduledJobQueue.ts b/fluxer_api/src/api/worker/tasks/ProcessScheduledJobQueue.ts deleted file mode 100644 index c7716e137..000000000 --- a/fluxer_api/src/api/worker/tasks/ProcessScheduledJobQueue.ts +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {WorkerTaskHandler} from '@pkgs/worker/src/contracts/WorkerTask'; -import type {WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes'; -import type {ParkedScheduledJob} from '../../infrastructure/KVScheduledJobQueueService'; -import {Logger} from '../../Logger'; -import {getWorkerDependencies} from '../WorkerContext'; -import {findLaneForTask, type WorkerTaskName} from '../WorkerLaneConfig'; - -const MAX_JOBS_PER_RUN = 500; - -function buildReleasePayload(job: ParkedScheduledJob): WorkerJobPayload { - if (job.ledgerJobId === null) { - return job.payload; - } - return {...job.payload, __jobId: job.ledgerJobId}; -} - -const processScheduledJobQueue: WorkerTaskHandler = async (_payload, helpers) => { - const {scheduledJobQueueService, workerService} = getWorkerDependencies(); - const nowMs = Date.now(); - const readyJobs = await scheduledJobQueueService.getReadyJobs(nowMs, MAX_JOBS_PER_RUN); - if (readyJobs.length === 0) { - helpers.logger.debug('No parked scheduled jobs are due'); - return; - } - let releasedCount = 0; - let claimedElsewhereCount = 0; - let droppedCount = 0; - let failedCount = 0; - for (const {job, member} of readyJobs) { - try { - const claimed = await scheduledJobQueueService.claimJob(job.jobIdentity, member); - if (!claimed) { - claimedElsewhereCount += 1; - continue; - } - if (findLaneForTask(job.taskType) === null) { - droppedCount += 1; - Logger.error( - {jobIdentity: job.jobIdentity, taskType: job.taskType}, - 'Dropping parked scheduled job with an unknown task type', - ); - continue; - } - await workerService.addJob(job.taskType as WorkerTaskName, buildReleasePayload(job), { - runAt: new Date(job.runAtMs), - jobKey: job.jobIdentity, - skipLedger: true, - }); - releasedCount += 1; - } catch (error) { - failedCount += 1; - Logger.error( - {error, jobIdentity: job.jobIdentity, taskType: job.taskType}, - 'Failed to release parked scheduled job, re-parking for the next drain', - ); - try { - await scheduledJobQueueService.parkJob(job, new Date(nowMs)); - } catch (parkError) { - Logger.error( - {error: parkError, jobIdentity: job.jobIdentity, taskType: job.taskType}, - 'Failed to re-park scheduled job after a release failure', - ); - } - } - } - helpers.logger.info( - { - total: readyJobs.length, - releasedCount, - claimedElsewhereCount, - droppedCount, - failedCount, - }, - 'Released parked scheduled jobs', - ); -}; - -export default processScheduledJobQueue; diff --git a/fluxer_api/src/api/worker/tasks/SendScheduledMessage.ts b/fluxer_api/src/api/worker/tasks/SendScheduledMessage.ts deleted file mode 100644 index 5c692bfc1..000000000 --- a/fluxer_api/src/api/worker/tasks/SendScheduledMessage.ts +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {WorkerTaskHelpers} from '@pkgs/worker/src/contracts/WorkerTask'; -import {z} from 'zod'; -import {ScheduledMessageExecutor, type SendScheduledMessageParams} from '../executors/ScheduledMessageExecutor'; -import {getWorkerDependencies} from '../WorkerContext'; - -const PayloadSchema = z.object({ - userId: z.string(), - scheduledMessageId: z.string(), - expectedScheduledAt: z.string(), -}); - -export async function sendScheduledMessage(payload: unknown, helpers: WorkerTaskHelpers): Promise { - const validated = PayloadSchema.parse(payload) as SendScheduledMessageParams; - const deps = getWorkerDependencies(); - const executor = new ScheduledMessageExecutor(deps, helpers.logger); - const result = await executor.execute(validated); - return result; -} diff --git a/fluxer_api/src/api/worker/tests/ScheduledJobQueue.test.ts b/fluxer_api/src/api/worker/tests/ScheduledJobQueue.test.ts deleted file mode 100644 index 75873235a..000000000 --- a/fluxer_api/src/api/worker/tests/ScheduledJobQueue.test.ts +++ /dev/null @@ -1,343 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {IWorkerService} from '@pkgs/worker/src/contracts/IWorkerService'; -import type {WorkerTaskHelpers} from '@pkgs/worker/src/contracts/WorkerTask'; -import type {WorkerJobOptions, WorkerJobPayload} from '@pkgs/worker/src/contracts/WorkerTypes'; -import type {JsMsg} from 'nats'; -import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest'; -import {KVScheduledJobQueueService} from '../../infrastructure/KVScheduledJobQueueService'; -import type {IJobLedgerRepository} from '../../jobs/IJobLedgerRepository'; -import {setInjectedWorkerService} from '../../middleware/ServiceRegistry'; -import {MockKVProvider} from '../../test/mocks/MockKVProvider'; -import {NoopLogger} from '../../test/mocks/NoopLogger'; -import {NoopWorkerService} from '../../test/NoopWorkerService'; -import processScheduledJobQueue from '../tasks/ProcessScheduledJobQueue'; -import {setWorkerDependenciesForTest} from '../WorkerContext'; -import {WorkerRunner} from '../WorkerRunner'; - -const LANE_ACK_WAIT_MS = 60000; -const LANE_MAX_DELIVER = 25; -const TASK_TYPE = 'sendScheduledMessage'; - -interface RecordedJob { - taskType: string; - payload: WorkerJobPayload; - options: WorkerJobOptions | undefined; -} - -class RecordingWorkerService implements IWorkerService { - readonly jobs: Array = []; - - async addJob( - taskType: string, - payload: TPayload, - options?: WorkerJobOptions, - ): Promise { - this.jobs.push({taskType, payload, options}); - return BigInt(this.jobs.length); - } - - async cancelJob(_jobId: bigint): Promise { - return false; - } - - async retryDeadLetterJob(_jobId: bigint): Promise { - return false; - } -} - -const queueStub = { - getConnectionManager: () => { - throw new Error('WorkerRunner tests never consume messages'); - }, - getStreamName: () => 'JOBS', - enqueue: vi.fn(), - publishToDlq: vi.fn(), -}; - -class ConcurrentlyClaimedKVProvider extends MockKVProvider { - override async removeBulkDeletion(queueKey: string, secondaryKey: string): Promise { - await super.removeBulkDeletion(queueKey, secondaryKey); - return false; - } -} - -class TestWorkerRunner extends WorkerRunner { - async runJob(taskType: string, msg: JsMsg): Promise { - return await this.processJob(taskType, msg); - } -} - -function createRunner(scheduledJobQueue: KVScheduledJobQueueService): TestWorkerRunner { - return new TestWorkerRunner({ - tasks: {[TASK_TYPE]: async () => {}}, - queue: queueStub, - scheduledJobQueue, - consumerName: 'workers_lifecycle', - laneName: 'lifecycle', - ledger: {} as IJobLedgerRepository, - concurrency: 8, - maxDeliver: LANE_MAX_DELIVER, - ackWaitMs: LANE_ACK_WAIT_MS, - }); -} - -function createJobMessage(options: { - runAt: Date; - payload: Record; - seq?: number; - deliveryCount?: number; -}) { - const envelope = { - payload: options.payload, - run_at: options.runAt.toISOString(), - max_attempts: 5, - priority: 0, - created_at: new Date().toISOString(), - }; - return { - seq: options.seq ?? 1, - subject: `jobs.${TASK_TYPE}`, - redelivered: (options.deliveryCount ?? 1) > 1, - data: new TextEncoder().encode(JSON.stringify(envelope)), - info: {deliveryCount: options.deliveryCount ?? 1}, - ack: vi.fn(), - nak: vi.fn(), - term: vi.fn(), - }; -} - -function createHelpers(): WorkerTaskHelpers { - return { - logger: new NoopLogger(), - jobId: 0n, - addJob: async () => 0n, - reportProgress: async () => {}, - shouldCancel: async () => false, - setContextLink: async () => {}, - }; -} - -async function drain(scheduledJobQueue: KVScheduledJobQueueService, workerService: RecordingWorkerService) { - setWorkerDependenciesForTest({scheduledJobQueueService: scheduledJobQueue, workerService}); - await processScheduledJobQueue({}, createHelpers()); -} - -describe('Scheduled job due queue', () => { - beforeAll(() => { - setInjectedWorkerService(new NoopWorkerService()); - }); - - afterEach(() => { - vi.useRealTimers(); - queueStub.enqueue.mockClear(); - queueStub.publishToDlq.mockClear(); - }); - - it('naks a job due inside one ack-wait instead of parking it', async () => { - const scheduledJobQueue = new KVScheduledJobQueueService(new MockKVProvider()); - const runner = createRunner(scheduledJobQueue); - const msg = createJobMessage({ - runAt: new Date(Date.now() + LANE_ACK_WAIT_MS - 30000), - payload: {scheduledMessageId: '77', __jobId: '4242'}, - }); - - await runner.runJob(TASK_TYPE, msg as unknown as JsMsg); - - expect(msg.nak).toHaveBeenCalledTimes(1); - expect(msg.ack).not.toHaveBeenCalled(); - const delayMs = msg.nak.mock.calls[0]![0] as number; - expect(delayMs).toBeGreaterThan(0); - expect(delayMs).toBeLessThanOrEqual(LANE_ACK_WAIT_MS - 30000); - expect(await scheduledJobQueue.getQueueSize()).toBe(0); - }); - - it('parks a job due beyond one ack-wait without republishing it', async () => { - const scheduledJobQueue = new KVScheduledJobQueueService(new MockKVProvider()); - const runner = createRunner(scheduledJobQueue); - const runAt = new Date(Date.now() + 60 * 60 * 1000); - const msg = createJobMessage({runAt, payload: {scheduledMessageId: '77', __jobId: '4242'}}); - - await runner.runJob(TASK_TYPE, msg as unknown as JsMsg); - - expect(msg.ack).toHaveBeenCalledTimes(1); - expect(msg.nak).not.toHaveBeenCalled(); - expect(queueStub.enqueue).not.toHaveBeenCalled(); - expect(await scheduledJobQueue.getQueueSize()).toBe(1); - expect(await scheduledJobQueue.getReadyJobs(Date.now(), 10)).toHaveLength(0); - const due = await scheduledJobQueue.getReadyJobs(runAt.getTime() - LANE_ACK_WAIT_MS, 10); - expect(due).toHaveLength(1); - expect(due[0]!.job).toEqual({ - jobIdentity: '4242', - taskType: TASK_TYPE, - payload: {scheduledMessageId: '77'}, - runAtMs: runAt.getTime(), - ledgerJobId: '4242', - }); - expect(due[0]!.member).toContain('"jobIdentity":"4242"'); - }); - - it('releases a parked job exactly once when its due time arrives', async () => { - vi.useFakeTimers({toFake: ['Date']}); - const start = new Date('2026-08-30T12:00:00.000Z'); - vi.setSystemTime(start); - const scheduledJobQueue = new KVScheduledJobQueueService(new MockKVProvider()); - const runner = createRunner(scheduledJobQueue); - const runAt = new Date(start.getTime() + 60 * 60 * 1000); - const msg = createJobMessage({runAt, payload: {scheduledMessageId: '77', __jobId: '4242'}}); - await runner.runJob(TASK_TYPE, msg as unknown as JsMsg); - const workerService = new RecordingWorkerService(); - - await drain(scheduledJobQueue, workerService); - expect(workerService.jobs).toHaveLength(0); - - vi.setSystemTime(new Date(runAt.getTime() - LANE_ACK_WAIT_MS + 1000)); - await drain(scheduledJobQueue, workerService); - - expect(workerService.jobs).toHaveLength(1); - expect(workerService.jobs[0]!.taskType).toBe(TASK_TYPE); - expect(workerService.jobs[0]!.payload).toEqual({scheduledMessageId: '77', __jobId: '4242'}); - expect(workerService.jobs[0]!.options?.runAt?.getTime()).toBe(runAt.getTime()); - expect(workerService.jobs[0]!.options?.jobKey).toBe('4242'); - expect(workerService.jobs[0]!.options?.skipLedger).toBe(true); - expect(await scheduledJobQueue.getQueueSize()).toBe(0); - - await drain(scheduledJobQueue, workerService); - expect(workerService.jobs).toHaveLength(1); - }); - - it('runs a job parked twice only once', async () => { - vi.useFakeTimers({toFake: ['Date']}); - const start = new Date('2026-08-30T12:00:00.000Z'); - vi.setSystemTime(start); - const scheduledJobQueue = new KVScheduledJobQueueService(new MockKVProvider()); - const runner = createRunner(scheduledJobQueue); - const runAt = new Date(start.getTime() + 60 * 60 * 1000); - const payload = {scheduledMessageId: '77', __jobId: '4242'}; - - await runner.runJob(TASK_TYPE, createJobMessage({runAt, payload, seq: 1}) as unknown as JsMsg); - await runner.runJob(TASK_TYPE, createJobMessage({runAt, payload, seq: 2, deliveryCount: 2}) as unknown as JsMsg); - - expect(await scheduledJobQueue.getQueueSize()).toBe(1); - - vi.setSystemTime(new Date(runAt.getTime() - LANE_ACK_WAIT_MS + 1000)); - const workerService = new RecordingWorkerService(); - await drain(scheduledJobQueue, workerService); - - expect(workerService.jobs).toHaveLength(1); - expect(await scheduledJobQueue.getQueueSize()).toBe(0); - }); - - it('parks a nearly delivery-exhausted job so it gets a fresh delivery budget', async () => { - const scheduledJobQueue = new KVScheduledJobQueueService(new MockKVProvider()); - const runner = createRunner(scheduledJobQueue); - const runAt = new Date(Date.now() + 20000); - const msg = createJobMessage({ - runAt, - payload: {scheduledMessageId: '77', __jobId: '4242'}, - deliveryCount: LANE_MAX_DELIVER - 1, - }); - - await runner.runJob(TASK_TYPE, msg as unknown as JsMsg); - - expect(msg.ack).toHaveBeenCalledTimes(1); - expect(msg.nak).not.toHaveBeenCalled(); - expect(queueStub.enqueue).not.toHaveBeenCalled(); - const workerService = new RecordingWorkerService(); - await drain(scheduledJobQueue, workerService); - expect(workerService.jobs).toHaveLength(1); - expect(workerService.jobs[0]!.options?.runAt?.getTime()).toBe(runAt.getTime()); - }); - - it('does nothing when the due queue is empty', async () => { - const scheduledJobQueue = new KVScheduledJobQueueService(new MockKVProvider()); - const workerService = new RecordingWorkerService(); - - await expect(drain(scheduledJobQueue, workerService)).resolves.toBeUndefined(); - - expect(workerService.jobs).toHaveLength(0); - expect(await scheduledJobQueue.getQueueSize()).toBe(0); - }); - - it('does not release a job another drain claimed first', async () => { - const scheduledJobQueue = new KVScheduledJobQueueService(new ConcurrentlyClaimedKVProvider()); - const runner = createRunner(scheduledJobQueue); - const runAt = new Date(Date.now() + 20000); - const msg = createJobMessage({ - runAt, - payload: {scheduledMessageId: '77', __jobId: '4242'}, - deliveryCount: LANE_MAX_DELIVER - 1, - }); - await runner.runJob(TASK_TYPE, msg as unknown as JsMsg); - expect(await scheduledJobQueue.getReadyJobs(Date.now(), 10)).toHaveLength(1); - - const workerService = new RecordingWorkerService(); - await drain(scheduledJobQueue, workerService); - - expect(workerService.jobs).toHaveLength(0); - expect(await scheduledJobQueue.getQueueSize()).toBe(0); - }); -}); - -describe('Parked job claim survives secondary key loss', () => { - it('claims by member when the secondary key was evicted', async () => { - const kv = new MockKVProvider(); - const service = new KVScheduledJobQueueService(kv); - const runAtMs = Date.now() + 86_400_000; - const job = { - jobIdentity: 'job-evicted', - taskType: TASK_TYPE, - payload: {message_id: '1'} as WorkerJobPayload, - runAtMs, - ledgerJobId: null, - }; - - await service.parkJob(job, new Date(Date.now() - 1000)); - expect(await service.getQueueSize()).toBe(1); - - await kv.del('scheduled_job_queue:job-evicted'); - - const ready = await service.getReadyJobs(Date.now(), 10); - expect(ready).toHaveLength(1); - - expect(await service.claimJob(ready[0]!.job.jobIdentity, ready[0]!.member)).toBe(true); - expect(await service.getQueueSize()).toBe(0); - }); - - it('leaves the entry wedged when claiming without the member', async () => { - const kv = new MockKVProvider(); - const service = new KVScheduledJobQueueService(kv); - const job = { - jobIdentity: 'job-wedged', - taskType: TASK_TYPE, - payload: {message_id: '2'} as WorkerJobPayload, - runAtMs: Date.now() + 86_400_000, - ledgerJobId: null, - }; - - await service.parkJob(job, new Date(Date.now() - 1000)); - await kv.del('scheduled_job_queue:job-wedged'); - - expect(await service.claimJob('job-wedged')).toBe(false); - expect(await service.getQueueSize()).toBe(1); - }); - - it('still reports a second claim of the same member as already claimed', async () => { - const kv = new MockKVProvider(); - const service = new KVScheduledJobQueueService(kv); - const job = { - jobIdentity: 'job-twice', - taskType: TASK_TYPE, - payload: {message_id: '3'} as WorkerJobPayload, - runAtMs: Date.now() + 86_400_000, - ledgerJobId: null, - }; - - await service.parkJob(job, new Date(Date.now() - 1000)); - const ready = await service.getReadyJobs(Date.now(), 10); - const member = ready[0]!.member; - - expect(await service.claimJob('job-twice', member)).toBe(true); - expect(await service.claimJob('job-twice', member)).toBe(false); - }); -}); diff --git a/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts b/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts index 29ab7ba18..92427456a 100644 --- a/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerAckHeartbeat.test.ts @@ -3,10 +3,8 @@ import {JobCancelledError} from '@pkgs/worker/src/contracts/WorkerTask'; import type {JsMsg} from 'nats'; import {afterEach, beforeAll, describe, expect, it, vi} from 'vitest'; -import {KVScheduledJobQueueService} from '../../infrastructure/KVScheduledJobQueueService'; import type {IJobLedgerRepository} from '../../jobs/IJobLedgerRepository'; import {setInjectedWorkerService} from '../../middleware/ServiceRegistry'; -import {MockKVProvider} from '../../test/mocks/MockKVProvider'; import {NoopWorkerService} from '../../test/NoopWorkerService'; import {WorkerRunner} from '../WorkerRunner'; @@ -32,7 +30,6 @@ function createRunner(task: () => Promise): TestWorkerRunner { return new TestWorkerRunner({ tasks: {[TASK_TYPE]: task}, queue: queueStub, - scheduledJobQueue: new KVScheduledJobQueueService(new MockKVProvider()), consumerName: 'workers_batch', laneName: 'batch', ledger: {} as IJobLedgerRepository, @@ -146,12 +143,10 @@ describe('Worker ack heartbeat', () => { expect(msg.working.mock.calls.length).toBe(heartbeatsAtCompletion); }); - it('does not heartbeat a parked scheduled job', async () => { - const scheduledJobQueue = new KVScheduledJobQueueService(new MockKVProvider()); + it('does not heartbeat a job deferred to the future', async () => { const runner = new TestWorkerRunner({ tasks: {[TASK_TYPE]: async () => {}}, queue: queueStub, - scheduledJobQueue, consumerName: 'workers_batch', laneName: 'batch', ledger: {} as IJobLedgerRepository, @@ -174,8 +169,8 @@ describe('Worker ack heartbeat', () => { await runner.runJob(TASK_TYPE, msg as unknown as JsMsg); - expect(msg.ack).toHaveBeenCalledTimes(1); + expect(msg.ack).not.toHaveBeenCalled(); + expect(msg.nak).toHaveBeenCalledTimes(1); expect(msg.working).not.toHaveBeenCalled(); - expect(await scheduledJobQueue.getQueueSize()).toBe(1); }); }); diff --git a/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts b/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts index 71dd5a663..661912944 100644 --- a/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts +++ b/fluxer_api/src/api/worker/tests/WorkerLaneConfig.test.ts @@ -1,12 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {describe, expect, it} from 'vitest'; -import { - resolveCronSchedulerEnabled, - resolveScheduledJobDrainIntervalSeconds, - resolveWorkerLanes, - WORKER_LANES, -} from '../WorkerLaneConfig'; +import {resolveCronSchedulerEnabled, resolveWorkerLanes, WORKER_LANES} from '../WorkerLaneConfig'; describe('WorkerLaneConfig', () => { it('returns all lanes in all_lanes mode', () => { @@ -102,10 +97,4 @@ describe('WorkerLaneConfig', () => { expect(resolveCronSchedulerEnabled('single_lane', true)).toBe(true); expect(resolveCronSchedulerEnabled('all_lanes', false)).toBe(false); }); - it('drains parked scheduled jobs well inside the shortest lane ack-wait', () => { - const intervalSeconds = resolveScheduledJobDrainIntervalSeconds(); - const shortestAckWaitMs = Math.min(...WORKER_LANES.map((lane) => lane.ackWaitMs)); - expect(intervalSeconds).toBeGreaterThanOrEqual(1); - expect(intervalSeconds * 1000 * 2).toBeLessThanOrEqual(shortestAckWaitMs); - }); }); diff --git a/fluxer_app/src/features/channel/components/ScheduledMessageEditBar.module.css b/fluxer_app/src/features/channel/components/ScheduledMessageEditBar.module.css deleted file mode 100644 index 82f5a6334..000000000 --- a/fluxer_app/src/features/channel/components/ScheduledMessageEditBar.module.css +++ /dev/null @@ -1,46 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-or-later */ - -.text { - display: flex; - flex-direction: column; - gap: 0.125rem; - overflow: hidden; -} - -.label { - display: flex; - align-items: center; - gap: 0.375rem; - font-size: 0.875rem; - color: var(--text-primary); -} - -.timestamp { - font-size: 0.75rem; - color: var(--text-primary-muted); -} - -.controls { - display: flex; - align-items: center; -} - -.button { - cursor: pointer; - flex-shrink: 0; - border: none; - background-color: transparent; - padding: 0.5rem 0 0.5rem 1rem; - color: var(--text-primary-muted); - line-height: 0; - transition: color 200ms; -} - -.button:hover { - color: var(--text-primary); -} - -.icon { - height: 1.25rem; - width: 1.25rem; -} diff --git a/fluxer_app/src/features/channel/components/ScheduledMessageEditBar.tsx b/fluxer_app/src/features/channel/components/ScheduledMessageEditBar.tsx deleted file mode 100644 index 1ea9de114..000000000 --- a/fluxer_app/src/features/channel/components/ScheduledMessageEditBar.tsx +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import styles from '@app/features/channel/components/ScheduledMessageEditBar.module.css'; -import wrapperStyles from '@app/features/channel/components/textarea/InputWrapper.module.css'; -import FocusRing from '@app/features/ui/focus_ring/FocusRing'; -import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; -import {getFormattedDateTimeInZone} from '@fluxer/date_utils/src/DateFormatting'; -import {msg} from '@lingui/core/macro'; -import {useLingui} from '@lingui/react/macro'; -import {ClockIcon, XCircleIcon} from '@phosphor-icons/react'; -import {observer} from 'mobx-react-lite'; -import {useCallback, useMemo} from 'react'; - -const EDITING_SCHEDULED_MESSAGE_DESCRIPTOR = msg({ - message: 'Editing scheduled message', - comment: 'Button or menu action label in the channel and chat scheduled message edit bar. Keep it concise.', -}); -const CANCEL_EDITING_SCHEDULED_MESSAGE_DESCRIPTOR = msg({ - message: 'Cancel editing scheduled message', - comment: 'Button or menu action label in the channel and chat scheduled message edit bar. Keep it concise.', -}); - -interface ScheduledMessageEditBarProps { - scheduledLocalAt: string; - timezone: string; - onCancel: () => void; -} - -const formatScheduleLabel = (local: string, timezone: string): string => { - const locale = getCurrentLocale(); - const formatted = getFormattedDateTimeInZone(local, timezone, locale); - return `${formatted} (${timezone})`; -}; -export const ScheduledMessageEditBar = observer( - ({scheduledLocalAt, timezone, onCancel}: ScheduledMessageEditBarProps) => { - const {i18n} = useLingui(); - const scheduleLabel = useMemo(() => formatScheduleLabel(scheduledLocalAt, timezone), [scheduledLocalAt, timezone]); - const handleStopEditing = useCallback(() => { - onCancel(); - }, [onCancel]); - return ( -
-
-
-
- - - {i18n._(EDITING_SCHEDULED_MESSAGE_DESCRIPTOR)} - -
-
- {scheduleLabel} -
-
-
- - - -
-
-
-
- ); - }, -); diff --git a/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.test.ts b/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.test.ts index ef5786e3a..45223491f 100644 --- a/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.test.ts +++ b/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.test.ts @@ -17,7 +17,6 @@ function signals(overrides: Partial = {}): MobileTe hasContent: false, hasAttachments: false, hasPendingSticker: false, - isEditingScheduledMessage: false, ...overrides, }; } @@ -93,15 +92,6 @@ describe('MobileTextareaButtonStateMachine', () => { }); }); - it('does not expose voice recording while editing a scheduled message', () => { - expect(state({isEditingScheduledMessage: true})).toMatchObject({ - mode: 'sendBlocked', - visibleButton: 'send', - sendButton: {disabled: true}, - voiceButton: {disabled: true}, - }); - }); - it('falls back to a disabled send button when voice recording is unavailable and the composer is empty', () => { expect(state({canRecordVoice: false})).toMatchObject({ mode: 'sendBlocked', diff --git a/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.ts b/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.ts index 0bf65553e..8d2342274 100644 --- a/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.ts +++ b/fluxer_app/src/features/channel/components/textarea/MobileTextareaButtonStateMachine.ts @@ -16,7 +16,6 @@ export interface MobileTextareaButtonSignals { hasContent: boolean; hasAttachments: boolean; hasPendingSticker: boolean; - isEditingScheduledMessage: boolean; } export interface MobileTextareaButtonModel { @@ -51,16 +50,13 @@ export function selectMobileTextareaButtonModel(signals: MobileTextareaButtonSig const typedText = hasTypedText(signals.value); const hasTextContent = signals.hasContent || typedText; const hasSubmissionContent = hasTextContent || signals.hasAttachments || signals.hasPendingSticker; - const shouldShowVoice = - signals.canRecordVoice && !signals.isEditingMessage && !signals.isEditingScheduledMessage && !hasSubmissionContent; + const shouldShowVoice = signals.canRecordVoice && !signals.isEditingMessage && !hasSubmissionContent; const sendDisabled = signals.disabled || signals.isOverCharacterLimit || - signals.isEditingScheduledMessage || (signals.isSlowmodeActive && !signals.isEditingMessage) || (!hasSubmissionContent && !signals.isEditingMessage); - const voiceDisabled = - signals.disabled || signals.isSlowmodeActive || signals.isOverCharacterLimit || signals.isEditingScheduledMessage; + const voiceDisabled = signals.disabled || signals.isSlowmodeActive || signals.isOverCharacterLimit; const visibleButton = shouldShowVoice ? 'voice' : 'send'; const mode = visibleButton === 'voice' ? 'voice' : sendDisabled ? 'sendBlocked' : 'sendReady'; return { @@ -84,7 +80,6 @@ const DEFAULT_SIGNALS: MobileTextareaButtonSignals = { hasContent: false, hasAttachments: false, hasPendingSticker: false, - isEditingScheduledMessage: false, }; const DEFAULT_MODEL = selectMobileTextareaButtonModel(DEFAULT_SIGNALS); diff --git a/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx b/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx index 4969fa41f..7b05e57df 100644 --- a/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx +++ b/fluxer_app/src/features/channel/components/textarea/TextareaButtons.tsx @@ -106,7 +106,6 @@ export const TextareaButtons = React.forwardRef; - tts?: boolean; - hasAttachments: boolean; -} - -interface UpdateScheduledMessageParams { - channelId: string; - scheduledMessageId: string; - scheduledLocalAt: string; - timezone: string; - normalized: NormalizedMessageContent; - payload: ScheduledMessagePayload; - replyMentioning?: boolean; -} - -interface PreparedScheduledMessage { - nonce: string; - payload: ScheduledMessageRequest; - files?: Array; - normalized: NormalizedMessageContent; -} - -const formatScheduledLabel = (local: string, timezone: string): string => { - return `${local.replace('T', ' ')} (${timezone})`; -}; - -function mapScheduledAttachments( - attachments?: ReadonlyArray, -): Array | undefined { - if (!attachments || attachments.length === 0) { - return undefined; - } - return attachments.map((attachment) => ({ - id: attachment.id, - filename: attachment.filename, - title: attachment.title ?? attachment.filename, - description: attachment.description ?? undefined, - flags: attachment.flags, - })); -} - -function scheduleAllowedMentions(params: ScheduleMessageParams): AllowedMentions { - return {replied_user: params.replyMentioning ?? true}; -} - -function claimScheduleAttachments(params: ScheduleMessageParams, nonce: string): void { - if (!params.hasAttachments) { - return; - } - MessageSubmitUtils.claimMessageAttachments( - params.channelId, - nonce, - params.content, - params.messageReference, - params.replyMentioning, - ); -} - -async function prepareScheduledMessage(params: ScheduleMessageParams): Promise { - const nonce = SnowflakeUtils.fromTimestamp(Date.now()); - const normalized = normalizeMessageContent(params.content); - claimScheduleAttachments(params, nonce); - let attachments: Array | undefined; - let files: Array | undefined; - if (params.hasAttachments) { - const result = await prepareAttachmentsForNonce(nonce, params.favoriteMemeId); - attachments = result.attachments; - files = result.files; - } - const requestBody = buildMessageCreateRequest({ - content: normalized.content, - nonce, - attachments, - allowedMentions: scheduleAllowedMentions(params), - messageReference: params.messageReference, - flags: normalized.flags, - favoriteMemeId: params.favoriteMemeId, - stickers: params.stickers, - tts: params.tts, - }); - return { - nonce, - normalized, - files, - payload: { - ...requestBody, - scheduled_local_at: params.scheduledLocalAt, - timezone: params.timezone, - }, - }; -} - -function handleScheduleSuccess( - i18n: I18n, - params: ScheduleMessageParams, - nonce: string, - response: ScheduledMessageResponse, -): ScheduledMessage { - const record = ScheduledMessage.fromResponse(response); - ScheduledMessages.upsert(record); - DraftCommands.deleteDraft(params.channelId); - TypingUtils.clear(params.channelId); - MessageCommands.stopReply(params.channelId); - if (params.hasAttachments) { - CloudUpload.removeMessageUpload(nonce); - } - ToastCommands.createToast({ - type: 'success', - children: i18n._(SCHEDULED_MESSAGE_FOR_DESCRIPTOR, { - scheduledTimeLabel: formatScheduledLabel(params.scheduledLocalAt, params.timezone), - }), - }); - return record; -} - -function scheduledMessageUpdateRequest(params: UpdateScheduledMessageParams): ScheduledMessageRequest { - return { - content: params.normalized.content, - attachments: mapScheduledAttachments(params.payload.attachments), - allowed_mentions: params.payload.allowed_mentions ?? (params.replyMentioning ? {replied_user: true} : undefined), - message_reference: - params.payload.message_reference?.channel_id && params.payload.message_reference.message_id - ? { - channel_id: params.payload.message_reference.channel_id, - message_id: params.payload.message_reference.message_id, - guild_id: params.payload.message_reference.guild_id, - type: params.payload.message_reference.type, - } - : undefined, - flags: params.normalized.flags, - favorite_meme_id: params.payload.favorite_meme_id ?? undefined, - sticker_ids: params.payload.sticker_ids, - tts: params.payload.tts ? true : undefined, - scheduled_local_at: params.scheduledLocalAt, - timezone: params.timezone, - }; -} - -async function requestScheduledMessageUpdate( - scheduledMessageId: string, - requestBody: ScheduledMessageRequest, -): Promise { - const response = await http.patch(Endpoints.USER_SCHEDULED_MESSAGE(scheduledMessageId), { - body: requestBody, - }); - return response.body; -} - -export async function fetchScheduledMessages(): Promise> { - logger.debug('Fetching scheduled messages'); - ScheduledMessages.fetchStart(); - try { - const response = await http.get>(Endpoints.USER_SCHEDULED_MESSAGES); - const data = response.body ?? []; - const messages = data.map(ScheduledMessage.fromResponse); - ScheduledMessages.fetchSuccess(messages); - logger.debug('Scheduled messages fetched successfully'); - return messages; - } catch (error) { - ScheduledMessages.fetchError(); - logger.error('Failed to fetch scheduled messages:', error); - throw error; - } -} - -export async function scheduleMessage(i18n: I18n, params: ScheduleMessageParams): Promise { - logger.debug('Scheduling message', params); - const prepared = await prepareScheduledMessage(params); - try { - const response = await scheduleMessageRequest(params.channelId, prepared.payload, prepared.files, prepared.nonce); - return handleScheduleSuccess(i18n, params, prepared.nonce, response.body); - } catch (error) { - handleScheduleError( - i18n, - error, - params.channelId, - prepared.nonce, - params.content, - params.messageReference, - params.replyMentioning, - params.hasAttachments, - ); - throw error; - } -} - -export async function updateScheduledMessage( - i18n: I18n, - params: UpdateScheduledMessageParams, -): Promise { - logger.debug('Updating scheduled message', params); - try { - const record = ScheduledMessage.fromResponse( - await requestScheduledMessageUpdate(params.scheduledMessageId, scheduledMessageUpdateRequest(params)), - ); - ScheduledMessages.upsert(record); - DraftCommands.deleteDraft(params.channelId); - TypingUtils.clear(params.channelId); - MessageCommands.stopReply(params.channelId); - ToastCommands.createToast({ - type: 'success', - children: i18n._(UPDATED_SCHEDULED_MESSAGE_FOR_DESCRIPTOR, { - scheduledTimeLabel: formatScheduledLabel(params.scheduledLocalAt, params.timezone), - }), - }); - return record; - } catch (error) { - logger.error('Failed to update scheduled message', error); - throw error; - } -} - -export async function cancelScheduledMessage(i18n: I18n, scheduledMessageId: string): Promise { - logger.debug('Canceling scheduled message', scheduledMessageId); - try { - await http.delete(Endpoints.USER_SCHEDULED_MESSAGE(scheduledMessageId)); - ScheduledMessages.remove(scheduledMessageId); - ToastCommands.createToast({ - type: 'success', - children: i18n._(REMOVED_SCHEDULED_MESSAGE_DESCRIPTOR), - }); - } catch (error) { - logger.error('Failed to cancel scheduled message', error); - throw error; - } -} - -function restoreDraftAfterScheduleFailure( - channelId: string, - nonce: string, - content: string, - messageReference?: MessageReference, - replyMentioning?: boolean, - hadAttachments?: boolean, -): void { - if (hadAttachments) { - CloudUpload.restoreAttachmentsToTextarea(nonce); - } - DraftCommands.createDraft(channelId, content); - if (messageReference && replyMentioning !== undefined) { - MessageCommands.startReply(channelId, messageReference.message_id, replyMentioning); - } -} - -async function scheduleMessageRequest( - channelId: string, - payload: ScheduledMessageRequest, - files?: Array, - nonce?: string, -): Promise> { - const abortController = new AbortController(); - try { - if (files?.length) { - return await scheduleMultipartMessage(channelId, payload, files, abortController.signal, nonce); - } - return await http.post(Endpoints.CHANNEL_MESSAGE_SCHEDULE(channelId), { - body: payload, - signal: abortController.signal, - }); - } finally { - abortController.abort(); - } -} - -async function scheduleMultipartMessage( - channelId: string, - payload: ScheduledMessageRequest, - files: Array, - signal: AbortSignal, - nonce?: string, -): Promise> { - return http.post(Endpoints.CHANNEL_MESSAGE_SCHEDULE(channelId), { - multipart: { - files: files.map((file, index) => ({name: `files[${index}]`, file, filename: file.name})), - fields: {payload_json: JSON.stringify(payload)}, - }, - signal, - onProgress: nonce - ? (event) => { - if (event.lengthComputable && event.total > 0) { - const progress = (event.loaded / event.total) * 100; - CloudUpload.updateSendingProgress(nonce, progress); - } - } - : undefined, - }); -} - -const getApiErrorBody = (error: HttpError): ApiErrorBody | undefined => { - return typeof error.body === 'object' && error.body !== null ? (error.body as ApiErrorBody) : undefined; -}; - -function handleScheduleError( - i18n: I18n, - error: unknown, - channelId: string, - nonce: string, - content: string, - messageReference?: MessageReference, - replyMentioning?: boolean, - hadAttachments?: boolean, -): void { - restoreDraftAfterScheduleFailure(channelId, nonce, content, messageReference, replyMentioning, hadAttachments); - if (!(error instanceof HttpError)) { - ModalCommands.push( - modal(() => ( - - )), - ); - return; - } - if (isRateLimitError(error)) { - handleScheduleRateLimit(i18n, error); - return; - } - if (isSlowmodeError(error)) { - const retryAfterMs = SlowmodeCommands.clampSlowmodeRetryAfterMs(resolveRetryAfterMs(error)); - if (retryAfterMs <= 0) { - ModalCommands.push( - modal(() => ( - - )), - ); - return; - } - const retryAfter = Math.ceil(retryAfterMs / 1000); - SlowmodeCommands.updateSlowmodeRemaining(channelId, retryAfterMs); - ModalCommands.push( - modal(() => ( - - )), - ); - return; - } - if (isFeatureDisabledError(error)) { - ModalCommands.push( - modal(() => ( - - )), - ); - return; - } - if (isExplicitContentError(error)) { - ModalCommands.push( - modal(() => ( - - )), - ); - return; - } - if (isFileTooLargeError(error)) { - ModalCommands.push( - modal(() => ( - - )), - ); - return; - } - ModalCommands.push( - modal(() => ( - - )), - ); -} - -function handleScheduleRateLimit(_i18n: I18n, error: HttpError): void { - const retryAfterMs = resolveRetryAfterMs(error); - const retryAfterSeconds = retryAfterMs === null ? undefined : Math.ceil(retryAfterMs / 1000); - ModalCommands.push( - modal(() => ( - - )), - ); - logger.warn('Scheduled message rate limited, retry after', retryAfterSeconds); -} - -function isRateLimitError(error: HttpError): boolean { - if (error.status !== 429) return false; - return !isSlowmodeError(error); -} - -function isSlowmodeError(error: HttpError): boolean { - if (error.status !== 400 && error.status !== 429) return false; - const body = getApiErrorBody(error); - if (body === undefined) return false; - return body.code === APIErrorCodes.SLOWMODE_RATE_LIMITED; -} - -function isFeatureDisabledError(error: HttpError): boolean { - return error?.status === 403 && getApiErrorBody(error)?.code === APIErrorCodes.FEATURE_TEMPORARILY_DISABLED; -} - -function isExplicitContentError(error: HttpError): boolean { - return getApiErrorBody(error)?.code === APIErrorCodes.EXPLICIT_CONTENT_CANNOT_BE_SENT; -} - -function isFileTooLargeError(error: HttpError): boolean { - return getApiErrorBody(error)?.code === APIErrorCodes.FILE_SIZE_TOO_LARGE; -} diff --git a/fluxer_app/src/features/messaging/components/popouts/InboxPopout.tsx b/fluxer_app/src/features/messaging/components/popouts/InboxPopout.tsx index c8b17f9b5..8f5115a19 100644 --- a/fluxer_app/src/features/messaging/components/popouts/InboxPopout.tsx +++ b/fluxer_app/src/features/messaging/components/popouts/InboxPopout.tsx @@ -7,7 +7,6 @@ import Inbox, {type InboxTab} from '@app/features/inbox/state/Inbox'; import styles from '@app/features/messaging/components/popouts/InboxPopout.module.css'; import {RecentMentionsContent} from '@app/features/messaging/components/popouts/RecentMentionsContent'; import {SavedMessagesContent} from '@app/features/messaging/components/popouts/SavedMessagesContent'; -import {ScheduledMessagesContent} from '@app/features/messaging/components/popouts/ScheduledMessagesContent'; import ReadStates from '@app/features/read_state/state/ReadStates'; import {remFromPx} from '@app/features/theme/layout/RemFromPx'; import FocusRing from '@app/features/ui/focus_ring/FocusRing'; @@ -20,10 +19,9 @@ import { import {getNextTabIndex, getTabNavigationDirection} from '@app/features/ui/tabs/TabKeyboardNavigation'; import {Tooltip} from '@app/features/ui/tooltip/Tooltip'; import UserGuildSettings from '@app/features/user/state/UserGuildSettings'; -import Users from '@app/features/user/state/Users'; import {msg} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; -import {AtIcon, BellIcon, BookmarkSimpleIcon, CheckIcon, ClockIcon} from '@phosphor-icons/react'; +import {AtIcon, BellIcon, BookmarkSimpleIcon, CheckIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; @@ -37,10 +35,6 @@ const BOOKMARKS_DESCRIPTOR = msg({ message: 'Bookmarks', comment: 'Tab label in the inbox popout listing the user bookmarks.', }); -const SCHEDULED_DESCRIPTOR = msg({ - message: 'Scheduled', - comment: 'Tab label in the inbox popout listing the user scheduled messages.', -}); const INBOX_TABS_DESCRIPTOR = msg({ message: 'Inbox tabs', comment: 'Accessible label for the inbox popout tablist.', @@ -141,15 +135,7 @@ export const InboxPopout = observer(({initialTab}: {initialTab?: InboxTab} = {}) icon: , }, ]; - const scheduledTab: TabConfig = { - key: 'scheduled', - label: i18n._(SCHEDULED_DESCRIPTOR), - icon: , - }; - const currentUser = Users.getCurrentUser(); - let showScheduledTab = false; - if (currentUser != null) showScheduledTab = currentUser.isStaff(); - const tabs = showScheduledTab ? [...baseTabs, scheduledTab] : baseTabs; + const tabs = baseTabs; const normalizedActiveTab = tabs.some((tab) => tab.key === activeTab) ? activeTab : tabs[0].key; const setActiveTab = useCallback((tab: InboxTab) => { InboxCommands.setTab(tab); @@ -253,19 +239,6 @@ export const InboxPopout = observer(({initialTab}: {initialTab?: InboxTab} = {}) />
)} - {showScheduledTab && normalizedActiveTab === 'scheduled' && ( -
- -
- )} ); return ( diff --git a/fluxer_app/src/features/messaging/components/popouts/ScheduledMessagesContent.module.css b/fluxer_app/src/features/messaging/components/popouts/ScheduledMessagesContent.module.css deleted file mode 100644 index bb901dc4b..000000000 --- a/fluxer_app/src/features/messaging/components/popouts/ScheduledMessagesContent.module.css +++ /dev/null @@ -1,56 +0,0 @@ -/* SPDX-License-Identifier: AGPL-3.0-or-later */ - -.cardHeader { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.375rem; - gap: 0.5rem; -} - -.statusBadge { - font-size: 0.6875rem; - font-weight: 600; - padding: 0.125rem 0.625rem; - border-radius: 62.4375rem; - background-color: var(--background-modifier-accent); - color: var(--text-primary); -} - -.statusInvalid { - background-color: var(--text-warning); - color: var(--background-primary); -} - -.timestamp { - font-size: 0.75rem; - color: var(--text-primary-muted); -} - -.messageText { - font-size: 0.875rem; - line-height: 1.25rem; - color: var(--text-primary); - margin-bottom: 0.375rem; - min-height: 2.75rem; -} - -.attachmentsInfo { - font-size: 0.75rem; - color: var(--text-primary-muted); - margin-bottom: 0.375rem; -} - -.statusReason { - display: flex; - align-items: center; - gap: 0.375rem; - font-size: 0.8125rem; - color: var(--text-warning); - margin-bottom: 0.375rem; -} - -.warningIcon { - width: 1rem; - height: 1rem; -} diff --git a/fluxer_app/src/features/messaging/components/popouts/ScheduledMessagesContent.tsx b/fluxer_app/src/features/messaging/components/popouts/ScheduledMessagesContent.tsx deleted file mode 100644 index c206262cc..000000000 --- a/fluxer_app/src/features/messaging/components/popouts/ScheduledMessagesContent.tsx +++ /dev/null @@ -1,218 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import previewStyles from '@app/features/app/components/shared/MessagePreview.module.css'; -import * as ScheduledMessageCommands from '@app/features/messaging/commands/ScheduledMessageCommands'; -import styles from '@app/features/messaging/components/popouts/ScheduledMessagesContent.module.css'; -import {useMessageSelectionCopy} from '@app/features/messaging/hooks/useMessageSelectionCopy'; -import ScheduledMessages from '@app/features/messaging/state/ScheduledMessages'; -import {Scroller} from '@app/features/ui/components/Scroller'; -import {Spinner} from '@app/features/ui/components/Spinner'; -import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils'; -import {formatScheduledMessage} from '@fluxer/date_utils/src/DateFormatting'; -import {msg} from '@lingui/core/macro'; -import {useLingui} from '@lingui/react/macro'; -import {FlagCheckeredIcon, WarningCircleIcon} from '@phosphor-icons/react'; -import {observer} from 'mobx-react-lite'; -import {useCallback, useEffect, useState} from 'react'; - -const NO_SCHEDULED_MESSAGES_DESCRIPTOR = msg({ - message: 'No scheduled messages', - comment: 'Empty state title in the scheduled messages popout when the user has none scheduled.', -}); -const RIGHT_CLICK_THE_SEND_BUTTON_TO_SCHEDULE_A_DESCRIPTOR = msg({ - message: 'Right-click the send button to schedule a message.', - comment: 'Empty state hint in the scheduled messages popout explaining how to schedule a new message.', -}); -const INVALID_DESCRIPTOR = msg({ - message: 'Invalid', - comment: 'Status pill on a scheduled message row when the schedule is in the past or otherwise invalid.', -}); -const SCHEDULED_DESCRIPTOR = msg({ - message: 'Scheduled', - comment: 'Status pill on a scheduled message row indicating it will send at the chosen time.', -}); -const ATTACHMENT_ONLY_MESSAGE_DESCRIPTOR = msg({ - message: 'Attachment only message', - comment: 'Placeholder content text shown for scheduled messages that have attachments but no text content.', -}); -const NO_CONTENT_DESCRIPTOR = msg({ - message: '(no content)', - comment: - 'Placeholder content text shown for scheduled messages with neither text nor attachments. Keep the parentheses.', -}); -const ATTACHMENTS_DESCRIPTOR = msg({ - message: 'Attachments', - comment: 'Label in the scheduled message row preview indicating that attachments are included.', -}); -const CANCEL_DESCRIPTOR = msg({ - message: 'Cancel', - comment: 'Button label on a scheduled message row that cancels (deletes) the scheduled send.', -}); -const YOU_RE_CAUGHT_UP_DESCRIPTOR = msg({ - message: "You're caught up", - comment: - 'End-of-list title in the scheduled messages popout when all scheduled messages are visible. Tone can be friendly.', -}); -const NO_MORE_SCHEDULED_MESSAGES_DESCRIPTOR = msg({ - message: "That's everything in the queue.", - comment: 'End-of-list body text in the scheduled messages popout when all scheduled messages are visible.', -}); -export const ScheduledMessagesContent = observer(() => { - const {i18n} = useLingui(); - const {scheduledMessages, fetched, fetching} = ScheduledMessages; - const [cancellingId, setCancellingId] = useState(null); - const getScheduledMessagePlaintext = useCallback( - (messageId: string) => { - const message = scheduledMessages.find((entry) => entry.id === messageId); - if (!message) { - return null; - } - const blocks: Array = []; - const content = message.payload.content?.trim(); - const attachmentCount = message.payload.attachments?.length ?? 0; - if (content) { - blocks.push(content); - } - if (attachmentCount > 0) { - blocks.push(`${i18n._(ATTACHMENTS_DESCRIPTOR)}: ${attachmentCount}`); - } - if (message.status === 'invalid' && message.statusReason) { - blocks.push(message.statusReason); - } - return blocks.length > 0 ? blocks.join('\n') : i18n._(NO_CONTENT_DESCRIPTOR); - }, - [i18n, scheduledMessages], - ); - const onCopySelectedMessages = useMessageSelectionCopy({ - getMessagePlaintext: getScheduledMessagePlaintext, - }); - useEffect(() => { - if (!fetched && !fetching) { - ScheduledMessageCommands.fetchScheduledMessages(); - } - }, [fetched, fetching]); - const handleCancel = useCallback( - async (messageId: string) => { - setCancellingId(messageId); - try { - await ScheduledMessageCommands.cancelScheduledMessage(i18n, messageId); - } finally { - setCancellingId(null); - } - }, - [i18n], - ); - if (fetching) { - return ( -
- -
- ); - } - if (scheduledMessages.length === 0) { - return ( -
-
- -
-

- {i18n._(NO_SCHEDULED_MESSAGES_DESCRIPTOR)} -

-

- {i18n._(RIGHT_CLICK_THE_SEND_BUTTON_TO_SCHEDULE_A_DESCRIPTOR)} -

-
-
-
- ); - } - const formatScheduledAt = (message: (typeof scheduledMessages)[number]) => { - try { - return formatScheduledMessage(message.scheduledAt, getCurrentLocale(), message.timezone); - } catch { - return `${message.scheduledLocalAt} (${message.timezone})`; - } - }; - return ( - - {scheduledMessages.map((message) => ( -
-
- - {message.status === 'invalid' ? i18n._(INVALID_DESCRIPTOR) : i18n._(SCHEDULED_DESCRIPTOR)} - - - {formatScheduledAt(message)} - -
-

- {message.payload.content ?? - (message.payload.attachments?.length - ? i18n._(ATTACHMENT_ONLY_MESSAGE_DESCRIPTOR) - : i18n._(NO_CONTENT_DESCRIPTOR))} -

- {message.payload.attachments?.length ? ( -
- {i18n._(ATTACHMENTS_DESCRIPTOR)}: {message.payload.attachments.length} -
- ) : null} - {message.status === 'invalid' && message.statusReason ? ( -
- - {message.statusReason} -
- ) : null} -
- -
-
- ))} -
-
- -
-

- {i18n._(YOU_RE_CAUGHT_UP_DESCRIPTOR)} -

-

- {i18n._(NO_MORE_SCHEDULED_MESSAGES_DESCRIPTOR)} -

-
-
-
-
- ); -}); diff --git a/fluxer_app/src/features/messaging/models/ScheduledMessage.ts b/fluxer_app/src/features/messaging/models/ScheduledMessage.ts deleted file mode 100644 index f30fbf430..000000000 --- a/fluxer_app/src/features/messaging/models/ScheduledMessage.ts +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -export interface ScheduledMessageReference { - message_id: string; - channel_id?: string; - guild_id?: string; - type?: number; -} - -export interface ScheduledAllowedMentions { - parse?: Array<'users' | 'roles' | 'everyone'>; - users?: Array; - roles?: Array; - replied_user?: boolean; -} - -export interface ScheduledAttachment { - id: string; - filename: string; - title?: string; - description?: string; - flags?: number; -} - -export interface ScheduledMessagePayload { - content?: string | null; - embeds?: Array; - attachments?: Array; - message_reference?: ScheduledMessageReference; - allowed_mentions?: ScheduledAllowedMentions; - flags?: number; - nonce?: string; - favorite_meme_id?: string; - sticker_ids?: Array; - tts?: boolean; -} - -export type ScheduledMessageStatus = 'pending' | 'invalid'; - -export interface ScheduledMessageResponse { - id: string; - channel_id: string; - scheduled_at: string; - scheduled_local_at: string; - timezone: string; - status: ScheduledMessageStatus; - status_reason: string | null; - payload: ScheduledMessagePayload; - created_at: string; - invalidated_at: string | null; -} - -export class ScheduledMessage { - readonly id: string; - readonly channelId: string; - readonly scheduledAt: Date; - readonly scheduledLocalAt: string; - readonly timezone: string; - readonly payload: ScheduledMessagePayload; - readonly status: ScheduledMessageStatus; - readonly statusReason: string | null; - readonly createdAt: Date; - readonly invalidatedAt: Date | null; - - constructor(response: ScheduledMessageResponse) { - this.id = response.id; - this.channelId = response.channel_id; - this.scheduledAt = new Date(response.scheduled_at); - this.scheduledLocalAt = response.scheduled_local_at; - this.timezone = response.timezone; - this.payload = response.payload; - this.status = response.status ?? 'pending'; - this.statusReason = response.status_reason; - this.createdAt = new Date(response.created_at); - this.invalidatedAt = response.invalidated_at ? new Date(response.invalidated_at) : null; - } - - static fromResponse(response: ScheduledMessageResponse): ScheduledMessage { - return new ScheduledMessage(response); - } -} diff --git a/fluxer_app/src/features/messaging/state/ScheduledMessageEditor.ts b/fluxer_app/src/features/messaging/state/ScheduledMessageEditor.ts deleted file mode 100644 index 5ef7f12ea..000000000 --- a/fluxer_app/src/features/messaging/state/ScheduledMessageEditor.ts +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ScheduledMessage, ScheduledMessagePayload} from '@app/features/messaging/models/ScheduledMessage'; -import {makeAutoObservable} from 'mobx'; - -interface ScheduledMessageEditState { - scheduledMessageId: string; - channelId: string; - payload: ScheduledMessagePayload; - scheduledLocalAt: string; - timezone: string; -} - -class ScheduledMessageEditor { - private state: ScheduledMessageEditState | null = null; - - constructor() { - makeAutoObservable(this, {}, {autoBind: true}); - } - - startEditing(record: ScheduledMessage): void { - this.state = { - scheduledMessageId: record.id, - channelId: record.channelId, - payload: record.payload, - scheduledLocalAt: record.scheduledLocalAt, - timezone: record.timezone, - }; - } - - stopEditing(): void { - this.state = null; - } - - isEditingChannel(channelId: string): boolean { - return this.state?.channelId === channelId; - } - - getEditingState(): ScheduledMessageEditState | null { - return this.state; - } -} - -export default new ScheduledMessageEditor(); diff --git a/fluxer_app/src/features/messaging/state/ScheduledMessages.ts b/fluxer_app/src/features/messaging/state/ScheduledMessages.ts deleted file mode 100644 index 81ca0cfea..000000000 --- a/fluxer_app/src/features/messaging/state/ScheduledMessages.ts +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import type {ScheduledMessage} from '@app/features/messaging/models/ScheduledMessage'; -import {makeAutoObservable} from 'mobx'; - -class ScheduledMessages { - scheduledMessages: Array = []; - fetched = false; - fetching = false; - - constructor() { - makeAutoObservable(this, {}, {autoBind: true}); - } - - get hasScheduledMessages(): boolean { - return this.scheduledMessages.length > 0; - } - - fetchStart(): void { - this.fetching = true; - } - - fetchSuccess(messages: Array): void { - this.scheduledMessages = sortScheduledMessages(messages); - this.fetching = false; - this.fetched = true; - } - - fetchError(): void { - this.fetching = false; - this.fetched = false; - this.scheduledMessages = []; - } - - handleGatewayReady(): void { - this.scheduledMessages = []; - this.fetched = false; - this.fetching = false; - } - - upsert(message: ScheduledMessage): void { - const existingIndex = this.scheduledMessages.findIndex((entry) => entry.id === message.id); - const next = [...this.scheduledMessages]; - if (existingIndex === -1) { - next.push(message); - } else { - next[existingIndex] = message; - } - this.scheduledMessages = sortScheduledMessages(next); - } - - remove(messageId: string): void { - this.scheduledMessages = this.scheduledMessages.filter((message) => message.id !== messageId); - } -} - -function sortScheduledMessages(messages: Array): Array { - return [...messages].sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime()); -} - -export default new ScheduledMessages(); diff --git a/packages/constants/src/ValidationErrorCodes.ts b/packages/constants/src/ValidationErrorCodes.ts index 8aa791283..1925bd95a 100644 --- a/packages/constants/src/ValidationErrorCodes.ts +++ b/packages/constants/src/ValidationErrorCodes.ts @@ -113,7 +113,6 @@ export const ValidationErrorCodes = { INVALID_CODE: 'INVALID_CODE', INVALID_CURRENT_PASSWORD: 'INVALID_CURRENT_PASSWORD', INVALID_DATE_OF_BIRTH_FORMAT: 'INVALID_DATE_OF_BIRTH_FORMAT', - INVALID_DATETIME_FOR_SCHEDULED_SEND: 'INVALID_DATETIME_FOR_SCHEDULED_SEND', INVALID_EMAIL_ADDRESS: 'INVALID_EMAIL_ADDRESS', INVALID_EMAIL_FORMAT: 'INVALID_EMAIL_FORMAT', INVALID_EMAIL_LOCAL_PART: 'INVALID_EMAIL_LOCAL_PART', @@ -143,7 +142,6 @@ export const ValidationErrorCodes = { INVALID_PROOF_TOKEN: 'INVALID_PROOF_TOKEN', INVALID_ROLE_ID: 'INVALID_ROLE_ID', INVALID_RTC_REGION: 'INVALID_RTC_REGION', - INVALID_SCHEDULED_MESSAGE_PAYLOAD: 'INVALID_SCHEDULED_MESSAGE_PAYLOAD', INVALID_SNOWFLAKE: 'INVALID_SNOWFLAKE', INVALID_SSO_AUTHORIZATION_CODE: 'INVALID_SSO_AUTHORIZATION_CODE', INVALID_SSO_TOKEN: 'INVALID_SSO_TOKEN', @@ -201,8 +199,6 @@ export const ValidationErrorCodes = { RECIPIENT_IDS_MUST_BE_VALID_SNOWFLAKES: 'RECIPIENT_IDS_MUST_BE_VALID_SNOWFLAKES', REFERENCED_ATTACHMENT_NOT_FOUND: 'REFERENCED_ATTACHMENT_NOT_FOUND', ROWS_IS_REQUIRED: 'ROWS_IS_REQUIRED', - SCHEDULED_MESSAGES_MAX_30_DAYS: 'SCHEDULED_MESSAGES_MAX_30_DAYS', - SCHEDULED_TIME_MUST_BE_FUTURE: 'SCHEDULED_TIME_MUST_BE_FUTURE', SESSION_TIMEOUT: 'SESSION_TIMEOUT', SIZE_BYTES_MUST_BE_VALID_INTEGER: 'SIZE_BYTES_MUST_BE_VALID_INTEGER', SSO_IDENTITY_MISMATCH: 'SSO_IDENTITY_MISMATCH', diff --git a/packages/date_utils/src/DateFormatting.ts b/packages/date_utils/src/DateFormatting.ts index 835201e40..0278186ce 100644 --- a/packages/date_utils/src/DateFormatting.ts +++ b/packages/date_utils/src/DateFormatting.ts @@ -23,31 +23,6 @@ export function getFormattedDateTime(timestamp: DateInput, locale: string = DEFA }).format(date); } -export function getFormattedDateTimeInZone( - isoString: string, - timezone: string, - locale: string = DEFAULT_LOCALE, - hour12?: boolean, -): string { - try { - const date = new Date(isoString); - if (Number.isNaN(date.getTime())) { - return isoString; - } - return getDateFormatter(locale, { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: resolveHour12(locale, hour12), - timeZone: timezone, - }).format(date); - } catch { - return isoString; - } -} - export function getFormattedShortDate(timestamp: DateInput, locale: string = DEFAULT_LOCALE): string { return getDateFormatter(locale, {month: 'short', day: 'numeric', year: 'numeric'}).format(parseDate(timestamp)); } @@ -149,11 +124,3 @@ export function formatLastActive(date: DateInput, locale: string = DEFAULT_LOCAL } return getDateFormatter(locale, {dateStyle: 'medium', timeStyle: 'short'}).format(dateObj); } - -export function formatScheduledMessage(date: DateInput, locale: string = DEFAULT_LOCALE, timeZone?: string): string { - const dateObj = parseDate(date); - if (Number.isNaN(dateObj.getTime())) { - return String(date); - } - return getDateFormatter(locale, {dateStyle: 'medium', timeStyle: 'short', timeZone}).format(dateObj); -} diff --git a/packages/errors/src/i18n/ErrorCodeMappings.ts b/packages/errors/src/i18n/ErrorCodeMappings.ts index 75b872e70..f5e722d0c 100644 --- a/packages/errors/src/i18n/ErrorCodeMappings.ts +++ b/packages/errors/src/i18n/ErrorCodeMappings.ts @@ -384,10 +384,6 @@ export const ErrorCodeToI18nKey = { [ValidationErrorCodes.VOICE_MESSAGES_DURATION_EXCEEDS_LIMIT]: 'calls_and_voice.voice_message_duration_exceeds_limit', [ValidationErrorCodes.VOICE_MESSAGES_REQUIRE_SINGLE_ATTACHMENT]: 'calls_and_voice.voice_messages_require_single_attachment', - [ValidationErrorCodes.SCHEDULED_TIME_MUST_BE_FUTURE]: 'messages.scheduled_time_must_be_future', - [ValidationErrorCodes.SCHEDULED_MESSAGES_MAX_30_DAYS]: 'messages.scheduled_messages_max_30_days', - [ValidationErrorCodes.INVALID_SCHEDULED_MESSAGE_PAYLOAD]: 'messages.invalid_scheduled_message_payload', - [ValidationErrorCodes.INVALID_DATETIME_FOR_SCHEDULED_SEND]: 'messages.scheduled_send_datetime_invalid', [ValidationErrorCodes.MUST_START_SESSION_BEFORE_SENDING]: 'auth_and_oauth.must_start_session_before_sending_messages', [ValidationErrorCodes.TOO_MANY_EMBEDS]: 'embeds.too_many_embeds', [ValidationErrorCodes.ATTACHMENTS_METADATA_REQUIRED_WHEN_UPLOADING]: diff --git a/packages/errors/src/i18n/ErrorI18nMessages.ts b/packages/errors/src/i18n/ErrorI18nMessages.ts index 36252f06b..f061a25f5 100644 --- a/packages/errors/src/i18n/ErrorI18nMessages.ts +++ b/packages/errors/src/i18n/ErrorI18nMessages.ts @@ -432,13 +432,9 @@ export const ERROR_I18N_MESSAGES = { 'messages.forward_reference_requires_channel_and_message': 'Forward message reference must include `channel_id` and `message_id`.', 'messages.invalid_message_data': 'Invalid message data.', - 'messages.invalid_scheduled_message_payload': 'Invalid scheduled message payload.', 'messages.message_ids_cannot_be_empty': "`message_ids` can't be empty.", 'messages.messages_array_required': '`messages` array is required and must not be empty.', 'messages.messages_with_snapshots_cannot_be_edited': "Messages with snapshots can't be edited.", - 'messages.scheduled_messages_max_30_days': 'Scheduled messages can be at most 30 days in the future.', - 'messages.scheduled_send_datetime_invalid': 'Invalid date or time for scheduled send.', - 'messages.scheduled_time_must_be_future': 'Scheduled time must be in the future.', 'messages.voice_messages_attachment_must_be_audio': 'Voice message attachments must be audio files.', 'messages.voice_messages_cannot_have_content': "Voice messages can't have text content.", 'messages.voice_messages_cannot_have_embeds': "Voice messages can't have embeds.", diff --git a/packages/errors/src/i18n/locales/ar.ts b/packages/errors/src/i18n/locales/ar.ts index 0740c74f0..91cd3b7c4 100644 --- a/packages/errors/src/i18n/locales/ar.ts +++ b/packages/errors/src/i18n/locales/ar.ts @@ -375,13 +375,9 @@ const ERROR_I18N_AR_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "لا يمكن أن تحتوي الرسائل المعاد توجيهها على محتوى، تضمينات، مرفقات، أو ملصقات.", "messages.forward_reference_requires_channel_and_message": "يجب أن يتضمن مرجع الرسالة المعاد توجيهها كلاً من `channel_id` و `message_id`.", "messages.invalid_message_data": "بيانات الرسالة غير صحيحة.", - "messages.invalid_scheduled_message_payload": "حمولة الرسالة المجدولة غير صالحة.", "messages.message_ids_cannot_be_empty": "لا يمكن أن تكون `message_ids` فارغة.", "messages.messages_array_required": "مصفوفة `messages` مطلوبة ولا يمكن أن تكون فارغة.", "messages.messages_with_snapshots_cannot_be_edited": "لا يمكن تعديل الرسائل التي تحتوي على لقطات.", - "messages.scheduled_messages_max_30_days": "يمكن جدولة الرسائل لمدة أقصاها 30 يومًا فقط.", - "messages.scheduled_send_datetime_invalid": "تاريخ أو وقت غير صحيح للإرسال المجدول.", - "messages.scheduled_time_must_be_future": "يجب أن يكون الوقت المحدد في المستقبل.", "messages.voice_messages_attachment_must_be_audio": "يجب أن تكون مرفقات الرسائل الصوتية ملفات صوتية.", "messages.voice_messages_cannot_have_content": "لا يمكن للرسائل الصوتية أن تحتوي على نص.", "messages.voice_messages_cannot_have_embeds": "لا يمكن للرسائل الصوتية أن تحتوي على تضمينات.", diff --git a/packages/errors/src/i18n/locales/bg.ts b/packages/errors/src/i18n/locales/bg.ts index 9ceeb5b97..cad892fc7 100644 --- a/packages/errors/src/i18n/locales/bg.ts +++ b/packages/errors/src/i18n/locales/bg.ts @@ -375,13 +375,9 @@ const ERROR_I18N_BG_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Препратените съобщения не могат да съдържат съдържание, вградено съдържание, прикачени файлове или стикери.", "messages.forward_reference_requires_channel_and_message": "Препратката към съобщението трябва да съдържа `channel_id` и `message_id`.", "messages.invalid_message_data": "Невалидни данни на съобщението.", - "messages.invalid_scheduled_message_payload": "Невалидни данни за планирано съобщение.", "messages.message_ids_cannot_be_empty": "`message_ids` не могат да бъдат празни.", "messages.messages_array_required": "Масивът `messages` е задължителен и не може да е празен.", "messages.messages_with_snapshots_cannot_be_edited": "Съобщения с моментни снимки не могат да бъдат редактирани.", - "messages.scheduled_messages_max_30_days": "Планираните съобщения могат да бъдат най-много 30 дни в бъдещето.", - "messages.scheduled_send_datetime_invalid": "Невалидна дата или час за изпращане по график.", - "messages.scheduled_time_must_be_future": "Часът за изпращане трябва да е в бъдещето.", "messages.voice_messages_attachment_must_be_audio": "Прикачените файлове към гласови съобщения трябва да бъдат аудио файлове.", "messages.voice_messages_cannot_have_content": "Гласовите съобщения не могат да имат текстово съдържание.", "messages.voice_messages_cannot_have_embeds": "Гласовите съобщения не могат да съдържат вградено съдържание.", diff --git a/packages/errors/src/i18n/locales/cs.ts b/packages/errors/src/i18n/locales/cs.ts index 648fc3fd1..2a6b2ea09 100644 --- a/packages/errors/src/i18n/locales/cs.ts +++ b/packages/errors/src/i18n/locales/cs.ts @@ -375,13 +375,9 @@ const ERROR_I18N_CS_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Přeposlané zprávy nemohou obsahovat obsah, vložené prvky, přílohy ani samolepky.", "messages.forward_reference_requires_channel_and_message": "Odkaz na přeposlanou zprávu musí obsahovat `channel_id` a `message_id`.", "messages.invalid_message_data": "Neplatná data u zprávy.", - "messages.invalid_scheduled_message_payload": "Neplatná data plánované zprávy.", "messages.message_ids_cannot_be_empty": "`message_ids` nemohou být prázdné.", "messages.messages_array_required": "Pole `messages` je nutné a nesmí být prázdné.", "messages.messages_with_snapshots_cannot_be_edited": "Zprávy se snímky nelze upravit.", - "messages.scheduled_messages_max_30_days": "Plánované zprávy mohou být maximálně 30 dní do budoucna.", - "messages.scheduled_send_datetime_invalid": "Neplatné datum nebo čas pro plánované odeslání.", - "messages.scheduled_time_must_be_future": "Plánovaný čas musí být v budoucnu.", "messages.voice_messages_attachment_must_be_audio": "Přílohy hlasových zpráv musí být zvukové soubory.", "messages.voice_messages_cannot_have_content": "Hlasové zprávy nemohou mít text.", "messages.voice_messages_cannot_have_embeds": "Hlasové zprávy nemohou mít vložené prvky.", diff --git a/packages/errors/src/i18n/locales/da.ts b/packages/errors/src/i18n/locales/da.ts index 69f65e80a..958bfa4da 100644 --- a/packages/errors/src/i18n/locales/da.ts +++ b/packages/errors/src/i18n/locales/da.ts @@ -375,13 +375,9 @@ const ERROR_I18N_DA_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Videresendte beskeder kan ikke indeholde indhold, indlejringer, vedhæftede filer eller stickers.", "messages.forward_reference_requires_channel_and_message": "Reference for videresendte beskeder skal indeholde `channel_id` og `message_id`.", "messages.invalid_message_data": "Ugyldige beskeddata.", - "messages.invalid_scheduled_message_payload": "Ugyldig planlagt besked-nyttelast.", "messages.message_ids_cannot_be_empty": "`message_ids` må ikke være tomme.", "messages.messages_array_required": "`messages`-arrayet skal være angivet og må ikke være tomt.", "messages.messages_with_snapshots_cannot_be_edited": "Beskeder med snapshots kan ikke redigeres.", - "messages.scheduled_messages_max_30_days": "Planlagte beskeder kan højst være 30 dage ude i fremtiden.", - "messages.scheduled_send_datetime_invalid": "Ugyldig dato eller tidspunkt for planlagt afsendelse.", - "messages.scheduled_time_must_be_future": "Planlagt tidspunkt skal være i fremtiden.", "messages.voice_messages_attachment_must_be_audio": "Vedhæftede filer til talebeskeder skal være lydfiler.", "messages.voice_messages_cannot_have_content": "Talebeskeder kan ikke have tekstindhold.", "messages.voice_messages_cannot_have_embeds": "Talebeskeder kan ikke have indlejringer.", diff --git a/packages/errors/src/i18n/locales/de.ts b/packages/errors/src/i18n/locales/de.ts index 1cafa1f64..30134a814 100644 --- a/packages/errors/src/i18n/locales/de.ts +++ b/packages/errors/src/i18n/locales/de.ts @@ -375,13 +375,9 @@ const ERROR_I18N_DE_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Weitergeleitete Nachrichten dürfen keine Inhalte, Embeds, Anhänge oder Sticker enthalten.", "messages.forward_reference_requires_channel_and_message": "Die Referenz für weitergeleitete Nachrichten muss `channel_id` und `message_id` enthalten.", "messages.invalid_message_data": "Ungültige Nachrichtendaten.", - "messages.invalid_scheduled_message_payload": "Ungültige Payload für geplante Nachrichten.", "messages.message_ids_cannot_be_empty": "`message_ids` darf nicht leer sein.", "messages.messages_array_required": "Das `messages`-Array ist erforderlich und darf nicht leer sein.", "messages.messages_with_snapshots_cannot_be_edited": "Nachrichten mit Snapshots können nicht bearbeitet werden.", - "messages.scheduled_messages_max_30_days": "Geplante Nachrichten können maximal 30 Tage in die Zukunft reichen.", - "messages.scheduled_send_datetime_invalid": "Ungültiges Datum oder ungültige Uhrzeit für den geplanten Versand.", - "messages.scheduled_time_must_be_future": "Die geplante Zeit muss in der Zukunft sein.", "messages.voice_messages_attachment_must_be_audio": "Sprachnachrichten-Anhänge müssen Audiodateien sein.", "messages.voice_messages_cannot_have_content": "Sprachnachrichten können keinen Text enthalten.", "messages.voice_messages_cannot_have_embeds": "Sprachnachrichten können keine Einbettungen enthalten.", diff --git a/packages/errors/src/i18n/locales/el.ts b/packages/errors/src/i18n/locales/el.ts index 07d93b2ea..1039b2bad 100644 --- a/packages/errors/src/i18n/locales/el.ts +++ b/packages/errors/src/i18n/locales/el.ts @@ -375,13 +375,9 @@ const ERROR_I18N_EL_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Τα προωθημένα μηνύματα δεν μπορούν να περιλαμβάνουν περιεχόμενο, ενσωματώσεις, συνημμένα ή sticker.", "messages.forward_reference_requires_channel_and_message": "Η αναφορά προώθησης μηνύματος πρέπει να περιλαμβάνει `channel_id` και `message_id`.", "messages.invalid_message_data": "Μη έγκυρα δεδομένα μηνύματος.", - "messages.invalid_scheduled_message_payload": "Μη έγκυρο ωφέλιμο φορτίο προγραμματισμένου μηνύματος.", "messages.message_ids_cannot_be_empty": "Το `message_ids` δεν μπορεί να είναι άδειο.", "messages.messages_array_required": "Ο πίνακας `messages` απαιτείται και δεν μπορεί να είναι άδειος.", "messages.messages_with_snapshots_cannot_be_edited": "Δεν μπορείς να επεξεργαστείς μηνύματα με στιγμιότυπα.", - "messages.scheduled_messages_max_30_days": "Τα προγραμματισμένα μηνύματα μπορούν να οριστούν έως και 30 ημέρες από τώρα.", - "messages.scheduled_send_datetime_invalid": "Άκυρη ημερομηνία ή ώρα για προγραμματισμένη αποστολή.", - "messages.scheduled_time_must_be_future": "Η προγραμματισμένη ώρα πρέπει να είναι στο μέλλον.", "messages.voice_messages_attachment_must_be_audio": "Τα συνημμένα φωνητικά μηνύματα πρέπει να είναι αρχεία ήχου.", "messages.voice_messages_cannot_have_content": "Τα φωνητικά μηνύματα δεν μπορούν να έχουν κείμενο.", "messages.voice_messages_cannot_have_embeds": "Τα φωνητικά μηνύματα δεν μπορούν να έχουν ενσωματώσεις.", diff --git a/packages/errors/src/i18n/locales/en-GB.ts b/packages/errors/src/i18n/locales/en-GB.ts index 69fcdc330..ef59c9b28 100644 --- a/packages/errors/src/i18n/locales/en-GB.ts +++ b/packages/errors/src/i18n/locales/en-GB.ts @@ -375,13 +375,9 @@ const ERROR_I18N_EN_GB_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Forwarded messages can't include content, embeds, attachments, or stickers.", "messages.forward_reference_requires_channel_and_message": "Forward message reference must include `channel_id` and `message_id`.", "messages.invalid_message_data": "Invalid message data.", - "messages.invalid_scheduled_message_payload": "Invalid scheduled message payload.", "messages.message_ids_cannot_be_empty": "`message_ids` can't be empty.", "messages.messages_array_required": "`messages` array is required and must not be empty.", "messages.messages_with_snapshots_cannot_be_edited": "Messages with snapshots can't be edited.", - "messages.scheduled_messages_max_30_days": "Scheduled messages can be at most 30 days in the future.", - "messages.scheduled_send_datetime_invalid": "Invalid date or time for scheduled send.", - "messages.scheduled_time_must_be_future": "Scheduled time must be in the future.", "messages.voice_messages_attachment_must_be_audio": "Voice message attachments must be audio files.", "messages.voice_messages_cannot_have_content": "Voice messages can't have text content.", "messages.voice_messages_cannot_have_embeds": "Voice messages can't have embeds.", diff --git a/packages/errors/src/i18n/locales/es-419.ts b/packages/errors/src/i18n/locales/es-419.ts index 3637ea783..8ead9160a 100644 --- a/packages/errors/src/i18n/locales/es-419.ts +++ b/packages/errors/src/i18n/locales/es-419.ts @@ -375,13 +375,9 @@ const ERROR_I18N_ES_419_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Los mensajes reenviados no pueden incluir contenido, elementos incrustados, archivos adjuntos o stickers.", "messages.forward_reference_requires_channel_and_message": "La referencia del mensaje reenviado debe incluir `channel_id` y `message_id`.", "messages.invalid_message_data": "Mensaje con datos no válidos.", - "messages.invalid_scheduled_message_payload": "Carga útil de mensaje programado no válida.", "messages.message_ids_cannot_be_empty": "`message_ids` no puede estar vacío.", "messages.messages_array_required": "El arreglo `messages` es requerido y no puede estar vacío.", "messages.messages_with_snapshots_cannot_be_edited": "Los mensajes con capturas no se pueden editar.", - "messages.scheduled_messages_max_30_days": "Los mensajes programados pueden enviarse hasta con 30 días de anticipación.", - "messages.scheduled_send_datetime_invalid": "La fecha o la hora del envío programado no es válida.", - "messages.scheduled_time_must_be_future": "La hora programada debe estar en el futuro.", "messages.voice_messages_attachment_must_be_audio": "Los archivos adjuntos de los mensajes de voz deben ser archivos de audio.", "messages.voice_messages_cannot_have_content": "Los mensajes de voz no pueden incluir texto.", "messages.voice_messages_cannot_have_embeds": "Los mensajes de voz no pueden tener embeds.", diff --git a/packages/errors/src/i18n/locales/es-ES.ts b/packages/errors/src/i18n/locales/es-ES.ts index d19a0a071..63de583d7 100644 --- a/packages/errors/src/i18n/locales/es-ES.ts +++ b/packages/errors/src/i18n/locales/es-ES.ts @@ -375,13 +375,9 @@ const ERROR_I18N_ES_ES_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Los mensajes reenviados no pueden incluir contenido, elementos incrustados, archivos adjuntos o stickers.", "messages.forward_reference_requires_channel_and_message": "La referencia del mensaje reenviado debe incluir `channel_id` y `message_id`.", "messages.invalid_message_data": "Datos de mensaje no válidos.", - "messages.invalid_scheduled_message_payload": "Carga útil de mensaje programado no válida.", "messages.message_ids_cannot_be_empty": "`message_ids` no puede estar vacío.", "messages.messages_array_required": "El array `messages` es necesario y no puede estar vacío.", "messages.messages_with_snapshots_cannot_be_edited": "Los mensajes con instantáneas no se pueden editar.", - "messages.scheduled_messages_max_30_days": "Los mensajes programados pueden ser como máximo 30 días en el futuro.", - "messages.scheduled_send_datetime_invalid": "Fecha u hora no válidas para el envío programado.", - "messages.scheduled_time_must_be_future": "La hora programada debe ser en el futuro.", "messages.voice_messages_attachment_must_be_audio": "Los adjuntos de los mensajes de voz deben ser archivos de audio.", "messages.voice_messages_cannot_have_content": "Los mensajes de voz no pueden tener contenido de texto.", "messages.voice_messages_cannot_have_embeds": "Los mensajes de voz no pueden tener embeds.", diff --git a/packages/errors/src/i18n/locales/fi.ts b/packages/errors/src/i18n/locales/fi.ts index 0bb0622b9..3c2184780 100644 --- a/packages/errors/src/i18n/locales/fi.ts +++ b/packages/errors/src/i18n/locales/fi.ts @@ -375,13 +375,9 @@ const ERROR_I18N_FI_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Edelleenlähetetyt viestit eivät voi sisältää sisältöä, upotuksia, liitteitä tai tarroja.", "messages.forward_reference_requires_channel_and_message": "Edelleenlähetetyn viestin viitteen on sisällettävä `channel_id` ja `message_id`.", "messages.invalid_message_data": "Virheellinen viestidata.", - "messages.invalid_scheduled_message_payload": "Virheellinen ajoitetun viestin hyötykuorma.", "messages.message_ids_cannot_be_empty": "`message_ids` ei voi olla tyhjä.", "messages.messages_array_required": "`messages`-taulukko on pakollinen eikä se saa olla tyhjä.", "messages.messages_with_snapshots_cannot_be_edited": "Viestit, joissa on tilannekuvia, eivät ole muokattavissa.", - "messages.scheduled_messages_max_30_days": "Ajoitetut viestit voivat olla enintään 30 päivän päässä tulevaisuudessa.", - "messages.scheduled_send_datetime_invalid": "Ajoitetun lähetyksen päivämäärä tai aika on virheellinen.", - "messages.scheduled_time_must_be_future": "Ajoitetun ajan on oltava tulevaisuudessa.", "messages.voice_messages_attachment_must_be_audio": "Ääniviestin liitteiden täytyy olla äänitiedostoja.", "messages.voice_messages_cannot_have_content": "Ääniviesteissä ei voi olla tekstisisältöä.", "messages.voice_messages_cannot_have_embeds": "Ääniviesteissä ei voi olla upotteita.", diff --git a/packages/errors/src/i18n/locales/fr.ts b/packages/errors/src/i18n/locales/fr.ts index ecaa89210..3cf6daf9c 100644 --- a/packages/errors/src/i18n/locales/fr.ts +++ b/packages/errors/src/i18n/locales/fr.ts @@ -375,13 +375,9 @@ const ERROR_I18N_FR_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Les messages transférés ne peuvent pas inclure de contenu, d'éléments intégrés, de pièces jointes ou de stickers.", "messages.forward_reference_requires_channel_and_message": "La référence du message à transférer doit inclure `channel_id` et `message_id`.", "messages.invalid_message_data": "Données de message non valides.", - "messages.invalid_scheduled_message_payload": "Contenu du message programmé invalide.", "messages.message_ids_cannot_be_empty": "Le champ `message_ids` ne peut pas être vide.", "messages.messages_array_required": "Le tableau `messages` est obligatoire et ne doit pas être vide.", "messages.messages_with_snapshots_cannot_be_edited": "Les messages avec des captures d'écran ne peuvent pas être modifiés.", - "messages.scheduled_messages_max_30_days": "Les messages programmés ne peuvent pas être planifiés plus de 30 jours à l'avance.", - "messages.scheduled_send_datetime_invalid": "Date ou heure incorrecte pour l'envoi programmé.", - "messages.scheduled_time_must_be_future": "L'heure programmée doit être dans le futur.", "messages.voice_messages_attachment_must_be_audio": "Les pièces jointes des messages vocaux doivent être des fichiers audio.", "messages.voice_messages_cannot_have_content": "Les messages vocaux ne peuvent pas avoir de contenu textuel.", "messages.voice_messages_cannot_have_embeds": "Les messages vocaux ne peuvent pas avoir d'intégrations.", diff --git a/packages/errors/src/i18n/locales/he.ts b/packages/errors/src/i18n/locales/he.ts index d7d0663cb..b117e6c8b 100644 --- a/packages/errors/src/i18n/locales/he.ts +++ b/packages/errors/src/i18n/locales/he.ts @@ -375,13 +375,9 @@ const ERROR_I18N_HE_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "הודעות מועברות לא יכולות לכלול תוכן, הטמעות, קבצים מצורפים או סטיקרים.", "messages.forward_reference_requires_channel_and_message": "הפניית הודעה מועברת צריכה לכלול `channel_id` ו-`message_id`.", "messages.invalid_message_data": "נתוני הודעה לא חוקיים.", - "messages.invalid_scheduled_message_payload": "מטען נתונים שגוי עבור הודעה מתוזמנת.", "messages.message_ids_cannot_be_empty": "`message_ids` לא יכול להיות ריק.", "messages.messages_array_required": "מערך `messages` נדרש ולא יכול להיות ריק.", "messages.messages_with_snapshots_cannot_be_edited": "לא ניתן לערוך הודעות הכוללות צילומי מסך.", - "messages.scheduled_messages_max_30_days": "ניתן לתזמן הודעות ל-30 הימים הבאים לכל היותר.", - "messages.scheduled_send_datetime_invalid": "תאריך או שעה לא חוקיים לשליחה מתוזמנת.", - "messages.scheduled_time_must_be_future": "מועד השליחה חייב להיות עתידי.", "messages.voice_messages_attachment_must_be_audio": "קבצים מצורפים בהודעות קוליות חייבים להיות קובצי שמע.", "messages.voice_messages_cannot_have_content": "הודעות קוליות אינן יכולות לכלול תוכן טקסט.", "messages.voice_messages_cannot_have_embeds": "הודעות קוליות לא יכולות לכלול הטמעות.", diff --git a/packages/errors/src/i18n/locales/hi.ts b/packages/errors/src/i18n/locales/hi.ts index aa834196b..ea2465972 100644 --- a/packages/errors/src/i18n/locales/hi.ts +++ b/packages/errors/src/i18n/locales/hi.ts @@ -375,13 +375,9 @@ const ERROR_I18N_HI_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "फॉरवर्ड किए गए मैसेज में कंटेंट, एम्बेड, अटैचमेंट या स्टीकर शामिल नहीं हो सकते हैं।", "messages.forward_reference_requires_channel_and_message": "फॉरवर्ड मैसेज रेफरेंस में `channel_id` और `message_id` शामिल होने चाहिए।", "messages.invalid_message_data": "अमान्य मैसेज डेटा।", - "messages.invalid_scheduled_message_payload": "अमान्य शेड्यूल किया गया मैसेज पेलोड।", "messages.message_ids_cannot_be_empty": "`message_ids` खाली नहीं हो सकते।", "messages.messages_array_required": "`messages` एरे ज़रूरी है और खाली नहीं होना चाहिए।", "messages.messages_with_snapshots_cannot_be_edited": "स्नैपशॉट वाले मैसेज एडिट नहीं किए जा सकते हैं.", - "messages.scheduled_messages_max_30_days": "शेड्यूल किए गए मैसेज ज़्यादा से ज़्यादा 30 दिन आगे के हो सकते हैं.", - "messages.scheduled_send_datetime_invalid": "शेड्यूल किए गए सेंड के लिए अमान्य तारीख या समय.", - "messages.scheduled_time_must_be_future": "शेड्यूल किया गया समय भविष्य में होना चाहिए.", "messages.voice_messages_attachment_must_be_audio": "वॉइस मैसेज अटैचमेंट सिर्फ़ ऑडियो फाइल होने चाहिए.", "messages.voice_messages_cannot_have_content": "वॉइस मैसेज में टेक्स्ट कंटेंट नहीं हो सकता है.", "messages.voice_messages_cannot_have_embeds": "वॉइस मैसेज में एम्बेड नहीं हो सकते हैं.", diff --git a/packages/errors/src/i18n/locales/hr.ts b/packages/errors/src/i18n/locales/hr.ts index 5c031a430..39193824d 100644 --- a/packages/errors/src/i18n/locales/hr.ts +++ b/packages/errors/src/i18n/locales/hr.ts @@ -375,13 +375,9 @@ const ERROR_I18N_HR_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Proslijeđene poruke ne mogu sadržavati sadržaj, ugrađene objekte, privitke ili naljepnice.", "messages.forward_reference_requires_channel_and_message": "Referenca prosljeđene poruke mora sadržavati `channel_id` i `message_id`.", "messages.invalid_message_data": "Neispravni podaci poruke.", - "messages.invalid_scheduled_message_payload": "Neispravan payload zakazane poruke.", "messages.message_ids_cannot_be_empty": "`message_ids` ne može biti prazan.", "messages.messages_array_required": "Polje `messages` je obavezno i ne smije biti prazno.", "messages.messages_with_snapshots_cannot_be_edited": "Poruke sa snimkama ne mogu se uređivati.", - "messages.scheduled_messages_max_30_days": "Zakazane poruke mogu biti najviše 30 dana u budućnosti.", - "messages.scheduled_send_datetime_invalid": "Nevažeći datum ili vrijeme za zakazano slanje.", - "messages.scheduled_time_must_be_future": "Zakazano vrijeme mora biti u budućnosti.", "messages.voice_messages_attachment_must_be_audio": "Privitci glasovnih poruka moraju biti audio datoteke.", "messages.voice_messages_cannot_have_content": "Glasovne poruke ne mogu imati tekstualni sadržaj.", "messages.voice_messages_cannot_have_embeds": "Glasovne poruke ne mogu imati ugrađene elemente.", diff --git a/packages/errors/src/i18n/locales/hu.ts b/packages/errors/src/i18n/locales/hu.ts index 41ceda9eb..bcf4087e8 100644 --- a/packages/errors/src/i18n/locales/hu.ts +++ b/packages/errors/src/i18n/locales/hu.ts @@ -375,13 +375,9 @@ const ERROR_I18N_HU_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "A továbbított üzenetek nem tartalmazhatnak tartalmat, beágyazásokat, mellékleteket vagy matricákat.", "messages.forward_reference_requires_channel_and_message": "A továbbított üzenet hivatkozásának tartalmaznia kell a `channel_id` és a `message_id` azonosítót.", "messages.invalid_message_data": "Érvénytelen üzenetadatok.", - "messages.invalid_scheduled_message_payload": "Érvénytelen ütemezett üzenetcsomag.", "messages.message_ids_cannot_be_empty": "A `message_ids` nem maradhat üresen.", "messages.messages_array_required": "A `messages` tömb szükséges és nem maradhat üresen.", "messages.messages_with_snapshots_cannot_be_edited": "A pillanatkép-üzenetek nem szerkeszthetők.", - "messages.scheduled_messages_max_30_days": "Az ütemezett üzenetek legfeljebb 30 nappal későbbre küldhetők.", - "messages.scheduled_send_datetime_invalid": "Érvénytelen dátum vagy idő az ütemezett küldéshez.", - "messages.scheduled_time_must_be_future": "Az ütemezés időpontjának a jövőben kell lennie.", "messages.voice_messages_attachment_must_be_audio": "A hangüzenet mellékleteinek hangfájlokat kell tartalmazniuk.", "messages.voice_messages_cannot_have_content": "A hangüzenetek nem tartalmazhatnak szöveges tartalmat.", "messages.voice_messages_cannot_have_embeds": "A hangüzenetek nem tartalmazhatnak beágyazásokat.", diff --git a/packages/errors/src/i18n/locales/id.ts b/packages/errors/src/i18n/locales/id.ts index 8bd7abf29..91d5c77c0 100644 --- a/packages/errors/src/i18n/locales/id.ts +++ b/packages/errors/src/i18n/locales/id.ts @@ -375,13 +375,9 @@ const ERROR_I18N_ID_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Pesan yang diteruskan tidak bisa menyertakan konten, embed, lampiran, atau stiker.", "messages.forward_reference_requires_channel_and_message": "Referensi pesan terusan harus menyertakan `channel_id` dan `message_id`.", "messages.invalid_message_data": "Data pesan tidak valid.", - "messages.invalid_scheduled_message_payload": "Payload pesan terjadwal tidak valid.", "messages.message_ids_cannot_be_empty": "`message_ids` tidak boleh kosong.", "messages.messages_array_required": "Array `messages` diperlukan dan tidak boleh kosong.", "messages.messages_with_snapshots_cannot_be_edited": "Pesan dengan snapshot tidak dapat diedit.", - "messages.scheduled_messages_max_30_days": "Pesan terjadwal maksimal 30 hari ke depan.", - "messages.scheduled_send_datetime_invalid": "Tanggal atau waktu tidak valid untuk kirim terjadwal.", - "messages.scheduled_time_must_be_future": "Waktu terjadwal harus di waktu mendatang.", "messages.voice_messages_attachment_must_be_audio": "Lampiran pesan suara harus berupa file audio.", "messages.voice_messages_cannot_have_content": "Pesan suara tidak boleh memiliki konten teks.", "messages.voice_messages_cannot_have_embeds": "Pesan suara tidak boleh memiliki sematan.", diff --git a/packages/errors/src/i18n/locales/it.ts b/packages/errors/src/i18n/locales/it.ts index 6b0f86a7b..156430c9b 100644 --- a/packages/errors/src/i18n/locales/it.ts +++ b/packages/errors/src/i18n/locales/it.ts @@ -375,13 +375,9 @@ const ERROR_I18N_IT_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "I messaggi inoltrati non possono includere contenuti, embed, allegati o sticker.", "messages.forward_reference_requires_channel_and_message": "Il riferimento per l'inoltro del messaggio deve includere `channel_id` e `message_id`.", "messages.invalid_message_data": "Dati messaggio non validi.", - "messages.invalid_scheduled_message_payload": "Payload del messaggio programmato non valido.", "messages.message_ids_cannot_be_empty": "`message_ids` non deve essere vuoto.", "messages.messages_array_required": "L'array `messages` è obbligatorio e non deve essere vuoto.", "messages.messages_with_snapshots_cannot_be_edited": "I messaggi con istantanee non possono essere modificati.", - "messages.scheduled_messages_max_30_days": "I messaggi programmati possono essere programmati per un massimo di 30 giorni nel futuro.", - "messages.scheduled_send_datetime_invalid": "Data o ora non valide per l'invio programmato.", - "messages.scheduled_time_must_be_future": "L'ora programmata deve essere nel futuro.", "messages.voice_messages_attachment_must_be_audio": "Gli allegati dei messaggi vocali devono essere file audio.", "messages.voice_messages_cannot_have_content": "I messaggi vocali non possono contenere testo.", "messages.voice_messages_cannot_have_embeds": "I messaggi vocali non possono contenere embed.", diff --git a/packages/errors/src/i18n/locales/ja.ts b/packages/errors/src/i18n/locales/ja.ts index 5a9f2c408..af7a22ad5 100644 --- a/packages/errors/src/i18n/locales/ja.ts +++ b/packages/errors/src/i18n/locales/ja.ts @@ -375,13 +375,9 @@ const ERROR_I18N_JA_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "転送メッセージにはコンテンツ、埋め込み、添付ファイル、スタンプを含められません。", "messages.forward_reference_requires_channel_and_message": "転送メッセージの参照には`channel_id`と`message_id`を含める必要があります。", "messages.invalid_message_data": "メッセージデータが無効です。", - "messages.invalid_scheduled_message_payload": "スケジュール済みメッセージペイロードが無効です。", "messages.message_ids_cannot_be_empty": "`message_ids`は空にできません。", "messages.messages_array_required": "`messages`配列は必須であり、空にできません。", "messages.messages_with_snapshots_cannot_be_edited": "スナップショット付きメッセージは編集できません。", - "messages.scheduled_messages_max_30_days": "予約メッセージは30日後まで設定できます。", - "messages.scheduled_send_datetime_invalid": "予約送信の日時が無効です。", - "messages.scheduled_time_must_be_future": "予約時間は未来の日時を指定してください。", "messages.voice_messages_attachment_must_be_audio": "音声メッセージの添付ファイルは音声ファイルのみです。", "messages.voice_messages_cannot_have_content": "音声メッセージにテキストは含められません。", "messages.voice_messages_cannot_have_embeds": "音声メッセージに埋め込みは含められません。", diff --git a/packages/errors/src/i18n/locales/ko.ts b/packages/errors/src/i18n/locales/ko.ts index f853d8d68..882308803 100644 --- a/packages/errors/src/i18n/locales/ko.ts +++ b/packages/errors/src/i18n/locales/ko.ts @@ -375,13 +375,9 @@ const ERROR_I18N_KO_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "전달된 메시지에는 콘텐츠, 임베드, 첨부 파일 또는 스티커를 포함할 수 없어요.", "messages.forward_reference_requires_channel_and_message": "메시지 전달 참조에는 `channel_id`와 `message_id`가 포함되어야 해요.", "messages.invalid_message_data": "유효하지 않은 메시지 데이터예요.", - "messages.invalid_scheduled_message_payload": "유효하지 않은 예약 메시지 페이로드예요.", "messages.message_ids_cannot_be_empty": "`message_ids`는 비워둘 수 없어요.", "messages.messages_array_required": "`messages` 배열은 필수 항목이며 비워둘 수 없어요.", "messages.messages_with_snapshots_cannot_be_edited": "스냅샷이 있는 메시지는 수정할 수 없어요.", - "messages.scheduled_messages_max_30_days": "예약 메시지는 최대 30일 이후로 설정할 수 있어요.", - "messages.scheduled_send_datetime_invalid": "예약 전송 날짜 또는 시간이 유효하지 않아요.", - "messages.scheduled_time_must_be_future": "예약 시간은 미래여야 해요.", "messages.voice_messages_attachment_must_be_audio": "음성 메시지 첨부 파일은 오디오 파일이어야 해요.", "messages.voice_messages_cannot_have_content": "음성 메시지에는 텍스트 콘텐츠를 포함할 수 없어요.", "messages.voice_messages_cannot_have_embeds": "음성 메시지에는 임베드를 포함할 수 없어요.", diff --git a/packages/errors/src/i18n/locales/lt.ts b/packages/errors/src/i18n/locales/lt.ts index 5d86ffbc7..c717bf8c8 100644 --- a/packages/errors/src/i18n/locales/lt.ts +++ b/packages/errors/src/i18n/locales/lt.ts @@ -375,13 +375,9 @@ const ERROR_I18N_LT_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Persiųstose žinutėse negali būti turinio, įterptinių elementų, priedų ar lipdukų.", "messages.forward_reference_requires_channel_and_message": "Persiųstos žinutės nuoroda turi apimti `channel_id` ir `message_id`.", "messages.invalid_message_data": "Neteisingi žinutės duomenys.", - "messages.invalid_scheduled_message_payload": "Neteisingas suplanuotos žinutės turinys.", "messages.message_ids_cannot_be_empty": "`message_ids` negali būti tuščias.", "messages.messages_array_required": "Masyvas `messages` yra privalomas ir negali būti tuščias.", "messages.messages_with_snapshots_cannot_be_edited": "Žinučių su momentinėmis nuotraukomis negalima redaguoti.", - "messages.scheduled_messages_max_30_days": "Suplanuotos žinutės gali būti daugiausia 30 dienų į priekį.", - "messages.scheduled_send_datetime_invalid": "Neteisinga data arba laikas suplanuotam siuntimui.", - "messages.scheduled_time_must_be_future": "Suplanuotas laikas turi būti ateityje.", "messages.voice_messages_attachment_must_be_audio": "Balso žinučių priedai turi būti garso failai.", "messages.voice_messages_cannot_have_content": "Balso žinutės negali turėti tekstinio turinio.", "messages.voice_messages_cannot_have_embeds": "Balso žinutės negali turėti įterptinių elementų.", diff --git a/packages/errors/src/i18n/locales/nl.ts b/packages/errors/src/i18n/locales/nl.ts index 3330dbe33..5c177798d 100644 --- a/packages/errors/src/i18n/locales/nl.ts +++ b/packages/errors/src/i18n/locales/nl.ts @@ -375,13 +375,9 @@ const ERROR_I18N_NL_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Doorgestuurde berichten kunnen geen inhoud, embeds, bijlagen of stickers bevatten.", "messages.forward_reference_requires_channel_and_message": "De referentie van een doorgestuurd bericht moet `channel_id` en `message_id` bevatten.", "messages.invalid_message_data": "Ongeldige berichtgegevens.", - "messages.invalid_scheduled_message_payload": "Ongeldige payload voor gepland bericht.", "messages.message_ids_cannot_be_empty": "`message_ids` mag niet leeg zijn.", "messages.messages_array_required": "De `messages`-array is vereist en mag niet leeg zijn.", "messages.messages_with_snapshots_cannot_be_edited": "Berichten met snapshots kunnen niet worden bewerkt.", - "messages.scheduled_messages_max_30_days": "Geplande berichten kunnen maximaal 30 dagen in de toekomst zijn.", - "messages.scheduled_send_datetime_invalid": "Ongeldige datum of tijd voor gepland verzenden.", - "messages.scheduled_time_must_be_future": "De geplande tijd moet in de toekomst zijn.", "messages.voice_messages_attachment_must_be_audio": "Bijlagen bij spraakberichten moeten audiobestanden zijn.", "messages.voice_messages_cannot_have_content": "Spraakberichten kunnen geen tekstinhoud bevatten.", "messages.voice_messages_cannot_have_embeds": "Spraakberichten kunnen geen embeds bevatten.", diff --git a/packages/errors/src/i18n/locales/no.ts b/packages/errors/src/i18n/locales/no.ts index 5b12e48bb..0fed02675 100644 --- a/packages/errors/src/i18n/locales/no.ts +++ b/packages/errors/src/i18n/locales/no.ts @@ -375,13 +375,9 @@ const ERROR_I18N_NO_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Videresendte meldinger kan ikke inneholde innhold, innebygde elementer, vedlegg eller stickere.", "messages.forward_reference_requires_channel_and_message": "Videresendt meldingsreferanse må inkludere `channel_id` og `message_id`.", "messages.invalid_message_data": "Ugyldig meldingsdata.", - "messages.invalid_scheduled_message_payload": "Ugyldig nyttelast for planlagt melding.", "messages.message_ids_cannot_be_empty": "`message_ids` kan ikke være tomme.", "messages.messages_array_required": "Matrisen `messages` er påkrevd og kan ikke være tom.", "messages.messages_with_snapshots_cannot_be_edited": "Meldinger med skjermbilder kan ikke redigeres.", - "messages.scheduled_messages_max_30_days": "Planlagte meldinger kan være maksimalt 30 dager frem i tid.", - "messages.scheduled_send_datetime_invalid": "Ugyldig dato eller tid for planlagt sending.", - "messages.scheduled_time_must_be_future": "Planlagt tid må være i fremtiden.", "messages.voice_messages_attachment_must_be_audio": "Vedlegg til talemeldinger må være lydfiler.", "messages.voice_messages_cannot_have_content": "Talemeldinger kan ikke ha tekstinnhold.", "messages.voice_messages_cannot_have_embeds": "Talemeldinger kan ikke ha innebygde elementer.", diff --git a/packages/errors/src/i18n/locales/pl.ts b/packages/errors/src/i18n/locales/pl.ts index 929112cd5..25812d3ec 100644 --- a/packages/errors/src/i18n/locales/pl.ts +++ b/packages/errors/src/i18n/locales/pl.ts @@ -375,13 +375,9 @@ const ERROR_I18N_PL_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Przekazane wiadomości nie mogą zawierać treści, osadzonych elementów, załączników ani naklejek.", "messages.forward_reference_requires_channel_and_message": "Odwołanie do przekazanej wiadomości musi zawierać `channel_id` i `message_id`.", "messages.invalid_message_data": "Nieprawidłowe dane wiadomości.", - "messages.invalid_scheduled_message_payload": "Nieprawidłowy ładunek zaplanowanej wiadomości.", "messages.message_ids_cannot_be_empty": "`message_ids` nie może być puste.", "messages.messages_array_required": "Tablica `messages` jest wymagana i nie może być pusta.", "messages.messages_with_snapshots_cannot_be_edited": "Wiadomości z migawkami nie mogą być edytowane.", - "messages.scheduled_messages_max_30_days": "Zaplanowane wiadomości mogą być wysłane na maksymalnie 30 dni do przodu.", - "messages.scheduled_send_datetime_invalid": "Nieprawidłowa data lub godzina zaplanowanego wysłania.", - "messages.scheduled_time_must_be_future": "Zaplanowany czas musi dotyczyć przyszłości.", "messages.voice_messages_attachment_must_be_audio": "Załączniki wiadomości głosowych muszą być plikami audio.", "messages.voice_messages_cannot_have_content": "Wiadomości głosowe nie mogą zawierać treści tekstowych.", "messages.voice_messages_cannot_have_embeds": "Wiadomości głosowe nie mogą zawierać osadzonych elementów.", diff --git a/packages/errors/src/i18n/locales/pt-BR.ts b/packages/errors/src/i18n/locales/pt-BR.ts index 4f14b4db6..6d93eca19 100644 --- a/packages/errors/src/i18n/locales/pt-BR.ts +++ b/packages/errors/src/i18n/locales/pt-BR.ts @@ -375,13 +375,9 @@ const ERROR_I18N_PT_BR_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Mensagens encaminhadas não podem incluir conteúdo, elementos incorporados, anexos ou figurinhas.", "messages.forward_reference_requires_channel_and_message": "A referência de encaminhamento de mensagem precisa incluir `channel_id` e `message_id`.", "messages.invalid_message_data": "Dados da mensagem inválidos.", - "messages.invalid_scheduled_message_payload": "Payload inválido da mensagem agendada.", "messages.message_ids_cannot_be_empty": "`message_ids` não pode ser vazio.", "messages.messages_array_required": "O array `messages` é necessário e não pode ser vazio.", "messages.messages_with_snapshots_cannot_be_edited": "Mensagens com snapshots não podem ser editadas.", - "messages.scheduled_messages_max_30_days": "Mensagens agendadas podem ser para no máximo 30 dias no futuro.", - "messages.scheduled_send_datetime_invalid": "Data ou hora inválida para o envio agendado.", - "messages.scheduled_time_must_be_future": "O horário agendado deve ser no futuro.", "messages.voice_messages_attachment_must_be_audio": "Anexos de mensagens de voz precisam ser arquivos de áudio.", "messages.voice_messages_cannot_have_content": "Mensagens de voz não podem conter texto.", "messages.voice_messages_cannot_have_embeds": "Mensagens de voz não podem ter embeds.", diff --git a/packages/errors/src/i18n/locales/ro.ts b/packages/errors/src/i18n/locales/ro.ts index 057ea583f..84bf37c52 100644 --- a/packages/errors/src/i18n/locales/ro.ts +++ b/packages/errors/src/i18n/locales/ro.ts @@ -375,13 +375,9 @@ const ERROR_I18N_RO_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Mesajele redirecționate nu pot include conținut, încorporări, atașamente sau stickere.", "messages.forward_reference_requires_channel_and_message": "Referința mesajului redirecționat trebuie să includă `channel_id` și `message_id`.", "messages.invalid_message_data": "Date mesaj invalide.", - "messages.invalid_scheduled_message_payload": "Sarcină utilă mesaj programat invalidă.", "messages.message_ids_cannot_be_empty": "`message_ids` nu poate fi gol.", "messages.messages_array_required": "Câmpul `messages` este obligatoriu și nu trebuie să fie gol.", "messages.messages_with_snapshots_cannot_be_edited": "Mesajele cu instantanee nu pot fi editate.", - "messages.scheduled_messages_max_30_days": "Mesajele programate pot fi programate pentru cel mult 30 de zile în avans.", - "messages.scheduled_send_datetime_invalid": "Data sau ora sunt invalide pentru programarea trimiterii.", - "messages.scheduled_time_must_be_future": "Ora programată trebuie să fie o oră viitoare.", "messages.voice_messages_attachment_must_be_audio": "Atașamentele mesajelor vocale trebuie să fie fișiere audio.", "messages.voice_messages_cannot_have_content": "Mesajele vocale nu pot avea conținut text.", "messages.voice_messages_cannot_have_embeds": "Mesajele vocale nu pot avea embed-uri.", diff --git a/packages/errors/src/i18n/locales/ru.ts b/packages/errors/src/i18n/locales/ru.ts index 7b1ea0597..5a770ab50 100644 --- a/packages/errors/src/i18n/locales/ru.ts +++ b/packages/errors/src/i18n/locales/ru.ts @@ -375,13 +375,9 @@ const ERROR_I18N_RU_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Пересланные сообщения не могут содержать контент, встраиваемые элементы, вложения или стикеры.", "messages.forward_reference_requires_channel_and_message": "Ссылка на пересылаемое сообщение должна включать `channel_id` и `message_id`.", "messages.invalid_message_data": "Неверные данные сообщения.", - "messages.invalid_scheduled_message_payload": "Недействительная нагрузка запланированного сообщения.", "messages.message_ids_cannot_be_empty": "`message_ids` не может быть пустым.", "messages.messages_array_required": "Массив `messages` обязателен и не должен быть пустым.", "messages.messages_with_snapshots_cannot_be_edited": "Сообщения со снимками нельзя редактировать.", - "messages.scheduled_messages_max_30_days": "Запланированные сообщения можно отправить не более чем через 30 дней.", - "messages.scheduled_send_datetime_invalid": "Неверная дата или время для запланированной отправки.", - "messages.scheduled_time_must_be_future": "Время отправки должно быть в будущем.", "messages.voice_messages_attachment_must_be_audio": "Вложения голосовых сообщений должны быть аудиофайлами.", "messages.voice_messages_cannot_have_content": "Голосовые сообщения не могут содержать текст.", "messages.voice_messages_cannot_have_embeds": "Голосовые сообщения не могут содержать встроенный контент.", diff --git a/packages/errors/src/i18n/locales/sv-SE.ts b/packages/errors/src/i18n/locales/sv-SE.ts index f3dbbb57b..7046fd179 100644 --- a/packages/errors/src/i18n/locales/sv-SE.ts +++ b/packages/errors/src/i18n/locales/sv-SE.ts @@ -375,13 +375,9 @@ const ERROR_I18N_SV_SE_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Vidarebefordrade meddelanden kan inte innehålla innehåll, inbäddningar, bilagor eller stickers.", "messages.forward_reference_requires_channel_and_message": "Vidarebefordrad meddelandereferens måste inkludera `channel_id` och `message_id`.", "messages.invalid_message_data": "Ogiltig meddelandedata.", - "messages.invalid_scheduled_message_payload": "Ogiltig nyttolast för schemalagt meddelande.", "messages.message_ids_cannot_be_empty": "`message_ids` kan inte vara tomma.", "messages.messages_array_required": "`messages`-arrayen krävs och får inte vara tom.", "messages.messages_with_snapshots_cannot_be_edited": "Meddelanden med ögonblicksbilder kan inte redigeras.", - "messages.scheduled_messages_max_30_days": "Schemalagda meddelanden kan vara högst 30 dagar framåt.", - "messages.scheduled_send_datetime_invalid": "Ogiltigt datum eller tid för schemalagd sändning.", - "messages.scheduled_time_must_be_future": "Schemalagd tid måste vara framåt i tiden.", "messages.voice_messages_attachment_must_be_audio": "Röstmeddelandebilagor måste vara ljudfiler.", "messages.voice_messages_cannot_have_content": "Röstmeddelanden kan inte ha textinnehåll.", "messages.voice_messages_cannot_have_embeds": "Röstmeddelanden kan inte ha inbäddningar.", diff --git a/packages/errors/src/i18n/locales/th.ts b/packages/errors/src/i18n/locales/th.ts index 6147ddc88..6881c7050 100644 --- a/packages/errors/src/i18n/locales/th.ts +++ b/packages/errors/src/i18n/locales/th.ts @@ -375,13 +375,9 @@ const ERROR_I18N_TH_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "ข้อความที่ส่งต่อไม่สามารถมีเนื้อหา, การฝัง, ไฟล์แนบ หรือสติกเกอร์ได้", "messages.forward_reference_requires_channel_and_message": "การอ้างอิงข้อความที่ส่งต่อต้องมี `channel_id` และ `message_id`", "messages.invalid_message_data": "ข้อมูลข้อความไม่ถูกต้อง", - "messages.invalid_scheduled_message_payload": "เพย์โหลดข้อความที่กำหนดเวลาไม่ถูกต้อง", "messages.message_ids_cannot_be_empty": "`message_ids` ต้องไม่ว่างเปล่า", "messages.messages_array_required": "ต้องระบุอาร์เรย์ `messages` และต้องไม่ว่างเปล่า", "messages.messages_with_snapshots_cannot_be_edited": "ข้อความที่มีสแนปช็อตไม่สามารถแก้ไขได้", - "messages.scheduled_messages_max_30_days": "ข้อความที่กำหนดเวลาสามารถตั้งล่วงหน้าได้สูงสุด 30 วัน", - "messages.scheduled_send_datetime_invalid": "วันที่หรือเวลาสำหรับการส่งตามกำหนดเวลาไม่ถูกต้อง", - "messages.scheduled_time_must_be_future": "เวลาที่กำหนดต้องเป็นเวลาในอนาคต", "messages.voice_messages_attachment_must_be_audio": "ไฟล์แนบข้อความเสียงต้องเป็นไฟล์เสียง", "messages.voice_messages_cannot_have_content": "ข้อความเสียงไม่สามารถมีเนื้อหาข้อความได้", "messages.voice_messages_cannot_have_embeds": "ข้อความเสียงไม่สามารถมีการฝังได้", diff --git a/packages/errors/src/i18n/locales/tr.ts b/packages/errors/src/i18n/locales/tr.ts index 55dacd1b5..c6d6e9632 100644 --- a/packages/errors/src/i18n/locales/tr.ts +++ b/packages/errors/src/i18n/locales/tr.ts @@ -375,13 +375,9 @@ const ERROR_I18N_TR_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "İletilen mesajlar içerik, gömülü içerik, ekler veya çıkartma içeremez.", "messages.forward_reference_requires_channel_and_message": "İletilen mesaj referansı `channel_id` ve `message_id` içermelidir.", "messages.invalid_message_data": "Geçersiz mesaj verisi.", - "messages.invalid_scheduled_message_payload": "Geçersiz zamanlanmış mesaj yükü.", "messages.message_ids_cannot_be_empty": "`message_ids` boş olamaz.", "messages.messages_array_required": "`messages` dizisi gereklidir ve boş olamaz.", "messages.messages_with_snapshots_cannot_be_edited": "Anlık görüntülü mesajlar düzenlenemez.", - "messages.scheduled_messages_max_30_days": "Zamanlanmış mesajlar en fazla 30 gün ileriye ayarlanabilir.", - "messages.scheduled_send_datetime_invalid": "Zamanlanmış gönderim için geçersiz tarih veya saat.", - "messages.scheduled_time_must_be_future": "Zamanlanmış gönderim gelecekte bir zaman olmalı.", "messages.voice_messages_attachment_must_be_audio": "Sesli mesaj ekleri ses dosyası türünde olmalı.", "messages.voice_messages_cannot_have_content": "Sesli mesajlar metin içeriği içeremez.", "messages.voice_messages_cannot_have_embeds": "Sesli mesajlar yerleştirme içeremez.", diff --git a/packages/errors/src/i18n/locales/uk.ts b/packages/errors/src/i18n/locales/uk.ts index e33ac8381..63e4cad3e 100644 --- a/packages/errors/src/i18n/locales/uk.ts +++ b/packages/errors/src/i18n/locales/uk.ts @@ -375,13 +375,9 @@ const ERROR_I18N_UK_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Переслані повідомлення не можуть містити вміст, вбудовані елементи, вкладення або стікери.", "messages.forward_reference_requires_channel_and_message": "Посилання на переслане повідомлення має містити `channel_id` та `message_id`.", "messages.invalid_message_data": "Недійсні дані повідомлення.", - "messages.invalid_scheduled_message_payload": "Недійсний вміст запланованого повідомлення.", "messages.message_ids_cannot_be_empty": "`message_ids` не можуть бути порожніми.", "messages.messages_array_required": "Масив `messages` обов'язковий і не може бути порожнім.", "messages.messages_with_snapshots_cannot_be_edited": "Повідомлення зі знімками не можна редагувати.", - "messages.scheduled_messages_max_30_days": "Заплановані повідомлення можуть бути щонайбільше на 30 днів уперед.", - "messages.scheduled_send_datetime_invalid": "Неправильна дата або час для запланованого надсилання.", - "messages.scheduled_time_must_be_future": "Запланований час має бути в майбутньому.", "messages.voice_messages_attachment_must_be_audio": "Вкладення голосових повідомлень повинні бути аудіофайлами.", "messages.voice_messages_cannot_have_content": "Голосові повідомлення не можуть містити текст.", "messages.voice_messages_cannot_have_embeds": "Голосові повідомлення не можуть містити вбудовані елементи.", diff --git a/packages/errors/src/i18n/locales/vi.ts b/packages/errors/src/i18n/locales/vi.ts index 75a41d46b..0e0481a2d 100644 --- a/packages/errors/src/i18n/locales/vi.ts +++ b/packages/errors/src/i18n/locales/vi.ts @@ -375,13 +375,9 @@ const ERROR_I18N_VI_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "Tin nhắn đã chuyển tiếp không được chứa nội dung, nhúng, tệp đính kèm hoặc sticker.", "messages.forward_reference_requires_channel_and_message": "Tham chiếu tin nhắn chuyển tiếp phải bao gồm `channel_id` và `message_id`.", "messages.invalid_message_data": "Dữ liệu tin nhắn không hợp lệ.", - "messages.invalid_scheduled_message_payload": "Payload của tin nhắn hẹn giờ không hợp lệ.", "messages.message_ids_cannot_be_empty": "`message_ids` không được trống.", "messages.messages_array_required": "Mảng `messages` là bắt buộc và không được trống.", "messages.messages_with_snapshots_cannot_be_edited": "Tin nhắn có snapshot không thể chỉnh sửa.", - "messages.scheduled_messages_max_30_days": "Tin nhắn đã lên lịch có thể tối đa 30 ngày tới.", - "messages.scheduled_send_datetime_invalid": "Ngày hoặc giờ không hợp lệ để gửi theo lịch.", - "messages.scheduled_time_must_be_future": "Thời gian đã lên lịch phải ở tương lai.", "messages.voice_messages_attachment_must_be_audio": "Tệp đính kèm tin nhắn thoại phải là tệp âm thanh.", "messages.voice_messages_cannot_have_content": "Tin nhắn thoại không thể có nội dung văn bản.", "messages.voice_messages_cannot_have_embeds": "Tin nhắn thoại không thể có nhúng.", diff --git a/packages/errors/src/i18n/locales/zh-CN.ts b/packages/errors/src/i18n/locales/zh-CN.ts index 64d4819fd..906cdf07a 100644 --- a/packages/errors/src/i18n/locales/zh-CN.ts +++ b/packages/errors/src/i18n/locales/zh-CN.ts @@ -375,13 +375,9 @@ const ERROR_I18N_ZH_CN_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "转发消息不能包含内容、嵌入内容、附件或贴纸。", "messages.forward_reference_requires_channel_and_message": "转发消息引用必须包含 `channel_id` 和 `message_id`。", "messages.invalid_message_data": "消息数据无效。", - "messages.invalid_scheduled_message_payload": "无效的定时消息负载。", "messages.message_ids_cannot_be_empty": "`message_ids` 不能为空。", "messages.messages_array_required": "`messages` 数组是必需的,且不能为空。", "messages.messages_with_snapshots_cannot_be_edited": "带有快照的消息无法编辑。", - "messages.scheduled_messages_max_30_days": "定时消息最多只能安排在未来 30 天内。", - "messages.scheduled_send_datetime_invalid": "定时发送的日期或时间不正确。", - "messages.scheduled_time_must_be_future": "定时时间必须在未来。", "messages.voice_messages_attachment_must_be_audio": "语音消息附件必须是音频文件。", "messages.voice_messages_cannot_have_content": "语音消息不能包含文本内容。", "messages.voice_messages_cannot_have_embeds": "语音消息不能包含嵌入内容。", diff --git a/packages/errors/src/i18n/locales/zh-TW.ts b/packages/errors/src/i18n/locales/zh-TW.ts index 8421fa6f3..057817eaf 100644 --- a/packages/errors/src/i18n/locales/zh-TW.ts +++ b/packages/errors/src/i18n/locales/zh-TW.ts @@ -375,13 +375,9 @@ const ERROR_I18N_ZH_TW_MESSAGES = defineErrorI18nLocaleMessages({ "messages.forward_messages_cannot_include_extra_content": "轉寄的訊息不能包含內容、嵌入、附件或貼圖。", "messages.forward_reference_requires_channel_and_message": "轉寄訊息參考必須包含 `channel_id` 和 `message_id`。", "messages.invalid_message_data": "無效的訊息資料。", - "messages.invalid_scheduled_message_payload": "無效的排程訊息內容。", "messages.message_ids_cannot_be_empty": "`message_ids` 不能為空。", "messages.messages_array_required": "`messages` 陣列為必填,且不能為空。", "messages.messages_with_snapshots_cannot_be_edited": "包含快照的訊息無法編輯。", - "messages.scheduled_messages_max_30_days": "排程訊息最多只能排程到未來 30 天。", - "messages.scheduled_send_datetime_invalid": "排程傳送的日期或時間無效。", - "messages.scheduled_time_must_be_future": "排程時間必須在未來。", "messages.voice_messages_attachment_must_be_audio": "語音訊息附件必須是音訊檔案。", "messages.voice_messages_cannot_have_content": "語音訊息不能包含文字內容。", "messages.voice_messages_cannot_have_embeds": "語音訊息不能包含嵌入內容。", diff --git a/packages/errors/src/i18n/weblate/locales/ar.json b/packages/errors/src/i18n/weblate/locales/ar.json index 5634725e2..c8a1fce03 100644 --- a/packages/errors/src/i18n/weblate/locales/ar.json +++ b/packages/errors/src/i18n/weblate/locales/ar.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "لا يمكن أن تحتوي الرسائل المعاد توجيهها على محتوى، تضمينات، مرفقات، أو ملصقات.", "messages.forward_reference_requires_channel_and_message": "يجب أن يتضمن مرجع الرسالة المعاد توجيهها كلاً من `channel_id` و `message_id`.", "messages.invalid_message_data": "بيانات الرسالة غير صحيحة.", - "messages.invalid_scheduled_message_payload": "حمولة الرسالة المجدولة غير صالحة.", "messages.message_ids_cannot_be_empty": "لا يمكن أن تكون `message_ids` فارغة.", "messages.messages_array_required": "مصفوفة `messages` مطلوبة ولا يمكن أن تكون فارغة.", "messages.messages_with_snapshots_cannot_be_edited": "لا يمكن تعديل الرسائل التي تحتوي على لقطات.", - "messages.scheduled_messages_max_30_days": "يمكن جدولة الرسائل لمدة أقصاها 30 يومًا فقط.", - "messages.scheduled_send_datetime_invalid": "تاريخ أو وقت غير صحيح للإرسال المجدول.", - "messages.scheduled_time_must_be_future": "يجب أن يكون الوقت المحدد في المستقبل.", "messages.voice_messages_attachment_must_be_audio": "يجب أن تكون مرفقات الرسائل الصوتية ملفات صوتية.", "messages.voice_messages_cannot_have_content": "لا يمكن للرسائل الصوتية أن تحتوي على نص.", "messages.voice_messages_cannot_have_embeds": "لا يمكن للرسائل الصوتية أن تحتوي على تضمينات.", diff --git a/packages/errors/src/i18n/weblate/locales/bg.json b/packages/errors/src/i18n/weblate/locales/bg.json index f10702e8b..45a0c39a2 100644 --- a/packages/errors/src/i18n/weblate/locales/bg.json +++ b/packages/errors/src/i18n/weblate/locales/bg.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Препратените съобщения не могат да съдържат съдържание, вградено съдържание, прикачени файлове или стикери.", "messages.forward_reference_requires_channel_and_message": "Препратката към съобщението трябва да съдържа `channel_id` и `message_id`.", "messages.invalid_message_data": "Невалидни данни на съобщението.", - "messages.invalid_scheduled_message_payload": "Невалидни данни за планирано съобщение.", "messages.message_ids_cannot_be_empty": "`message_ids` не могат да бъдат празни.", "messages.messages_array_required": "Масивът `messages` е задължителен и не може да е празен.", "messages.messages_with_snapshots_cannot_be_edited": "Съобщения с моментни снимки не могат да бъдат редактирани.", - "messages.scheduled_messages_max_30_days": "Планираните съобщения могат да бъдат най-много 30 дни в бъдещето.", - "messages.scheduled_send_datetime_invalid": "Невалидна дата или час за изпращане по график.", - "messages.scheduled_time_must_be_future": "Часът за изпращане трябва да е в бъдещето.", "messages.voice_messages_attachment_must_be_audio": "Прикачените файлове към гласови съобщения трябва да бъдат аудио файлове.", "messages.voice_messages_cannot_have_content": "Гласовите съобщения не могат да имат текстово съдържание.", "messages.voice_messages_cannot_have_embeds": "Гласовите съобщения не могат да съдържат вградено съдържание.", diff --git a/packages/errors/src/i18n/weblate/locales/cs.json b/packages/errors/src/i18n/weblate/locales/cs.json index ba55f147e..98827f4a3 100644 --- a/packages/errors/src/i18n/weblate/locales/cs.json +++ b/packages/errors/src/i18n/weblate/locales/cs.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Přeposlané zprávy nemohou obsahovat obsah, vložené prvky, přílohy ani samolepky.", "messages.forward_reference_requires_channel_and_message": "Odkaz na přeposlanou zprávu musí obsahovat `channel_id` a `message_id`.", "messages.invalid_message_data": "Neplatná data u zprávy.", - "messages.invalid_scheduled_message_payload": "Neplatná data plánované zprávy.", "messages.message_ids_cannot_be_empty": "`message_ids` nemohou být prázdné.", "messages.messages_array_required": "Pole `messages` je nutné a nesmí být prázdné.", "messages.messages_with_snapshots_cannot_be_edited": "Zprávy se snímky nelze upravit.", - "messages.scheduled_messages_max_30_days": "Plánované zprávy mohou být maximálně 30 dní do budoucna.", - "messages.scheduled_send_datetime_invalid": "Neplatné datum nebo čas pro plánované odeslání.", - "messages.scheduled_time_must_be_future": "Plánovaný čas musí být v budoucnu.", "messages.voice_messages_attachment_must_be_audio": "Přílohy hlasových zpráv musí být zvukové soubory.", "messages.voice_messages_cannot_have_content": "Hlasové zprávy nemohou mít text.", "messages.voice_messages_cannot_have_embeds": "Hlasové zprávy nemohou mít vložené prvky.", diff --git a/packages/errors/src/i18n/weblate/locales/da.json b/packages/errors/src/i18n/weblate/locales/da.json index c2745aecc..ccaa08dc1 100644 --- a/packages/errors/src/i18n/weblate/locales/da.json +++ b/packages/errors/src/i18n/weblate/locales/da.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Videresendte beskeder kan ikke indeholde indhold, indlejringer, vedhæftede filer eller stickers.", "messages.forward_reference_requires_channel_and_message": "Reference for videresendte beskeder skal indeholde `channel_id` og `message_id`.", "messages.invalid_message_data": "Ugyldige beskeddata.", - "messages.invalid_scheduled_message_payload": "Ugyldig planlagt besked-nyttelast.", "messages.message_ids_cannot_be_empty": "`message_ids` må ikke være tomme.", "messages.messages_array_required": "`messages`-arrayet skal være angivet og må ikke være tomt.", "messages.messages_with_snapshots_cannot_be_edited": "Beskeder med snapshots kan ikke redigeres.", - "messages.scheduled_messages_max_30_days": "Planlagte beskeder kan højst være 30 dage ude i fremtiden.", - "messages.scheduled_send_datetime_invalid": "Ugyldig dato eller tidspunkt for planlagt afsendelse.", - "messages.scheduled_time_must_be_future": "Planlagt tidspunkt skal være i fremtiden.", "messages.voice_messages_attachment_must_be_audio": "Vedhæftede filer til talebeskeder skal være lydfiler.", "messages.voice_messages_cannot_have_content": "Talebeskeder kan ikke have tekstindhold.", "messages.voice_messages_cannot_have_embeds": "Talebeskeder kan ikke have indlejringer.", diff --git a/packages/errors/src/i18n/weblate/locales/de.json b/packages/errors/src/i18n/weblate/locales/de.json index 0e8209862..06c8d398c 100644 --- a/packages/errors/src/i18n/weblate/locales/de.json +++ b/packages/errors/src/i18n/weblate/locales/de.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Weitergeleitete Nachrichten dürfen keine Inhalte, Embeds, Anhänge oder Sticker enthalten.", "messages.forward_reference_requires_channel_and_message": "Die Referenz für weitergeleitete Nachrichten muss `channel_id` und `message_id` enthalten.", "messages.invalid_message_data": "Ungültige Nachrichtendaten.", - "messages.invalid_scheduled_message_payload": "Ungültige Payload für geplante Nachrichten.", "messages.message_ids_cannot_be_empty": "`message_ids` darf nicht leer sein.", "messages.messages_array_required": "Das `messages`-Array ist erforderlich und darf nicht leer sein.", "messages.messages_with_snapshots_cannot_be_edited": "Nachrichten mit Snapshots können nicht bearbeitet werden.", - "messages.scheduled_messages_max_30_days": "Geplante Nachrichten können maximal 30 Tage in die Zukunft reichen.", - "messages.scheduled_send_datetime_invalid": "Ungültiges Datum oder ungültige Uhrzeit für den geplanten Versand.", - "messages.scheduled_time_must_be_future": "Die geplante Zeit muss in der Zukunft sein.", "messages.voice_messages_attachment_must_be_audio": "Sprachnachrichten-Anhänge müssen Audiodateien sein.", "messages.voice_messages_cannot_have_content": "Sprachnachrichten können keinen Text enthalten.", "messages.voice_messages_cannot_have_embeds": "Sprachnachrichten können keine Einbettungen enthalten.", diff --git a/packages/errors/src/i18n/weblate/locales/el.json b/packages/errors/src/i18n/weblate/locales/el.json index b8beb2953..9bc204b43 100644 --- a/packages/errors/src/i18n/weblate/locales/el.json +++ b/packages/errors/src/i18n/weblate/locales/el.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Τα προωθημένα μηνύματα δεν μπορούν να περιλαμβάνουν περιεχόμενο, ενσωματώσεις, συνημμένα ή sticker.", "messages.forward_reference_requires_channel_and_message": "Η αναφορά προώθησης μηνύματος πρέπει να περιλαμβάνει `channel_id` και `message_id`.", "messages.invalid_message_data": "Μη έγκυρα δεδομένα μηνύματος.", - "messages.invalid_scheduled_message_payload": "Μη έγκυρο ωφέλιμο φορτίο προγραμματισμένου μηνύματος.", "messages.message_ids_cannot_be_empty": "Το `message_ids` δεν μπορεί να είναι άδειο.", "messages.messages_array_required": "Ο πίνακας `messages` απαιτείται και δεν μπορεί να είναι άδειος.", "messages.messages_with_snapshots_cannot_be_edited": "Δεν μπορείς να επεξεργαστείς μηνύματα με στιγμιότυπα.", - "messages.scheduled_messages_max_30_days": "Τα προγραμματισμένα μηνύματα μπορούν να οριστούν έως και 30 ημέρες από τώρα.", - "messages.scheduled_send_datetime_invalid": "Άκυρη ημερομηνία ή ώρα για προγραμματισμένη αποστολή.", - "messages.scheduled_time_must_be_future": "Η προγραμματισμένη ώρα πρέπει να είναι στο μέλλον.", "messages.voice_messages_attachment_must_be_audio": "Τα συνημμένα φωνητικά μηνύματα πρέπει να είναι αρχεία ήχου.", "messages.voice_messages_cannot_have_content": "Τα φωνητικά μηνύματα δεν μπορούν να έχουν κείμενο.", "messages.voice_messages_cannot_have_embeds": "Τα φωνητικά μηνύματα δεν μπορούν να έχουν ενσωματώσεις.", diff --git a/packages/errors/src/i18n/weblate/locales/en-GB.json b/packages/errors/src/i18n/weblate/locales/en-GB.json index 5c52ab651..4ca14170b 100644 --- a/packages/errors/src/i18n/weblate/locales/en-GB.json +++ b/packages/errors/src/i18n/weblate/locales/en-GB.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Forwarded messages can't include content, embeds, attachments, or stickers.", "messages.forward_reference_requires_channel_and_message": "Forward message reference must include `channel_id` and `message_id`.", "messages.invalid_message_data": "Invalid message data.", - "messages.invalid_scheduled_message_payload": "Invalid scheduled message payload.", "messages.message_ids_cannot_be_empty": "`message_ids` can't be empty.", "messages.messages_array_required": "`messages` array is required and must not be empty.", "messages.messages_with_snapshots_cannot_be_edited": "Messages with snapshots can't be edited.", - "messages.scheduled_messages_max_30_days": "Scheduled messages can be at most 30 days in the future.", - "messages.scheduled_send_datetime_invalid": "Invalid date or time for scheduled send.", - "messages.scheduled_time_must_be_future": "Scheduled time must be in the future.", "messages.voice_messages_attachment_must_be_audio": "Voice message attachments must be audio files.", "messages.voice_messages_cannot_have_content": "Voice messages can't have text content.", "messages.voice_messages_cannot_have_embeds": "Voice messages can't have embeds.", diff --git a/packages/errors/src/i18n/weblate/locales/es-419.json b/packages/errors/src/i18n/weblate/locales/es-419.json index 02c5fba4a..688124ef7 100644 --- a/packages/errors/src/i18n/weblate/locales/es-419.json +++ b/packages/errors/src/i18n/weblate/locales/es-419.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Los mensajes reenviados no pueden incluir contenido, elementos incrustados, archivos adjuntos o stickers.", "messages.forward_reference_requires_channel_and_message": "La referencia del mensaje reenviado debe incluir `channel_id` y `message_id`.", "messages.invalid_message_data": "Mensaje con datos no válidos.", - "messages.invalid_scheduled_message_payload": "Carga útil de mensaje programado no válida.", "messages.message_ids_cannot_be_empty": "`message_ids` no puede estar vacío.", "messages.messages_array_required": "El arreglo `messages` es requerido y no puede estar vacío.", "messages.messages_with_snapshots_cannot_be_edited": "Los mensajes con capturas no se pueden editar.", - "messages.scheduled_messages_max_30_days": "Los mensajes programados pueden enviarse hasta con 30 días de anticipación.", - "messages.scheduled_send_datetime_invalid": "La fecha o la hora del envío programado no es válida.", - "messages.scheduled_time_must_be_future": "La hora programada debe estar en el futuro.", "messages.voice_messages_attachment_must_be_audio": "Los archivos adjuntos de los mensajes de voz deben ser archivos de audio.", "messages.voice_messages_cannot_have_content": "Los mensajes de voz no pueden incluir texto.", "messages.voice_messages_cannot_have_embeds": "Los mensajes de voz no pueden tener embeds.", diff --git a/packages/errors/src/i18n/weblate/locales/es-ES.json b/packages/errors/src/i18n/weblate/locales/es-ES.json index 134e58c69..b651035d0 100644 --- a/packages/errors/src/i18n/weblate/locales/es-ES.json +++ b/packages/errors/src/i18n/weblate/locales/es-ES.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Los mensajes reenviados no pueden incluir contenido, elementos incrustados, archivos adjuntos o stickers.", "messages.forward_reference_requires_channel_and_message": "La referencia del mensaje reenviado debe incluir `channel_id` y `message_id`.", "messages.invalid_message_data": "Datos de mensaje no válidos.", - "messages.invalid_scheduled_message_payload": "Carga útil de mensaje programado no válida.", "messages.message_ids_cannot_be_empty": "`message_ids` no puede estar vacío.", "messages.messages_array_required": "El array `messages` es necesario y no puede estar vacío.", "messages.messages_with_snapshots_cannot_be_edited": "Los mensajes con instantáneas no se pueden editar.", - "messages.scheduled_messages_max_30_days": "Los mensajes programados pueden ser como máximo 30 días en el futuro.", - "messages.scheduled_send_datetime_invalid": "Fecha u hora no válidas para el envío programado.", - "messages.scheduled_time_must_be_future": "La hora programada debe ser en el futuro.", "messages.voice_messages_attachment_must_be_audio": "Los adjuntos de los mensajes de voz deben ser archivos de audio.", "messages.voice_messages_cannot_have_content": "Los mensajes de voz no pueden tener contenido de texto.", "messages.voice_messages_cannot_have_embeds": "Los mensajes de voz no pueden tener embeds.", diff --git a/packages/errors/src/i18n/weblate/locales/fi.json b/packages/errors/src/i18n/weblate/locales/fi.json index ca807707b..db4cb3f6d 100644 --- a/packages/errors/src/i18n/weblate/locales/fi.json +++ b/packages/errors/src/i18n/weblate/locales/fi.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Edelleenlähetetyt viestit eivät voi sisältää sisältöä, upotuksia, liitteitä tai tarroja.", "messages.forward_reference_requires_channel_and_message": "Edelleenlähetetyn viestin viitteen on sisällettävä `channel_id` ja `message_id`.", "messages.invalid_message_data": "Virheellinen viestidata.", - "messages.invalid_scheduled_message_payload": "Virheellinen ajoitetun viestin hyötykuorma.", "messages.message_ids_cannot_be_empty": "`message_ids` ei voi olla tyhjä.", "messages.messages_array_required": "`messages`-taulukko on pakollinen eikä se saa olla tyhjä.", "messages.messages_with_snapshots_cannot_be_edited": "Viestit, joissa on tilannekuvia, eivät ole muokattavissa.", - "messages.scheduled_messages_max_30_days": "Ajoitetut viestit voivat olla enintään 30 päivän päässä tulevaisuudessa.", - "messages.scheduled_send_datetime_invalid": "Ajoitetun lähetyksen päivämäärä tai aika on virheellinen.", - "messages.scheduled_time_must_be_future": "Ajoitetun ajan on oltava tulevaisuudessa.", "messages.voice_messages_attachment_must_be_audio": "Ääniviestin liitteiden täytyy olla äänitiedostoja.", "messages.voice_messages_cannot_have_content": "Ääniviesteissä ei voi olla tekstisisältöä.", "messages.voice_messages_cannot_have_embeds": "Ääniviesteissä ei voi olla upotteita.", diff --git a/packages/errors/src/i18n/weblate/locales/fr.json b/packages/errors/src/i18n/weblate/locales/fr.json index be51b4862..32f3373ff 100644 --- a/packages/errors/src/i18n/weblate/locales/fr.json +++ b/packages/errors/src/i18n/weblate/locales/fr.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Les messages transférés ne peuvent pas inclure de contenu, d'éléments intégrés, de pièces jointes ou de stickers.", "messages.forward_reference_requires_channel_and_message": "La référence du message à transférer doit inclure `channel_id` et `message_id`.", "messages.invalid_message_data": "Données de message non valides.", - "messages.invalid_scheduled_message_payload": "Contenu du message programmé invalide.", "messages.message_ids_cannot_be_empty": "Le champ `message_ids` ne peut pas être vide.", "messages.messages_array_required": "Le tableau `messages` est obligatoire et ne doit pas être vide.", "messages.messages_with_snapshots_cannot_be_edited": "Les messages avec des captures d'écran ne peuvent pas être modifiés.", - "messages.scheduled_messages_max_30_days": "Les messages programmés ne peuvent pas être planifiés plus de 30 jours à l'avance.", - "messages.scheduled_send_datetime_invalid": "Date ou heure incorrecte pour l'envoi programmé.", - "messages.scheduled_time_must_be_future": "L'heure programmée doit être dans le futur.", "messages.voice_messages_attachment_must_be_audio": "Les pièces jointes des messages vocaux doivent être des fichiers audio.", "messages.voice_messages_cannot_have_content": "Les messages vocaux ne peuvent pas avoir de contenu textuel.", "messages.voice_messages_cannot_have_embeds": "Les messages vocaux ne peuvent pas avoir d'intégrations.", diff --git a/packages/errors/src/i18n/weblate/locales/he.json b/packages/errors/src/i18n/weblate/locales/he.json index 425f68bf6..13d800861 100644 --- a/packages/errors/src/i18n/weblate/locales/he.json +++ b/packages/errors/src/i18n/weblate/locales/he.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "הודעות מועברות לא יכולות לכלול תוכן, הטמעות, קבצים מצורפים או סטיקרים.", "messages.forward_reference_requires_channel_and_message": "הפניית הודעה מועברת צריכה לכלול `channel_id` ו-`message_id`.", "messages.invalid_message_data": "נתוני הודעה לא חוקיים.", - "messages.invalid_scheduled_message_payload": "מטען נתונים שגוי עבור הודעה מתוזמנת.", "messages.message_ids_cannot_be_empty": "`message_ids` לא יכול להיות ריק.", "messages.messages_array_required": "מערך `messages` נדרש ולא יכול להיות ריק.", "messages.messages_with_snapshots_cannot_be_edited": "לא ניתן לערוך הודעות הכוללות צילומי מסך.", - "messages.scheduled_messages_max_30_days": "ניתן לתזמן הודעות ל-30 הימים הבאים לכל היותר.", - "messages.scheduled_send_datetime_invalid": "תאריך או שעה לא חוקיים לשליחה מתוזמנת.", - "messages.scheduled_time_must_be_future": "מועד השליחה חייב להיות עתידי.", "messages.voice_messages_attachment_must_be_audio": "קבצים מצורפים בהודעות קוליות חייבים להיות קובצי שמע.", "messages.voice_messages_cannot_have_content": "הודעות קוליות אינן יכולות לכלול תוכן טקסט.", "messages.voice_messages_cannot_have_embeds": "הודעות קוליות לא יכולות לכלול הטמעות.", diff --git a/packages/errors/src/i18n/weblate/locales/hi.json b/packages/errors/src/i18n/weblate/locales/hi.json index 2cd73c120..d5ea8a64e 100644 --- a/packages/errors/src/i18n/weblate/locales/hi.json +++ b/packages/errors/src/i18n/weblate/locales/hi.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "फॉरवर्ड किए गए मैसेज में कंटेंट, एम्बेड, अटैचमेंट या स्टीकर शामिल नहीं हो सकते हैं।", "messages.forward_reference_requires_channel_and_message": "फॉरवर्ड मैसेज रेफरेंस में `channel_id` और `message_id` शामिल होने चाहिए।", "messages.invalid_message_data": "अमान्य मैसेज डेटा।", - "messages.invalid_scheduled_message_payload": "अमान्य शेड्यूल किया गया मैसेज पेलोड।", "messages.message_ids_cannot_be_empty": "`message_ids` खाली नहीं हो सकते।", "messages.messages_array_required": "`messages` एरे ज़रूरी है और खाली नहीं होना चाहिए।", "messages.messages_with_snapshots_cannot_be_edited": "स्नैपशॉट वाले मैसेज एडिट नहीं किए जा सकते हैं.", - "messages.scheduled_messages_max_30_days": "शेड्यूल किए गए मैसेज ज़्यादा से ज़्यादा 30 दिन आगे के हो सकते हैं.", - "messages.scheduled_send_datetime_invalid": "शेड्यूल किए गए सेंड के लिए अमान्य तारीख या समय.", - "messages.scheduled_time_must_be_future": "शेड्यूल किया गया समय भविष्य में होना चाहिए.", "messages.voice_messages_attachment_must_be_audio": "वॉइस मैसेज अटैचमेंट सिर्फ़ ऑडियो फाइल होने चाहिए.", "messages.voice_messages_cannot_have_content": "वॉइस मैसेज में टेक्स्ट कंटेंट नहीं हो सकता है.", "messages.voice_messages_cannot_have_embeds": "वॉइस मैसेज में एम्बेड नहीं हो सकते हैं.", diff --git a/packages/errors/src/i18n/weblate/locales/hr.json b/packages/errors/src/i18n/weblate/locales/hr.json index 0d936f2cd..db28cce24 100644 --- a/packages/errors/src/i18n/weblate/locales/hr.json +++ b/packages/errors/src/i18n/weblate/locales/hr.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Proslijeđene poruke ne mogu sadržavati sadržaj, ugrađene objekte, privitke ili naljepnice.", "messages.forward_reference_requires_channel_and_message": "Referenca prosljeđene poruke mora sadržavati `channel_id` i `message_id`.", "messages.invalid_message_data": "Neispravni podaci poruke.", - "messages.invalid_scheduled_message_payload": "Neispravan payload zakazane poruke.", "messages.message_ids_cannot_be_empty": "`message_ids` ne može biti prazan.", "messages.messages_array_required": "Polje `messages` je obavezno i ne smije biti prazno.", "messages.messages_with_snapshots_cannot_be_edited": "Poruke sa snimkama ne mogu se uređivati.", - "messages.scheduled_messages_max_30_days": "Zakazane poruke mogu biti najviše 30 dana u budućnosti.", - "messages.scheduled_send_datetime_invalid": "Nevažeći datum ili vrijeme za zakazano slanje.", - "messages.scheduled_time_must_be_future": "Zakazano vrijeme mora biti u budućnosti.", "messages.voice_messages_attachment_must_be_audio": "Privitci glasovnih poruka moraju biti audio datoteke.", "messages.voice_messages_cannot_have_content": "Glasovne poruke ne mogu imati tekstualni sadržaj.", "messages.voice_messages_cannot_have_embeds": "Glasovne poruke ne mogu imati ugrađene elemente.", diff --git a/packages/errors/src/i18n/weblate/locales/hu.json b/packages/errors/src/i18n/weblate/locales/hu.json index e3be73943..2841ff242 100644 --- a/packages/errors/src/i18n/weblate/locales/hu.json +++ b/packages/errors/src/i18n/weblate/locales/hu.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "A továbbított üzenetek nem tartalmazhatnak tartalmat, beágyazásokat, mellékleteket vagy matricákat.", "messages.forward_reference_requires_channel_and_message": "A továbbított üzenet hivatkozásának tartalmaznia kell a `channel_id` és a `message_id` azonosítót.", "messages.invalid_message_data": "Érvénytelen üzenetadatok.", - "messages.invalid_scheduled_message_payload": "Érvénytelen ütemezett üzenetcsomag.", "messages.message_ids_cannot_be_empty": "A `message_ids` nem maradhat üresen.", "messages.messages_array_required": "A `messages` tömb szükséges és nem maradhat üresen.", "messages.messages_with_snapshots_cannot_be_edited": "A pillanatkép-üzenetek nem szerkeszthetők.", - "messages.scheduled_messages_max_30_days": "Az ütemezett üzenetek legfeljebb 30 nappal későbbre küldhetők.", - "messages.scheduled_send_datetime_invalid": "Érvénytelen dátum vagy idő az ütemezett küldéshez.", - "messages.scheduled_time_must_be_future": "Az ütemezés időpontjának a jövőben kell lennie.", "messages.voice_messages_attachment_must_be_audio": "A hangüzenet mellékleteinek hangfájlokat kell tartalmazniuk.", "messages.voice_messages_cannot_have_content": "A hangüzenetek nem tartalmazhatnak szöveges tartalmat.", "messages.voice_messages_cannot_have_embeds": "A hangüzenetek nem tartalmazhatnak beágyazásokat.", diff --git a/packages/errors/src/i18n/weblate/locales/id.json b/packages/errors/src/i18n/weblate/locales/id.json index 34c8b09e2..afd605587 100644 --- a/packages/errors/src/i18n/weblate/locales/id.json +++ b/packages/errors/src/i18n/weblate/locales/id.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Pesan yang diteruskan tidak bisa menyertakan konten, embed, lampiran, atau stiker.", "messages.forward_reference_requires_channel_and_message": "Referensi pesan terusan harus menyertakan `channel_id` dan `message_id`.", "messages.invalid_message_data": "Data pesan tidak valid.", - "messages.invalid_scheduled_message_payload": "Payload pesan terjadwal tidak valid.", "messages.message_ids_cannot_be_empty": "`message_ids` tidak boleh kosong.", "messages.messages_array_required": "Array `messages` diperlukan dan tidak boleh kosong.", "messages.messages_with_snapshots_cannot_be_edited": "Pesan dengan snapshot tidak dapat diedit.", - "messages.scheduled_messages_max_30_days": "Pesan terjadwal maksimal 30 hari ke depan.", - "messages.scheduled_send_datetime_invalid": "Tanggal atau waktu tidak valid untuk kirim terjadwal.", - "messages.scheduled_time_must_be_future": "Waktu terjadwal harus di waktu mendatang.", "messages.voice_messages_attachment_must_be_audio": "Lampiran pesan suara harus berupa file audio.", "messages.voice_messages_cannot_have_content": "Pesan suara tidak boleh memiliki konten teks.", "messages.voice_messages_cannot_have_embeds": "Pesan suara tidak boleh memiliki sematan.", diff --git a/packages/errors/src/i18n/weblate/locales/it.json b/packages/errors/src/i18n/weblate/locales/it.json index a2640e72c..691ecd2ec 100644 --- a/packages/errors/src/i18n/weblate/locales/it.json +++ b/packages/errors/src/i18n/weblate/locales/it.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "I messaggi inoltrati non possono includere contenuti, embed, allegati o sticker.", "messages.forward_reference_requires_channel_and_message": "Il riferimento per l'inoltro del messaggio deve includere `channel_id` e `message_id`.", "messages.invalid_message_data": "Dati messaggio non validi.", - "messages.invalid_scheduled_message_payload": "Payload del messaggio programmato non valido.", "messages.message_ids_cannot_be_empty": "`message_ids` non deve essere vuoto.", "messages.messages_array_required": "L'array `messages` è obbligatorio e non deve essere vuoto.", "messages.messages_with_snapshots_cannot_be_edited": "I messaggi con istantanee non possono essere modificati.", - "messages.scheduled_messages_max_30_days": "I messaggi programmati possono essere programmati per un massimo di 30 giorni nel futuro.", - "messages.scheduled_send_datetime_invalid": "Data o ora non valide per l'invio programmato.", - "messages.scheduled_time_must_be_future": "L'ora programmata deve essere nel futuro.", "messages.voice_messages_attachment_must_be_audio": "Gli allegati dei messaggi vocali devono essere file audio.", "messages.voice_messages_cannot_have_content": "I messaggi vocali non possono contenere testo.", "messages.voice_messages_cannot_have_embeds": "I messaggi vocali non possono contenere embed.", diff --git a/packages/errors/src/i18n/weblate/locales/ja.json b/packages/errors/src/i18n/weblate/locales/ja.json index 15ca09341..33e709ea7 100644 --- a/packages/errors/src/i18n/weblate/locales/ja.json +++ b/packages/errors/src/i18n/weblate/locales/ja.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "転送メッセージにはコンテンツ、埋め込み、添付ファイル、スタンプを含められません。", "messages.forward_reference_requires_channel_and_message": "転送メッセージの参照には`channel_id`と`message_id`を含める必要があります。", "messages.invalid_message_data": "メッセージデータが無効です。", - "messages.invalid_scheduled_message_payload": "スケジュール済みメッセージペイロードが無効です。", "messages.message_ids_cannot_be_empty": "`message_ids`は空にできません。", "messages.messages_array_required": "`messages`配列は必須であり、空にできません。", "messages.messages_with_snapshots_cannot_be_edited": "スナップショット付きメッセージは編集できません。", - "messages.scheduled_messages_max_30_days": "予約メッセージは30日後まで設定できます。", - "messages.scheduled_send_datetime_invalid": "予約送信の日時が無効です。", - "messages.scheduled_time_must_be_future": "予約時間は未来の日時を指定してください。", "messages.voice_messages_attachment_must_be_audio": "音声メッセージの添付ファイルは音声ファイルのみです。", "messages.voice_messages_cannot_have_content": "音声メッセージにテキストは含められません。", "messages.voice_messages_cannot_have_embeds": "音声メッセージに埋め込みは含められません。", diff --git a/packages/errors/src/i18n/weblate/locales/ko.json b/packages/errors/src/i18n/weblate/locales/ko.json index f2341e27c..211178ea4 100644 --- a/packages/errors/src/i18n/weblate/locales/ko.json +++ b/packages/errors/src/i18n/weblate/locales/ko.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "전달된 메시지에는 콘텐츠, 임베드, 첨부 파일 또는 스티커를 포함할 수 없어요.", "messages.forward_reference_requires_channel_and_message": "메시지 전달 참조에는 `channel_id`와 `message_id`가 포함되어야 해요.", "messages.invalid_message_data": "유효하지 않은 메시지 데이터예요.", - "messages.invalid_scheduled_message_payload": "유효하지 않은 예약 메시지 페이로드예요.", "messages.message_ids_cannot_be_empty": "`message_ids`는 비워둘 수 없어요.", "messages.messages_array_required": "`messages` 배열은 필수 항목이며 비워둘 수 없어요.", "messages.messages_with_snapshots_cannot_be_edited": "스냅샷이 있는 메시지는 수정할 수 없어요.", - "messages.scheduled_messages_max_30_days": "예약 메시지는 최대 30일 이후로 설정할 수 있어요.", - "messages.scheduled_send_datetime_invalid": "예약 전송 날짜 또는 시간이 유효하지 않아요.", - "messages.scheduled_time_must_be_future": "예약 시간은 미래여야 해요.", "messages.voice_messages_attachment_must_be_audio": "음성 메시지 첨부 파일은 오디오 파일이어야 해요.", "messages.voice_messages_cannot_have_content": "음성 메시지에는 텍스트 콘텐츠를 포함할 수 없어요.", "messages.voice_messages_cannot_have_embeds": "음성 메시지에는 임베드를 포함할 수 없어요.", diff --git a/packages/errors/src/i18n/weblate/locales/lt.json b/packages/errors/src/i18n/weblate/locales/lt.json index 909d72b91..d8de57c62 100644 --- a/packages/errors/src/i18n/weblate/locales/lt.json +++ b/packages/errors/src/i18n/weblate/locales/lt.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Persiųstose žinutėse negali būti turinio, įterptinių elementų, priedų ar lipdukų.", "messages.forward_reference_requires_channel_and_message": "Persiųstos žinutės nuoroda turi apimti `channel_id` ir `message_id`.", "messages.invalid_message_data": "Neteisingi žinutės duomenys.", - "messages.invalid_scheduled_message_payload": "Neteisingas suplanuotos žinutės turinys.", "messages.message_ids_cannot_be_empty": "`message_ids` negali būti tuščias.", "messages.messages_array_required": "Masyvas `messages` yra privalomas ir negali būti tuščias.", "messages.messages_with_snapshots_cannot_be_edited": "Žinučių su momentinėmis nuotraukomis negalima redaguoti.", - "messages.scheduled_messages_max_30_days": "Suplanuotos žinutės gali būti daugiausia 30 dienų į priekį.", - "messages.scheduled_send_datetime_invalid": "Neteisinga data arba laikas suplanuotam siuntimui.", - "messages.scheduled_time_must_be_future": "Suplanuotas laikas turi būti ateityje.", "messages.voice_messages_attachment_must_be_audio": "Balso žinučių priedai turi būti garso failai.", "messages.voice_messages_cannot_have_content": "Balso žinutės negali turėti tekstinio turinio.", "messages.voice_messages_cannot_have_embeds": "Balso žinutės negali turėti įterptinių elementų.", diff --git a/packages/errors/src/i18n/weblate/locales/nl.json b/packages/errors/src/i18n/weblate/locales/nl.json index 354ce0470..50c88a5d8 100644 --- a/packages/errors/src/i18n/weblate/locales/nl.json +++ b/packages/errors/src/i18n/weblate/locales/nl.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Doorgestuurde berichten kunnen geen inhoud, embeds, bijlagen of stickers bevatten.", "messages.forward_reference_requires_channel_and_message": "De referentie van een doorgestuurd bericht moet `channel_id` en `message_id` bevatten.", "messages.invalid_message_data": "Ongeldige berichtgegevens.", - "messages.invalid_scheduled_message_payload": "Ongeldige payload voor gepland bericht.", "messages.message_ids_cannot_be_empty": "`message_ids` mag niet leeg zijn.", "messages.messages_array_required": "De `messages`-array is vereist en mag niet leeg zijn.", "messages.messages_with_snapshots_cannot_be_edited": "Berichten met snapshots kunnen niet worden bewerkt.", - "messages.scheduled_messages_max_30_days": "Geplande berichten kunnen maximaal 30 dagen in de toekomst zijn.", - "messages.scheduled_send_datetime_invalid": "Ongeldige datum of tijd voor gepland verzenden.", - "messages.scheduled_time_must_be_future": "De geplande tijd moet in de toekomst zijn.", "messages.voice_messages_attachment_must_be_audio": "Bijlagen bij spraakberichten moeten audiobestanden zijn.", "messages.voice_messages_cannot_have_content": "Spraakberichten kunnen geen tekstinhoud bevatten.", "messages.voice_messages_cannot_have_embeds": "Spraakberichten kunnen geen embeds bevatten.", diff --git a/packages/errors/src/i18n/weblate/locales/no.json b/packages/errors/src/i18n/weblate/locales/no.json index 6aa68a2a0..73b86b01f 100644 --- a/packages/errors/src/i18n/weblate/locales/no.json +++ b/packages/errors/src/i18n/weblate/locales/no.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Videresendte meldinger kan ikke inneholde innhold, innebygde elementer, vedlegg eller stickere.", "messages.forward_reference_requires_channel_and_message": "Videresendt meldingsreferanse må inkludere `channel_id` og `message_id`.", "messages.invalid_message_data": "Ugyldig meldingsdata.", - "messages.invalid_scheduled_message_payload": "Ugyldig nyttelast for planlagt melding.", "messages.message_ids_cannot_be_empty": "`message_ids` kan ikke være tomme.", "messages.messages_array_required": "Matrisen `messages` er påkrevd og kan ikke være tom.", "messages.messages_with_snapshots_cannot_be_edited": "Meldinger med skjermbilder kan ikke redigeres.", - "messages.scheduled_messages_max_30_days": "Planlagte meldinger kan være maksimalt 30 dager frem i tid.", - "messages.scheduled_send_datetime_invalid": "Ugyldig dato eller tid for planlagt sending.", - "messages.scheduled_time_must_be_future": "Planlagt tid må være i fremtiden.", "messages.voice_messages_attachment_must_be_audio": "Vedlegg til talemeldinger må være lydfiler.", "messages.voice_messages_cannot_have_content": "Talemeldinger kan ikke ha tekstinnhold.", "messages.voice_messages_cannot_have_embeds": "Talemeldinger kan ikke ha innebygde elementer.", diff --git a/packages/errors/src/i18n/weblate/locales/pl.json b/packages/errors/src/i18n/weblate/locales/pl.json index 2f9608800..68d50128f 100644 --- a/packages/errors/src/i18n/weblate/locales/pl.json +++ b/packages/errors/src/i18n/weblate/locales/pl.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Przekazane wiadomości nie mogą zawierać treści, osadzonych elementów, załączników ani naklejek.", "messages.forward_reference_requires_channel_and_message": "Odwołanie do przekazanej wiadomości musi zawierać `channel_id` i `message_id`.", "messages.invalid_message_data": "Nieprawidłowe dane wiadomości.", - "messages.invalid_scheduled_message_payload": "Nieprawidłowy ładunek zaplanowanej wiadomości.", "messages.message_ids_cannot_be_empty": "`message_ids` nie może być puste.", "messages.messages_array_required": "Tablica `messages` jest wymagana i nie może być pusta.", "messages.messages_with_snapshots_cannot_be_edited": "Wiadomości z migawkami nie mogą być edytowane.", - "messages.scheduled_messages_max_30_days": "Zaplanowane wiadomości mogą być wysłane na maksymalnie 30 dni do przodu.", - "messages.scheduled_send_datetime_invalid": "Nieprawidłowa data lub godzina zaplanowanego wysłania.", - "messages.scheduled_time_must_be_future": "Zaplanowany czas musi dotyczyć przyszłości.", "messages.voice_messages_attachment_must_be_audio": "Załączniki wiadomości głosowych muszą być plikami audio.", "messages.voice_messages_cannot_have_content": "Wiadomości głosowe nie mogą zawierać treści tekstowych.", "messages.voice_messages_cannot_have_embeds": "Wiadomości głosowe nie mogą zawierać osadzonych elementów.", diff --git a/packages/errors/src/i18n/weblate/locales/pt-BR.json b/packages/errors/src/i18n/weblate/locales/pt-BR.json index a0e6ddddc..aab04c6ef 100644 --- a/packages/errors/src/i18n/weblate/locales/pt-BR.json +++ b/packages/errors/src/i18n/weblate/locales/pt-BR.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Mensagens encaminhadas não podem incluir conteúdo, elementos incorporados, anexos ou figurinhas.", "messages.forward_reference_requires_channel_and_message": "A referência de encaminhamento de mensagem precisa incluir `channel_id` e `message_id`.", "messages.invalid_message_data": "Dados da mensagem inválidos.", - "messages.invalid_scheduled_message_payload": "Payload inválido da mensagem agendada.", "messages.message_ids_cannot_be_empty": "`message_ids` não pode ser vazio.", "messages.messages_array_required": "O array `messages` é necessário e não pode ser vazio.", "messages.messages_with_snapshots_cannot_be_edited": "Mensagens com snapshots não podem ser editadas.", - "messages.scheduled_messages_max_30_days": "Mensagens agendadas podem ser para no máximo 30 dias no futuro.", - "messages.scheduled_send_datetime_invalid": "Data ou hora inválida para o envio agendado.", - "messages.scheduled_time_must_be_future": "O horário agendado deve ser no futuro.", "messages.voice_messages_attachment_must_be_audio": "Anexos de mensagens de voz precisam ser arquivos de áudio.", "messages.voice_messages_cannot_have_content": "Mensagens de voz não podem conter texto.", "messages.voice_messages_cannot_have_embeds": "Mensagens de voz não podem ter embeds.", diff --git a/packages/errors/src/i18n/weblate/locales/ro.json b/packages/errors/src/i18n/weblate/locales/ro.json index 66d40da45..61027e115 100644 --- a/packages/errors/src/i18n/weblate/locales/ro.json +++ b/packages/errors/src/i18n/weblate/locales/ro.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Mesajele redirecționate nu pot include conținut, încorporări, atașamente sau stickere.", "messages.forward_reference_requires_channel_and_message": "Referința mesajului redirecționat trebuie să includă `channel_id` și `message_id`.", "messages.invalid_message_data": "Date mesaj invalide.", - "messages.invalid_scheduled_message_payload": "Sarcină utilă mesaj programat invalidă.", "messages.message_ids_cannot_be_empty": "`message_ids` nu poate fi gol.", "messages.messages_array_required": "Câmpul `messages` este obligatoriu și nu trebuie să fie gol.", "messages.messages_with_snapshots_cannot_be_edited": "Mesajele cu instantanee nu pot fi editate.", - "messages.scheduled_messages_max_30_days": "Mesajele programate pot fi programate pentru cel mult 30 de zile în avans.", - "messages.scheduled_send_datetime_invalid": "Data sau ora sunt invalide pentru programarea trimiterii.", - "messages.scheduled_time_must_be_future": "Ora programată trebuie să fie o oră viitoare.", "messages.voice_messages_attachment_must_be_audio": "Atașamentele mesajelor vocale trebuie să fie fișiere audio.", "messages.voice_messages_cannot_have_content": "Mesajele vocale nu pot avea conținut text.", "messages.voice_messages_cannot_have_embeds": "Mesajele vocale nu pot avea embed-uri.", diff --git a/packages/errors/src/i18n/weblate/locales/ru.json b/packages/errors/src/i18n/weblate/locales/ru.json index bd9f1ba53..c2d72e5c1 100644 --- a/packages/errors/src/i18n/weblate/locales/ru.json +++ b/packages/errors/src/i18n/weblate/locales/ru.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Пересланные сообщения не могут содержать контент, встраиваемые элементы, вложения или стикеры.", "messages.forward_reference_requires_channel_and_message": "Ссылка на пересылаемое сообщение должна включать `channel_id` и `message_id`.", "messages.invalid_message_data": "Неверные данные сообщения.", - "messages.invalid_scheduled_message_payload": "Недействительная нагрузка запланированного сообщения.", "messages.message_ids_cannot_be_empty": "`message_ids` не может быть пустым.", "messages.messages_array_required": "Массив `messages` обязателен и не должен быть пустым.", "messages.messages_with_snapshots_cannot_be_edited": "Сообщения со снимками нельзя редактировать.", - "messages.scheduled_messages_max_30_days": "Запланированные сообщения можно отправить не более чем через 30 дней.", - "messages.scheduled_send_datetime_invalid": "Неверная дата или время для запланированной отправки.", - "messages.scheduled_time_must_be_future": "Время отправки должно быть в будущем.", "messages.voice_messages_attachment_must_be_audio": "Вложения голосовых сообщений должны быть аудиофайлами.", "messages.voice_messages_cannot_have_content": "Голосовые сообщения не могут содержать текст.", "messages.voice_messages_cannot_have_embeds": "Голосовые сообщения не могут содержать встроенный контент.", diff --git a/packages/errors/src/i18n/weblate/locales/sv-SE.json b/packages/errors/src/i18n/weblate/locales/sv-SE.json index db58e6729..f982db0e6 100644 --- a/packages/errors/src/i18n/weblate/locales/sv-SE.json +++ b/packages/errors/src/i18n/weblate/locales/sv-SE.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Vidarebefordrade meddelanden kan inte innehålla innehåll, inbäddningar, bilagor eller stickers.", "messages.forward_reference_requires_channel_and_message": "Vidarebefordrad meddelandereferens måste inkludera `channel_id` och `message_id`.", "messages.invalid_message_data": "Ogiltig meddelandedata.", - "messages.invalid_scheduled_message_payload": "Ogiltig nyttolast för schemalagt meddelande.", "messages.message_ids_cannot_be_empty": "`message_ids` kan inte vara tomma.", "messages.messages_array_required": "`messages`-arrayen krävs och får inte vara tom.", "messages.messages_with_snapshots_cannot_be_edited": "Meddelanden med ögonblicksbilder kan inte redigeras.", - "messages.scheduled_messages_max_30_days": "Schemalagda meddelanden kan vara högst 30 dagar framåt.", - "messages.scheduled_send_datetime_invalid": "Ogiltigt datum eller tid för schemalagd sändning.", - "messages.scheduled_time_must_be_future": "Schemalagd tid måste vara framåt i tiden.", "messages.voice_messages_attachment_must_be_audio": "Röstmeddelandebilagor måste vara ljudfiler.", "messages.voice_messages_cannot_have_content": "Röstmeddelanden kan inte ha textinnehåll.", "messages.voice_messages_cannot_have_embeds": "Röstmeddelanden kan inte ha inbäddningar.", diff --git a/packages/errors/src/i18n/weblate/locales/th.json b/packages/errors/src/i18n/weblate/locales/th.json index a50844e84..e8e78808a 100644 --- a/packages/errors/src/i18n/weblate/locales/th.json +++ b/packages/errors/src/i18n/weblate/locales/th.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "ข้อความที่ส่งต่อไม่สามารถมีเนื้อหา, การฝัง, ไฟล์แนบ หรือสติกเกอร์ได้", "messages.forward_reference_requires_channel_and_message": "การอ้างอิงข้อความที่ส่งต่อต้องมี `channel_id` และ `message_id`", "messages.invalid_message_data": "ข้อมูลข้อความไม่ถูกต้อง", - "messages.invalid_scheduled_message_payload": "เพย์โหลดข้อความที่กำหนดเวลาไม่ถูกต้อง", "messages.message_ids_cannot_be_empty": "`message_ids` ต้องไม่ว่างเปล่า", "messages.messages_array_required": "ต้องระบุอาร์เรย์ `messages` และต้องไม่ว่างเปล่า", "messages.messages_with_snapshots_cannot_be_edited": "ข้อความที่มีสแนปช็อตไม่สามารถแก้ไขได้", - "messages.scheduled_messages_max_30_days": "ข้อความที่กำหนดเวลาสามารถตั้งล่วงหน้าได้สูงสุด 30 วัน", - "messages.scheduled_send_datetime_invalid": "วันที่หรือเวลาสำหรับการส่งตามกำหนดเวลาไม่ถูกต้อง", - "messages.scheduled_time_must_be_future": "เวลาที่กำหนดต้องเป็นเวลาในอนาคต", "messages.voice_messages_attachment_must_be_audio": "ไฟล์แนบข้อความเสียงต้องเป็นไฟล์เสียง", "messages.voice_messages_cannot_have_content": "ข้อความเสียงไม่สามารถมีเนื้อหาข้อความได้", "messages.voice_messages_cannot_have_embeds": "ข้อความเสียงไม่สามารถมีการฝังได้", diff --git a/packages/errors/src/i18n/weblate/locales/tr.json b/packages/errors/src/i18n/weblate/locales/tr.json index 6a8cfc5c3..bd5e7d5d2 100644 --- a/packages/errors/src/i18n/weblate/locales/tr.json +++ b/packages/errors/src/i18n/weblate/locales/tr.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "İletilen mesajlar içerik, gömülü içerik, ekler veya çıkartma içeremez.", "messages.forward_reference_requires_channel_and_message": "İletilen mesaj referansı `channel_id` ve `message_id` içermelidir.", "messages.invalid_message_data": "Geçersiz mesaj verisi.", - "messages.invalid_scheduled_message_payload": "Geçersiz zamanlanmış mesaj yükü.", "messages.message_ids_cannot_be_empty": "`message_ids` boş olamaz.", "messages.messages_array_required": "`messages` dizisi gereklidir ve boş olamaz.", "messages.messages_with_snapshots_cannot_be_edited": "Anlık görüntülü mesajlar düzenlenemez.", - "messages.scheduled_messages_max_30_days": "Zamanlanmış mesajlar en fazla 30 gün ileriye ayarlanabilir.", - "messages.scheduled_send_datetime_invalid": "Zamanlanmış gönderim için geçersiz tarih veya saat.", - "messages.scheduled_time_must_be_future": "Zamanlanmış gönderim gelecekte bir zaman olmalı.", "messages.voice_messages_attachment_must_be_audio": "Sesli mesaj ekleri ses dosyası türünde olmalı.", "messages.voice_messages_cannot_have_content": "Sesli mesajlar metin içeriği içeremez.", "messages.voice_messages_cannot_have_embeds": "Sesli mesajlar yerleştirme içeremez.", diff --git a/packages/errors/src/i18n/weblate/locales/uk.json b/packages/errors/src/i18n/weblate/locales/uk.json index e1132c25c..5514cfe92 100644 --- a/packages/errors/src/i18n/weblate/locales/uk.json +++ b/packages/errors/src/i18n/weblate/locales/uk.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Переслані повідомлення не можуть містити вміст, вбудовані елементи, вкладення або стікери.", "messages.forward_reference_requires_channel_and_message": "Посилання на переслане повідомлення має містити `channel_id` та `message_id`.", "messages.invalid_message_data": "Недійсні дані повідомлення.", - "messages.invalid_scheduled_message_payload": "Недійсний вміст запланованого повідомлення.", "messages.message_ids_cannot_be_empty": "`message_ids` не можуть бути порожніми.", "messages.messages_array_required": "Масив `messages` обов'язковий і не може бути порожнім.", "messages.messages_with_snapshots_cannot_be_edited": "Повідомлення зі знімками не можна редагувати.", - "messages.scheduled_messages_max_30_days": "Заплановані повідомлення можуть бути щонайбільше на 30 днів уперед.", - "messages.scheduled_send_datetime_invalid": "Неправильна дата або час для запланованого надсилання.", - "messages.scheduled_time_must_be_future": "Запланований час має бути в майбутньому.", "messages.voice_messages_attachment_must_be_audio": "Вкладення голосових повідомлень повинні бути аудіофайлами.", "messages.voice_messages_cannot_have_content": "Голосові повідомлення не можуть містити текст.", "messages.voice_messages_cannot_have_embeds": "Голосові повідомлення не можуть містити вбудовані елементи.", diff --git a/packages/errors/src/i18n/weblate/locales/vi.json b/packages/errors/src/i18n/weblate/locales/vi.json index 025014bcd..82e5edc19 100644 --- a/packages/errors/src/i18n/weblate/locales/vi.json +++ b/packages/errors/src/i18n/weblate/locales/vi.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Tin nhắn đã chuyển tiếp không được chứa nội dung, nhúng, tệp đính kèm hoặc sticker.", "messages.forward_reference_requires_channel_and_message": "Tham chiếu tin nhắn chuyển tiếp phải bao gồm `channel_id` và `message_id`.", "messages.invalid_message_data": "Dữ liệu tin nhắn không hợp lệ.", - "messages.invalid_scheduled_message_payload": "Payload của tin nhắn hẹn giờ không hợp lệ.", "messages.message_ids_cannot_be_empty": "`message_ids` không được trống.", "messages.messages_array_required": "Mảng `messages` là bắt buộc và không được trống.", "messages.messages_with_snapshots_cannot_be_edited": "Tin nhắn có snapshot không thể chỉnh sửa.", - "messages.scheduled_messages_max_30_days": "Tin nhắn đã lên lịch có thể tối đa 30 ngày tới.", - "messages.scheduled_send_datetime_invalid": "Ngày hoặc giờ không hợp lệ để gửi theo lịch.", - "messages.scheduled_time_must_be_future": "Thời gian đã lên lịch phải ở tương lai.", "messages.voice_messages_attachment_must_be_audio": "Tệp đính kèm tin nhắn thoại phải là tệp âm thanh.", "messages.voice_messages_cannot_have_content": "Tin nhắn thoại không thể có nội dung văn bản.", "messages.voice_messages_cannot_have_embeds": "Tin nhắn thoại không thể có nhúng.", diff --git a/packages/errors/src/i18n/weblate/locales/zh-CN.json b/packages/errors/src/i18n/weblate/locales/zh-CN.json index 8ac071111..93931d7e1 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-CN.json +++ b/packages/errors/src/i18n/weblate/locales/zh-CN.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "转发消息不能包含内容、嵌入内容、附件或贴纸。", "messages.forward_reference_requires_channel_and_message": "转发消息引用必须包含 `channel_id` 和 `message_id`。", "messages.invalid_message_data": "消息数据无效。", - "messages.invalid_scheduled_message_payload": "无效的定时消息负载。", "messages.message_ids_cannot_be_empty": "`message_ids` 不能为空。", "messages.messages_array_required": "`messages` 数组是必需的,且不能为空。", "messages.messages_with_snapshots_cannot_be_edited": "带有快照的消息无法编辑。", - "messages.scheduled_messages_max_30_days": "定时消息最多只能安排在未来 30 天内。", - "messages.scheduled_send_datetime_invalid": "定时发送的日期或时间不正确。", - "messages.scheduled_time_must_be_future": "定时时间必须在未来。", "messages.voice_messages_attachment_must_be_audio": "语音消息附件必须是音频文件。", "messages.voice_messages_cannot_have_content": "语音消息不能包含文本内容。", "messages.voice_messages_cannot_have_embeds": "语音消息不能包含嵌入内容。", diff --git a/packages/errors/src/i18n/weblate/locales/zh-TW.json b/packages/errors/src/i18n/weblate/locales/zh-TW.json index 6092ed0e4..d1040687b 100644 --- a/packages/errors/src/i18n/weblate/locales/zh-TW.json +++ b/packages/errors/src/i18n/weblate/locales/zh-TW.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "轉寄的訊息不能包含內容、嵌入、附件或貼圖。", "messages.forward_reference_requires_channel_and_message": "轉寄訊息參考必須包含 `channel_id` 和 `message_id`。", "messages.invalid_message_data": "無效的訊息資料。", - "messages.invalid_scheduled_message_payload": "無效的排程訊息內容。", "messages.message_ids_cannot_be_empty": "`message_ids` 不能為空。", "messages.messages_array_required": "`messages` 陣列為必填,且不能為空。", "messages.messages_with_snapshots_cannot_be_edited": "包含快照的訊息無法編輯。", - "messages.scheduled_messages_max_30_days": "排程訊息最多只能排程到未來 30 天。", - "messages.scheduled_send_datetime_invalid": "排程傳送的日期或時間無效。", - "messages.scheduled_time_must_be_future": "排程時間必須在未來。", "messages.voice_messages_attachment_must_be_audio": "語音訊息附件必須是音訊檔案。", "messages.voice_messages_cannot_have_content": "語音訊息不能包含文字內容。", "messages.voice_messages_cannot_have_embeds": "語音訊息不能包含嵌入內容。", diff --git a/packages/errors/src/i18n/weblate/messages.json b/packages/errors/src/i18n/weblate/messages.json index c3a4f2f8b..5e46c94a8 100644 --- a/packages/errors/src/i18n/weblate/messages.json +++ b/packages/errors/src/i18n/weblate/messages.json @@ -371,13 +371,9 @@ "messages.forward_messages_cannot_include_extra_content": "Forwarded messages can't include content, embeds, attachments, or stickers.", "messages.forward_reference_requires_channel_and_message": "Forward message reference must include `channel_id` and `message_id`.", "messages.invalid_message_data": "Invalid message data.", - "messages.invalid_scheduled_message_payload": "Invalid scheduled message payload.", "messages.message_ids_cannot_be_empty": "`message_ids` can't be empty.", "messages.messages_array_required": "`messages` array is required and must not be empty.", "messages.messages_with_snapshots_cannot_be_edited": "Messages with snapshots can't be edited.", - "messages.scheduled_messages_max_30_days": "Scheduled messages can be at most 30 days in the future.", - "messages.scheduled_send_datetime_invalid": "Invalid date or time for scheduled send.", - "messages.scheduled_time_must_be_future": "Scheduled time must be in the future.", "messages.voice_messages_attachment_must_be_audio": "Voice message attachments must be audio files.", "messages.voice_messages_cannot_have_content": "Voice messages can't have text content.", "messages.voice_messages_cannot_have_embeds": "Voice messages can't have embeds.", diff --git a/packages/schema/src/domains/common/CommonParamSchemas.ts b/packages/schema/src/domains/common/CommonParamSchemas.ts index 9ca79c46b..c96066c83 100644 --- a/packages/schema/src/domains/common/CommonParamSchemas.ts +++ b/packages/schema/src/domains/common/CommonParamSchemas.ts @@ -246,12 +246,6 @@ export const CredentialIdParam = z.object({ export type CredentialIdParam = z.infer; -export const ScheduledMessageIdParam = z.object({ - scheduled_message_id: SnowflakeType.describe('The ID of the scheduled message'), -}); - -export type ScheduledMessageIdParam = z.infer; - const ArchiveSubjectTypeEnum = z .enum(['user', 'guild']) .describe('Type of entity being archived: user for user data archives, guild for guild data archives'); diff --git a/packages/schema/src/domains/message/ScheduledMessageSchemas.ts b/packages/schema/src/domains/message/ScheduledMessageSchemas.ts deleted file mode 100644 index 7522b75ff..000000000 --- a/packages/schema/src/domains/message/ScheduledMessageSchemas.ts +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - -import {MessageFlags, MessageFlagsDescriptions} from '@fluxer/constants/src/ChannelConstants'; -import {MessageEmbedResponse} from '@fluxer/schema/src/domains/message/EmbedSchemas'; -import {MessageNonceRequest, MessageRequestSchema} from '@fluxer/schema/src/domains/message/MessageRequestSchemas'; -import { - MessageAttachmentResponse, - MessageStickerResponse, -} from '@fluxer/schema/src/domains/message/MessageResponseSchemas'; -import { - AllowedMentionParseTypeSchema, - MessageReferenceTypeSchema, -} from '@fluxer/schema/src/primitives/MessageValidators'; -import { - createBitflagInt32Type, - createNamedStringLiteralUnion, - createStringType, - SnowflakeStringType, - withFieldDescription, - withOpenApiType, -} from '@fluxer/schema/src/primitives/SchemaPrimitives'; -import {z} from 'zod'; - -export const ScheduledMessageRequestSchema = MessageRequestSchema.extend({ - scheduled_local_at: createStringType(1, 64).describe( - 'ISO 8601 timestamp expressed in the user local timezone for when the message should be delivered', - ), - timezone: createStringType(1, 128).describe('IANA timezone identifier the schedule_local_at value is anchored to'), -}); - -export type ScheduledMessageRequestSchema = z.infer; - -const ScheduledMessageAllowedMentionsSchema = z.object({ - parse: z.array(AllowedMentionParseTypeSchema).optional().describe('Types of mentions to parse from content'), - users: z.array(SnowflakeStringType).optional().describe('Array of user IDs to mention'), - roles: z.array(SnowflakeStringType).optional().describe('Array of role IDs to mention'), - replied_user: z.boolean().optional().describe('Whether to mention the author of the replied message'), -}); - -const ScheduledMessageReferenceSchema = z.object({ - message_id: SnowflakeStringType.describe('ID of the message being referenced'), - channel_id: SnowflakeStringType.optional().describe('ID of the channel containing the referenced message'), - guild_id: SnowflakeStringType.optional().describe('ID of the guild containing the referenced message'), - type: withFieldDescription(MessageReferenceTypeSchema, 'The type of message reference').optional(), -}); - -const ScheduledMessagePayloadResponseSchema = z.object({ - content: z.string().nullish().describe('The text content of the scheduled message'), - tts: z.boolean().optional().describe('Whether this is a text-to-speech message'), - embeds: z.array(MessageEmbedResponse).optional().describe('Array of embed objects attached to the message'), - attachments: z.array(MessageAttachmentResponse).optional().describe('Array of attachment objects for the message'), - stickers: z.array(MessageStickerResponse).optional().describe('Array of sticker objects attached to the message'), - sticker_ids: z.array(SnowflakeStringType).optional().describe('Array of sticker IDs to include in the message'), - allowed_mentions: ScheduledMessageAllowedMentionsSchema.optional().describe( - 'Controls which mentions trigger notifications', - ), - message_reference: ScheduledMessageReferenceSchema.optional().describe( - 'Reference to another message (for replies or forwards)', - ), - flags: createBitflagInt32Type(MessageFlags, MessageFlagsDescriptions, 'Message flags', 'MessageFlags').optional(), - nonce: MessageNonceRequest.optional(), - favorite_meme_id: SnowflakeStringType.optional().describe('ID of a favorite meme to attach'), -}); - -const ScheduledMessageStatus = withOpenApiType( - createNamedStringLiteralUnion( - [ - ['pending', 'pending', 'The message is pending validation and has not yet been scheduled'], - ['invalid', 'invalid', 'The message failed validation and cannot be sent'], - ['scheduled', 'scheduled', 'The message has been validated and is scheduled for delivery'], - ['sent', 'sent', 'The message has been successfully sent'], - ['failed', 'failed', 'The message failed to send after being scheduled'], - ['cancelled', 'cancelled', 'The scheduled message was cancelled by the user'], - ], - 'The current status of the scheduled message', - ), - 'ScheduledMessageStatus', -); - -export const ScheduledMessageResponseSchema = z.object({ - id: SnowflakeStringType.describe('The unique identifier for this scheduled message'), - channel_id: SnowflakeStringType.describe('The ID of the channel this message will be sent to'), - scheduled_at: z.string().describe('The ISO 8601 UTC timestamp when the message is scheduled to be sent'), - scheduled_local_at: z.string().describe('The ISO 8601 timestamp in the user local timezone'), - timezone: z.string().describe('The IANA timezone identifier used for scheduling'), - status: ScheduledMessageStatus.describe('The current status of the scheduled message'), - status_reason: z.string().nullable().describe('A human-readable reason for the current status, if applicable'), - payload: ScheduledMessagePayloadResponseSchema.describe('The message content and metadata to be sent'), - created_at: z.string().describe('The ISO 8601 timestamp when this scheduled message was created'), - invalidated_at: z.string().nullable().describe('The ISO 8601 timestamp when the message was marked invalid'), -}); - -export type ScheduledMessageResponseSchema = z.infer; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f7f4f939..cb48a24cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -614,9 +614,6 @@ importers: lodash: specifier: 'catalog:' version: 4.18.1 - luxon: - specifier: 'catalog:' - version: 3.7.2 maxmind: specifier: 'catalog:' version: 5.0.5 @@ -666,9 +663,6 @@ importers: '@types/lodash': specifier: 'catalog:' version: 4.17.24 - '@types/luxon': - specifier: 'catalog:' - version: 3.7.1 '@typescript/native-preview': specifier: 'catalog:' version: 7.0.0-dev.20260224.1