refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,152 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import type {MessageID, UserID} from '@fluxer/api/src/BrandedTypes';
import {createMessageID, createUserID} from '@fluxer/api/src/BrandedTypes';
import {SCHEDULED_MESSAGE_TTL_SECONDS} from '@fluxer/api/src/channel/services/ScheduledMessageService';
import {createRequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
import {ScheduledMessageRepository} from '@fluxer/api/src/user/repositories/ScheduledMessageRepository';
import type {WorkerDependencies} from '@fluxer/api/src/worker/WorkerDependencies';
export 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<void> {
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.validateMessageCanBeSent({
user,
channelId: scheduledMessage.channelId,
data: messageRequest,
});
await this.deps.channelService.messages.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<void> {
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;
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {type ChannelID, createGuildID, createUserID, type GuildID, type UserID} from '@fluxer/api/src/BrandedTypes';
import type {MessageRequest} from '@fluxer/api/src/channel/MessageTypes';
import {createRequestCache, type RequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
import {getUserSearchService} from '@fluxer/api/src/SearchFactory';
import {collectSystemDmTargets, type SystemDmTargetFilters} from '@fluxer/api/src/system_dm/TargetFinder';
import {UserChannelService} from '@fluxer/api/src/user/services/UserChannelService';
import type {WorkerDependencies} from '@fluxer/api/src/worker/WorkerDependencies';
export 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 SendSystemDmParams {
job_id: string;
}
export class SystemDmExecutor {
private readonly systemUserId = createUserID(0n);
private readonly userChannelService: UserChannelService;
constructor(
private readonly deps: WorkerDependencies,
private readonly logger: WorkerLogger,
) {
this.userChannelService = new UserChannelService(
this.deps.userRepository,
this.deps.userRepository,
this.deps.userRepository,
this.deps.channelService,
this.deps.channelRepository,
this.deps.gatewayService,
this.deps.mediaService,
this.deps.snowflakeService,
this.deps.userPermissionUtils,
this.deps.limitConfigService,
);
}
async execute(params: SendSystemDmParams): Promise<void> {
const jobId = this.parseJobId(params.job_id);
if (jobId === null) {
this.logger.warn('Invalid system DM job id', {payload: params});
return;
}
const job = await this.deps.systemDmJobRepository.getJob(jobId);
if (!job) {
this.logger.warn('System DM job missing', {jobId: jobId.toString()});
return;
}
if (job.status !== 'approved') {
this.logger.warn('Skipping system DM job in unexpected state', {jobId: jobId.toString(), status: job.status});
return;
}
await this.deps.systemDmJobRepository.patchJob(jobId, {status: 'running', updated_at: new Date()});
const userSearchService = getUserSearchService();
if (!userSearchService) {
await this.failJob(jobId, 'User search service unavailable');
return;
}
const systemUser = await this.deps.userRepository.findUnique(this.systemUserId);
if (!systemUser) {
await this.failJob(jobId, 'System user not found');
return;
}
const filters: SystemDmTargetFilters = {
...(job.registration_start != null && {registrationStart: job.registration_start}),
...(job.registration_end != null && {registrationEnd: job.registration_end}),
excludedGuildIds: this.convertExcludedGuildIds(job.excluded_guild_ids),
};
const recipients = await collectSystemDmTargets(
{userRepository: this.deps.userRepository, userSearchService},
filters,
);
if (recipients.length === 0) {
await this.completeJob(jobId, job.sent_count, job.failed_count, job.last_error);
return;
}
const requestCache = createRequestCache();
let sentCount = job.sent_count;
let failedCount = job.failed_count;
let lastError = job.last_error;
for (const recipientId of recipients) {
const channelId = await this.ensureDmChannel(recipientId, requestCache);
try {
const messageRequest: MessageRequest = {
content: job.content,
};
await this.deps.channelService.messages.sendMessage({
user: systemUser,
channelId,
data: messageRequest,
requestCache,
});
sentCount += 1;
} catch (error) {
failedCount += 1;
lastError = error instanceof Error ? error.message : 'Failed to send system DM';
this.logger.warn('System DM send failed', {jobId: jobId.toString(), error});
}
await this.deps.systemDmJobRepository.patchJob(jobId, {
sent_count: sentCount,
failed_count: failedCount,
last_error: lastError,
updated_at: new Date(),
});
}
await this.completeJob(jobId, sentCount, failedCount, lastError);
requestCache.clear();
}
private async ensureDmChannel(recipientId: UserID, requestCache: RequestCache): Promise<ChannelID> {
const channel = await this.userChannelService.ensureDmOpenForBothUsers({
userId: this.systemUserId,
recipientId,
userCacheService: this.deps.userCacheService,
requestCache,
});
return channel.id;
}
private convertExcludedGuildIds(value?: ReadonlySet<string>): Set<GuildID> {
const result = new Set<GuildID>();
if (!value) {
return result;
}
for (const id of value) {
try {
result.add(createGuildID(BigInt(id)));
} catch (error) {
this.logger.warn('Failed to convert excluded guild ID', {
guildId: id,
error: error instanceof Error ? error.message : String(error),
});
}
}
return result;
}
private async completeJob(jobId: bigint, sentCount: number, failedCount: number, lastError: string | null) {
await this.deps.systemDmJobRepository.patchJob(jobId, {
status: 'completed',
sent_count: sentCount,
failed_count: failedCount,
last_error: lastError,
updated_at: new Date(),
});
}
private async failJob(jobId: bigint, reason: string) {
await this.deps.systemDmJobRepository.patchJob(jobId, {
status: 'failed',
last_error: reason,
updated_at: new Date(),
});
}
private parseJobId(value: string): bigint | null {
try {
return BigInt(value);
} catch {
return null;
}
}
}