mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-11 21:18:29 +09:00
298 lines
10 KiB
TypeScript
298 lines
10 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {type ChannelID, createChannelID, createGuildID, type MessageID, type UserID} from '@app/api/BrandedTypes';
|
|
import {mapChannelToResponse} from '@app/api/channel/ChannelMappers';
|
|
import type {MessageRequest, MessageUpdateRequest} from '@app/api/channel/MessageTypes';
|
|
import type {IChannelRepositoryAggregate} from '@app/api/channel/repositories/IChannelRepositoryAggregate';
|
|
import type {MessageDispatchService} from '@app/api/channel/services/message/MessageDispatchService';
|
|
import {isPersonalNotesChannel} from '@app/api/channel/services/message/MessageHelpers';
|
|
import type {MessageMentionService} from '@app/api/channel/services/message/MessageMentionService';
|
|
import type {MessagePersistenceService} from '@app/api/channel/services/message/MessagePersistenceService';
|
|
import {incrementDmMentionCounts} from '@app/api/channel/services/message/ReadStateHelpers';
|
|
import type {GatewayChannelMention, IGatewayService} from '@app/api/infrastructure/IGatewayService';
|
|
import type {UserCacheService} from '@app/api/infrastructure/UserCacheService';
|
|
import {Logger} from '@app/api/Logger';
|
|
import type {RequestCache} from '@app/api/middleware/RequestCacheMiddleware';
|
|
import {Channel} from '@app/api/models/Channel';
|
|
import type {Message} from '@app/api/models/Message';
|
|
import type {User} from '@app/api/models/User';
|
|
import type {ReadStateService} from '@app/api/read_state/ReadStateService';
|
|
import type {IUserRepository} from '@app/api/user/IUserRepository';
|
|
import {ChannelTypes, MessageFlags} from '@fluxer/constants/src/ChannelConstants';
|
|
import {CannotEditOtherUserMessageError} from '@fluxer/errors/src/domains/channel/CannotEditOtherUserMessageError';
|
|
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
|
import type {AllowedMentionsRequest} from '@fluxer/schema/src/domains/message/SharedMessageSchemas';
|
|
|
|
interface RecipientOpenState {
|
|
recipientId: UserID;
|
|
isOpen: boolean;
|
|
}
|
|
|
|
interface MentionProcessingResult {
|
|
message: Message;
|
|
mentionChannels: Array<GatewayChannelMention>;
|
|
}
|
|
|
|
function channelWithLastMessageId(channel: Channel, messageId: MessageID): Channel {
|
|
if (channel.lastMessageId != null && channel.lastMessageId >= messageId) {
|
|
return channel;
|
|
}
|
|
return new Channel({...channel.toRow(), last_message_id: messageId});
|
|
}
|
|
|
|
export class MessageProcessingService {
|
|
constructor(
|
|
private channelRepository: IChannelRepositoryAggregate,
|
|
private userRepository: IUserRepository,
|
|
private userCacheService: UserCacheService,
|
|
private gatewayService: IGatewayService,
|
|
private readStateService: ReadStateService,
|
|
private mentionService: MessageMentionService,
|
|
) {}
|
|
|
|
async processMessageAfterCreation(params: {
|
|
message: Message;
|
|
channel: Channel;
|
|
guild: GuildResponse | null;
|
|
user: User;
|
|
data: MessageRequest;
|
|
referencedMessage: Message | null;
|
|
mentionHere?: boolean;
|
|
}): Promise<void> {
|
|
const {message, guild, user, mentionHere = false} = params;
|
|
await this.mentionService.handleMentionTasks({
|
|
guildId: guild ? createGuildID(BigInt(guild.id)) : null,
|
|
message,
|
|
authorId: user.id,
|
|
mentionHere,
|
|
});
|
|
}
|
|
|
|
async updateDMRecipients({
|
|
channel,
|
|
channelId,
|
|
messageId,
|
|
requestCache,
|
|
}: {
|
|
channel: Channel;
|
|
channelId: ChannelID;
|
|
messageId: MessageID;
|
|
requestCache: RequestCache;
|
|
}): Promise<void> {
|
|
if (channel.guildId || channel.type !== ChannelTypes.DM) return;
|
|
if (channel.recipientIds?.size !== 2) return;
|
|
const recipientIds = Array.from(channel.recipientIds);
|
|
const openStates = await this.batchCheckDmChannelOpen(recipientIds, channelId);
|
|
const closedRecipients = openStates.filter((state) => !state.isOpen);
|
|
if (closedRecipients.length === 0) return;
|
|
const snapshotChannel = channelWithLastMessageId(channel, messageId);
|
|
await Promise.all(
|
|
closedRecipients.map((state) =>
|
|
this.openDmAndDispatch({
|
|
recipientId: state.recipientId,
|
|
channel: snapshotChannel,
|
|
requestCache,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
private async batchCheckDmChannelOpen(
|
|
recipientIds: Array<UserID>,
|
|
channelId: ChannelID,
|
|
): Promise<Array<RecipientOpenState>> {
|
|
return Promise.all(
|
|
recipientIds.map(async (recipientId) => ({
|
|
recipientId,
|
|
isOpen: await this.userRepository.isDmChannelOpen(recipientId, channelId),
|
|
})),
|
|
);
|
|
}
|
|
|
|
private async openDmAndDispatch(params: {
|
|
recipientId: UserID;
|
|
channel: Channel;
|
|
requestCache: RequestCache;
|
|
}): Promise<void> {
|
|
const {recipientId, channel, requestCache} = params;
|
|
await this.userRepository.openPrivateChannelForUser(recipientId, channel);
|
|
const channelResponse = await mapChannelToResponse({
|
|
channel,
|
|
currentUserId: recipientId,
|
|
userCacheService: this.userCacheService,
|
|
requestCache,
|
|
});
|
|
await this.gatewayService.dispatchPresence({
|
|
userId: recipientId,
|
|
event: 'CHANNEL_CREATE',
|
|
data: channelResponse,
|
|
});
|
|
}
|
|
|
|
async updateReadStates({
|
|
user,
|
|
guild,
|
|
channel,
|
|
channelId,
|
|
messageId,
|
|
}: {
|
|
user: User;
|
|
guild: GuildResponse | null;
|
|
channel: Channel;
|
|
channelId: ChannelID;
|
|
messageId: MessageID;
|
|
}): Promise<void> {
|
|
if (!guild) {
|
|
const recipients = await this.userRepository.listUsers(Array.from(channel.recipientIds));
|
|
await incrementDmMentionCounts({
|
|
readStateService: this.readStateService,
|
|
userRepository: this.userRepository,
|
|
user,
|
|
recipients,
|
|
channelId,
|
|
messageId,
|
|
});
|
|
}
|
|
}
|
|
|
|
async handleMentions(params: {
|
|
channel: Channel;
|
|
message: Message;
|
|
referencedMessageOnSend: Message | null;
|
|
allowedMentions: AllowedMentionsRequest | null;
|
|
guild?: GuildResponse | null;
|
|
canMentionEveryone: boolean;
|
|
canMentionRoles: boolean;
|
|
}): Promise<MentionProcessingResult> {
|
|
const {channel, message, referencedMessageOnSend, allowedMentions, guild, canMentionEveryone, canMentionRoles} =
|
|
params;
|
|
if (message.authorId != null && isPersonalNotesChannel({userId: message.authorId, channelId: channel.id})) {
|
|
return {message, mentionChannels: []};
|
|
}
|
|
const content = message.content ?? '';
|
|
const mentions = await this.mentionService.extractMentions({
|
|
content,
|
|
referencedMessage: referencedMessageOnSend,
|
|
message,
|
|
channelType: channel.type,
|
|
allowedMentions,
|
|
guild,
|
|
canMentionEveryone,
|
|
});
|
|
const {validUserIds, validRoleIds, validChannelMentions} = await this.mentionService.validateMentions({
|
|
userMentions: mentions.userMentions,
|
|
roleMentions: mentions.roleMentions,
|
|
channelMentions: mentions.channelMentions,
|
|
channel,
|
|
message,
|
|
guild,
|
|
canMentionRoles,
|
|
});
|
|
const updatedMessageData = {
|
|
...message.toRow(),
|
|
flags: mentions.flags,
|
|
mention_users: validUserIds.length > 0 ? new Set(validUserIds) : null,
|
|
mention_roles: validRoleIds.length > 0 ? new Set(validRoleIds) : null,
|
|
mention_channels:
|
|
validChannelMentions.length > 0
|
|
? new Set(validChannelMentions.map((mentionedChannel) => createChannelID(BigInt(mentionedChannel.id))))
|
|
: null,
|
|
mention_everyone: mentions.mentionsEveryone,
|
|
};
|
|
const updatedMessage = await this.channelRepository.messages.upsertMessage(updatedMessageData, message.toRow());
|
|
return {message: updatedMessage, mentionChannels: validChannelMentions};
|
|
}
|
|
|
|
async repairMentionsOnRead(message: Message, sourceChannel?: Channel): Promise<Message> {
|
|
if ((message.flags & MessageFlags.IS_CROSSPOST) !== 0) {
|
|
return message;
|
|
}
|
|
if (
|
|
message.mentionedUserIds.size === 0 &&
|
|
message.mentionedRoleIds.size === 0 &&
|
|
message.mentionedChannelIds.size === 0 &&
|
|
!message.mentionEveryone
|
|
) {
|
|
const hasChannelMentionSyntax = (message.content ?? '').includes('<#');
|
|
if (!hasChannelMentionSyntax) {
|
|
return message;
|
|
}
|
|
}
|
|
const referencedMessage =
|
|
message.reference?.messageId && message.mentionedUserIds.size > 0
|
|
? await this.channelRepository.messages.getMessage(message.reference.channelId, message.reference.messageId)
|
|
: null;
|
|
const repair = await this.mentionService.buildReadRepairMentionData({message, referencedMessage});
|
|
if (!repair.changed) {
|
|
return message;
|
|
}
|
|
const resolvedSourceChannel =
|
|
sourceChannel ??
|
|
(repair.channelMentions.size > 0 || message.mentionedChannelIds.size > 0
|
|
? await this.channelRepository.channelData.findUnique(message.channelId)
|
|
: null);
|
|
const validChannelMentions = resolvedSourceChannel
|
|
? await this.mentionService.validateChannelMentions({
|
|
channelMentions: repair.channelMentions,
|
|
channel: resolvedSourceChannel,
|
|
})
|
|
: [];
|
|
const updatedMessageData = {
|
|
...message.toRow(),
|
|
mention_users: repair.userMentions.size > 0 ? repair.userMentions : null,
|
|
mention_roles: repair.roleMentions.size > 0 ? repair.roleMentions : null,
|
|
mention_channels:
|
|
validChannelMentions.length > 0
|
|
? new Set(validChannelMentions.map((mentionedChannel) => createChannelID(BigInt(mentionedChannel.id))))
|
|
: null,
|
|
mention_everyone: repair.mentionsEveryone,
|
|
};
|
|
try {
|
|
return await this.channelRepository.messages.upsertMessage(updatedMessageData, message.toRow());
|
|
} catch (error) {
|
|
Logger.warn(
|
|
{
|
|
error,
|
|
channelId: message.channelId.toString(),
|
|
messageId: message.id.toString(),
|
|
},
|
|
'Failed to repair message mentions during read',
|
|
);
|
|
return message;
|
|
}
|
|
}
|
|
|
|
async handleNonAuthorEdit(params: {
|
|
message: Message;
|
|
messageId: MessageID;
|
|
data: MessageUpdateRequest;
|
|
guild: GuildResponse | null;
|
|
hasPermission: (permission: bigint) => Promise<boolean>;
|
|
channel: Channel;
|
|
requestCache: RequestCache;
|
|
persistenceService: MessagePersistenceService;
|
|
dispatchService: MessageDispatchService;
|
|
}): Promise<Message> {
|
|
const {message, data, guild, hasPermission, channel, requestCache, persistenceService, dispatchService} = params;
|
|
const editResult = await persistenceService.handleNonAuthorEdit({
|
|
message,
|
|
data,
|
|
guild,
|
|
hasPermission,
|
|
});
|
|
if (editResult.canEdit && (editResult.updatedFlags !== undefined || editResult.updatedAttachments !== undefined)) {
|
|
const updatedRowData = {...message.toRow()};
|
|
if (editResult.updatedFlags !== undefined) {
|
|
updatedRowData.flags = editResult.updatedFlags;
|
|
}
|
|
if (editResult.updatedAttachments !== undefined) {
|
|
updatedRowData.attachments = editResult.updatedAttachments;
|
|
}
|
|
const updatedMessage = await this.channelRepository.messages.upsertMessage(updatedRowData, message.toRow());
|
|
await dispatchService.dispatchMessageUpdate({channel, message: updatedMessage, requestCache});
|
|
return updatedMessage;
|
|
}
|
|
throw new CannotEditOtherUserMessageError();
|
|
}
|
|
}
|