/*
* 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 .
*/
import {AttachmentDecayService} from '@fluxer/api/src/attachment/AttachmentDecayService';
import type {ChannelID, MessageID, UserID} from '@fluxer/api/src/BrandedTypes';
import type {IChannelRepository} from '@fluxer/api/src/channel/IChannelRepository';
import {mapMessageToResponse} from '@fluxer/api/src/channel/MessageMappers';
import type {MessageRequest, MessageUpdateRequest} from '@fluxer/api/src/channel/MessageTypes';
import type {ChannelService} from '@fluxer/api/src/channel/services/ChannelService';
import {
collectMessageAttachments,
isPersonalNotesChannel,
} from '@fluxer/api/src/channel/services/message/MessageHelpers';
import type {IMediaService} from '@fluxer/api/src/infrastructure/IMediaService';
import type {UserCacheService} from '@fluxer/api/src/infrastructure/UserCacheService';
import type {RequestCache} from '@fluxer/api/src/middleware/RequestCacheMiddleware';
import type {Message} from '@fluxer/api/src/models/Message';
import type {User} from '@fluxer/api/src/models/User';
import type {AttachmentDecayRow} from '@fluxer/api/src/types/AttachmentDecayTypes';
import {UnclaimedAccountCannotSendMessagesError} from '@fluxer/errors/src/domains/channel/UnclaimedAccountCannotSendMessagesError';
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
export class MessageRequestService {
private readonly decayService = new AttachmentDecayService();
constructor(
private readonly channelService: ChannelService,
private readonly channelRepository: IChannelRepository,
private readonly userCacheService: UserCacheService,
private readonly mediaService: IMediaService,
) {}
async listMessages(params: {
userId: UserID;
channelId: ChannelID;
query: {limit: number; before?: MessageID; after?: MessageID; around?: MessageID};
requestCache: RequestCache;
}): Promise> {
const messages = await this.channelService.getMessages({
userId: params.userId,
channelId: params.channelId,
limit: params.query.limit,
before: params.query.before,
after: params.query.after,
around: params.query.around,
});
const attachmentDecayMap = await this.buildAttachmentDecayMap(messages);
return Promise.all(
messages.map((message) =>
this.mapMessage({
message,
currentUserId: params.userId,
requestCache: params.requestCache,
attachmentDecayMap,
includeReactions: true,
}),
),
);
}
async getMessage(params: {
userId: UserID;
channelId: ChannelID;
messageId: MessageID;
requestCache: RequestCache;
}): Promise {
const message = await this.channelService.getMessage({
userId: params.userId,
channelId: params.channelId,
messageId: params.messageId,
});
const attachmentDecayMap = await this.buildAttachmentDecayMap([message]);
return this.mapMessage({
message,
currentUserId: params.userId,
requestCache: params.requestCache,
attachmentDecayMap,
includeReactions: true,
});
}
async sendMessage(params: {
user: User;
channelId: ChannelID;
data: MessageRequest;
requestCache: RequestCache;
}): Promise {
if (
params.user.isUnclaimedAccount() &&
!isPersonalNotesChannel({userId: params.user.id, channelId: params.channelId})
) {
throw new UnclaimedAccountCannotSendMessagesError();
}
const message = await this.channelService.sendMessage({
user: params.user,
channelId: params.channelId,
data: params.data,
requestCache: params.requestCache,
});
const attachmentDecayMap = await this.buildAttachmentDecayMap([message]);
return this.mapMessage({
message,
currentUserId: params.user.id,
nonce: params.data.nonce,
tts: params.data.tts,
requestCache: params.requestCache,
attachmentDecayMap,
});
}
async editMessage(params: {
userId: UserID;
channelId: ChannelID;
messageId: MessageID;
data: MessageUpdateRequest;
requestCache: RequestCache;
}): Promise {
const message = await this.channelService.editMessage({
userId: params.userId,
channelId: params.channelId,
messageId: params.messageId,
data: params.data,
requestCache: params.requestCache,
});
const attachmentDecayMap = await this.buildAttachmentDecayMap([message]);
return this.mapMessage({
message,
currentUserId: params.userId,
requestCache: params.requestCache,
attachmentDecayMap,
});
}
private async mapMessage(params: {
message: Message;
currentUserId: UserID;
requestCache: RequestCache;
attachmentDecayMap?: Map;
nonce?: string;
tts?: boolean;
includeReactions?: boolean;
}): Promise {
const baseParams = {
message: params.message,
currentUserId: params.currentUserId,
nonce: params.nonce,
tts: params.tts,
userCacheService: this.userCacheService,
requestCache: params.requestCache,
mediaService: this.mediaService,
attachmentDecayMap: params.attachmentDecayMap,
getReferencedMessage: (channelId: ChannelID, messageId: MessageID) =>
this.channelRepository.getMessage(channelId, messageId),
};
if (!params.includeReactions) {
return mapMessageToResponse(baseParams);
}
return mapMessageToResponse({
...baseParams,
getReactions: (channelId: ChannelID, messageId: MessageID) =>
this.channelService.getMessageReactions({
userId: params.currentUserId,
channelId,
messageId,
}),
setHasReaction: (channelId: ChannelID, messageId: MessageID, hasReaction: boolean) =>
this.channelService.setHasReaction(channelId, messageId, hasReaction),
});
}
private async buildAttachmentDecayMap(
messages: Array,
): Promise