/* * 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 | undefined> { const allAttachments = messages.flatMap((message) => collectMessageAttachments(message)); if (allAttachments.length === 0) { return undefined; } return this.decayService.fetchMetadata(allAttachments.map((att) => ({attachmentId: att.id}))); } }