mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
perf(message): reuse channel auth across send and edit (#2135)
This commit is contained in:
1 parent
1c135176d2
commit
e1bab2e353
5 files changed
+133
-16
No files matched your search
@@ -18,6 +18,7 @@ import type {IUserRepository} from '../../../user/IUserRepository';
|
||||
import {assertGuildMemberCanCommunicate} from '../../../utils/GuildCommunicationUtils';
|
||||
import type {MessageUpdateRequest} from '../../MessageTypes';
|
||||
import type {IChannelRepositoryAggregate} from '../../repositories/IChannelRepositoryAggregate';
|
||||
import type {AuthenticatedChannel} from '../AuthenticatedChannel';
|
||||
import type {MessageChannelAuthService} from './MessageChannelAuthService';
|
||||
import type {MessageDispatchService} from './MessageDispatchService';
|
||||
import type {MessageEmbedAttachmentResolver} from './MessageEmbedAttachmentResolver';
|
||||
@@ -32,6 +33,11 @@ const MESSAGE_LOCK_TTL_SECONDS = 5;
|
||||
const MESSAGE_LOCK_ACQUIRE_ATTEMPTS = 6;
|
||||
const MESSAGE_LOCK_RETRY_DELAY_MS = 50;
|
||||
|
||||
interface EditMessageResult {
|
||||
message: Message;
|
||||
authChannel: AuthenticatedChannel;
|
||||
}
|
||||
|
||||
interface MessageEditServiceDeps {
|
||||
channelRepository: IChannelRepositoryAggregate;
|
||||
userRepository: IUserRepository;
|
||||
@@ -61,11 +67,12 @@ export class MessageEditService {
|
||||
messageId: MessageID;
|
||||
data: MessageUpdateRequest;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Message> {
|
||||
const {channel, guild, hasPermission, member} = await this.deps.channelAuthService.getChannelAuthenticated({
|
||||
}): Promise<EditMessageResult> {
|
||||
const authChannel = await this.deps.channelAuthService.getChannelAuthenticated({
|
||||
userId,
|
||||
channelId,
|
||||
});
|
||||
const {channel, guild, hasPermission, member} = authChannel;
|
||||
const hasNewAttachments =
|
||||
data.attachments?.some(
|
||||
(attachment) =>
|
||||
@@ -131,7 +138,7 @@ export class MessageEditService {
|
||||
});
|
||||
}
|
||||
if (message.authorId !== userId) {
|
||||
return await this.withMessageLock(channelId, messageId, () =>
|
||||
const editedMessage = await this.withMessageLock(channelId, messageId, () =>
|
||||
this.deps.processingService.handleNonAuthorEdit({
|
||||
message,
|
||||
messageId,
|
||||
@@ -144,6 +151,7 @@ export class MessageEditService {
|
||||
dispatchService: this.deps.dispatchService,
|
||||
}),
|
||||
);
|
||||
return {message: editedMessage, authChannel};
|
||||
}
|
||||
const isBugHunterBot = !!user?.isBot && (user.flags & UserFlags.BUG_HUNTER) !== 0n;
|
||||
const updateResult = await this.withMessageLock(channelId, messageId, () =>
|
||||
@@ -184,7 +192,7 @@ export class MessageEditService {
|
||||
if (channel.indexedAt != null) {
|
||||
void this.deps.searchService.updateMessageIndex(updatedMessage);
|
||||
}
|
||||
return updatedMessage;
|
||||
return {message: updatedMessage, authChannel};
|
||||
}
|
||||
|
||||
private getEffectiveAllowedMentionsForEdit({
|
||||
|
||||
@@ -106,7 +106,7 @@ export class MessageRequestService {
|
||||
) {
|
||||
throw new UnclaimedAccountCannotSendMessagesError();
|
||||
}
|
||||
const message = await this.channelService.messages.send.sendMessage({
|
||||
const {message, authChannel} = await this.channelService.messages.send.sendMessage({
|
||||
user: params.user,
|
||||
channelId: params.channelId,
|
||||
data: params.data,
|
||||
@@ -115,6 +115,7 @@ export class MessageRequestService {
|
||||
const access = await this.channelService.messages.retrieval.getResponseAccessContext({
|
||||
userId: params.user.id,
|
||||
channelId: params.channelId,
|
||||
authChannel,
|
||||
});
|
||||
return this.responseDataService.buildMessage({
|
||||
userId: params.user.id,
|
||||
@@ -132,7 +133,7 @@ export class MessageRequestService {
|
||||
data: MessageUpdateRequest;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<MessageResponse> {
|
||||
const message = await this.channelService.messages.edit.editMessage({
|
||||
const {message, authChannel} = await this.channelService.messages.edit.editMessage({
|
||||
userId: params.userId,
|
||||
channelId: params.channelId,
|
||||
messageId: params.messageId,
|
||||
@@ -143,6 +144,7 @@ export class MessageRequestService {
|
||||
userId: params.userId,
|
||||
channelId: params.channelId,
|
||||
messageId: message.id,
|
||||
authChannel,
|
||||
});
|
||||
return this.responseDataService.buildMessage({
|
||||
userId: params.userId,
|
||||
|
||||
@@ -63,11 +63,14 @@ export class MessageRetrievalService {
|
||||
userId: UserID;
|
||||
channelId: ChannelID;
|
||||
messageId?: MessageID;
|
||||
authChannel?: AuthenticatedChannel;
|
||||
}): Promise<MessageResponseAccessContext> {
|
||||
const authChannel = await this.channelAuthService.getChannelAuthenticated({
|
||||
userId: params.userId,
|
||||
channelId: params.channelId,
|
||||
});
|
||||
const authChannel =
|
||||
params.authChannel ??
|
||||
(await this.channelAuthService.getChannelAuthenticated({
|
||||
userId: params.userId,
|
||||
channelId: params.channelId,
|
||||
}));
|
||||
if (params.messageId && !(await this.canAccessMessage(authChannel, params.messageId))) {
|
||||
throw new UnknownMessageError();
|
||||
}
|
||||
|
||||
@@ -94,6 +94,11 @@ interface MessageSendServiceDeps {
|
||||
directMessageSpamMitigationService: DirectMessageSpamMitigationService;
|
||||
}
|
||||
|
||||
interface SendMessageResult {
|
||||
message: Message;
|
||||
authChannel: AuthenticatedChannel;
|
||||
}
|
||||
|
||||
interface SendMentionData {
|
||||
flags: number;
|
||||
mentionUserIds: Array<UserID>;
|
||||
@@ -783,7 +788,7 @@ export class MessageSendService {
|
||||
channelId: ChannelID;
|
||||
data: MessageRequest;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Message> {
|
||||
}): Promise<SendMessageResult> {
|
||||
const authChannel = await this.deps.channelAuthService.getChannelAuthenticated({
|
||||
userId: user.id,
|
||||
channelId,
|
||||
@@ -792,7 +797,8 @@ export class MessageSendService {
|
||||
throw InputValidationError.fromCode('content', ValidationErrorCodes.MUST_START_SESSION_BEFORE_SENDING);
|
||||
}
|
||||
if (isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||
return this.sendPersonalNoteMessage({user, channelId, data, requestCache});
|
||||
const message = await this.sendPersonalNoteMessage({authChannel, user, channelId, data, requestCache});
|
||||
return {message, authChannel};
|
||||
}
|
||||
const {channel, guild, checkPermission, hasPermission, member} = authChannel;
|
||||
const {canEmbedLinks, canMentionEveryone, canAttachFiles} = await this.checkMessageSendPermissions({
|
||||
@@ -820,7 +826,7 @@ export class MessageSendService {
|
||||
expectedChannelId: channelId,
|
||||
});
|
||||
if (existingMessage) {
|
||||
return existingMessage;
|
||||
return {message: existingMessage, authChannel};
|
||||
}
|
||||
const referenceContext = await this.resolveReferenceContext({
|
||||
data,
|
||||
@@ -1031,7 +1037,7 @@ export class MessageSendService {
|
||||
if (searchIndexOptions && !suppressDmRecipientDelivery) {
|
||||
void this.deps.searchService.indexMessage(message, user.isBot, searchIndexOptions);
|
||||
}
|
||||
return message;
|
||||
return {message, authChannel};
|
||||
}
|
||||
|
||||
async sendWebhookMessage({
|
||||
@@ -1270,17 +1276,19 @@ export class MessageSendService {
|
||||
}
|
||||
|
||||
private async sendPersonalNoteMessage({
|
||||
authChannel,
|
||||
user,
|
||||
channelId,
|
||||
data,
|
||||
requestCache,
|
||||
}: {
|
||||
authChannel: AuthenticatedChannel;
|
||||
user: User;
|
||||
channelId: ChannelID;
|
||||
data: MessageRequest;
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Message> {
|
||||
const {channel} = await this.deps.channelAuthService.getChannelAuthenticated({userId: user.id, channelId});
|
||||
const {channel} = authChannel;
|
||||
const isForwardMessage = this.ensureMessageRequestIsValid({user, data, guildFeatures: null});
|
||||
this.deps.embedAttachmentResolver.validateAttachmentReferences({
|
||||
embeds: data.embeds,
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {MAX_MESSAGE_LENGTH_PREMIUM} from '@fluxer/constants/src/LimitConstants';
|
||||
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from 'vitest';
|
||||
import {authorizeBot, createTestBotAccount} from '../../bot/tests/BotTestUtils';
|
||||
import {ensureSessionStarted} from '../../message/tests/MessageTestUtils';
|
||||
import {getGatewayService} from '../../middleware/ServiceRegistry';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '../../test/TestConstants';
|
||||
import {createBuilder} from '../../test/TestRequestBuilder';
|
||||
@@ -46,6 +47,101 @@ describe('Message send permissions', () => {
|
||||
expect(sentMessage.author.id).toBe(member.userId);
|
||||
});
|
||||
|
||||
it('rejects sending and editing when VIEW_CHANNEL is denied', async () => {
|
||||
const {owner, members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0]!;
|
||||
await ensureSessionStarted(harness, member.token);
|
||||
const sentMessage = await createBuilder<MessageResponse>(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/messages`)
|
||||
.body({content: 'before the lockout'})
|
||||
.execute();
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: '0',
|
||||
deny: Permissions.VIEW_CHANNEL.toString(),
|
||||
});
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/messages`)
|
||||
.body({content: 'after the lockout'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_PERMISSIONS')
|
||||
.execute();
|
||||
await createBuilder(harness, member.token)
|
||||
.patch(`/channels/${systemChannel.id}/messages/${sentMessage.id}`)
|
||||
.body({content: 'after the lockout'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_PERMISSIONS')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('hides the edited message when the editor cannot read history', async () => {
|
||||
const {owner, members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0]!;
|
||||
await ensureSessionStarted(harness, member.token);
|
||||
const sentMessage = await createBuilder<MessageResponse>(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/messages`)
|
||||
.body({content: 'history is a luxury'})
|
||||
.execute();
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: (Permissions.VIEW_CHANNEL | Permissions.SEND_MESSAGES).toString(),
|
||||
deny: Permissions.READ_MESSAGE_HISTORY.toString(),
|
||||
});
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.patch(`/channels/${systemChannel.id}/messages/${sentMessage.id}`)
|
||||
.body({content: 'history is still a luxury'})
|
||||
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_MESSAGE')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('authenticates the channel once per send and once per edit', async () => {
|
||||
const {owner, members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0]!;
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: (Permissions.VIEW_CHANNEL | Permissions.SEND_MESSAGES | Permissions.READ_MESSAGE_HISTORY).toString(),
|
||||
deny: '0',
|
||||
});
|
||||
await ensureSessionStarted(harness, member.token);
|
||||
const gatewayService = getGatewayService();
|
||||
const getGuildData = vi.spyOn(gatewayService, 'getGuildData');
|
||||
const getGuildMember = vi.spyOn(gatewayService, 'getGuildMember');
|
||||
const checkPermission = vi.spyOn(gatewayService, 'checkPermission');
|
||||
const countViewChannelChecks = () =>
|
||||
checkPermission.mock.calls.filter((call) => call[0].permission === Permissions.VIEW_CHANNEL).length;
|
||||
|
||||
try {
|
||||
const sentMessage = await createBuilder<MessageResponse>(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/messages`)
|
||||
.body({content: 'authenticate me once'})
|
||||
.execute();
|
||||
const sendCounts = {
|
||||
guildData: getGuildData.mock.calls.length,
|
||||
guildMember: getGuildMember.mock.calls.length,
|
||||
viewChannel: countViewChannelChecks(),
|
||||
};
|
||||
getGuildData.mockClear();
|
||||
getGuildMember.mockClear();
|
||||
checkPermission.mockClear();
|
||||
await createBuilder<MessageResponse>(harness, member.token)
|
||||
.patch(`/channels/${systemChannel.id}/messages/${sentMessage.id}`)
|
||||
.body({content: 'authenticate me once again'})
|
||||
.execute();
|
||||
const editCounts = {
|
||||
guildData: getGuildData.mock.calls.length,
|
||||
guildMember: getGuildMember.mock.calls.length,
|
||||
viewChannel: countViewChannelChecks(),
|
||||
};
|
||||
|
||||
expect(sendCounts).toEqual({guildData: 1, guildMember: 1, viewChannel: 1});
|
||||
expect(editCounts).toEqual({guildData: 1, guildMember: 1, viewChannel: 1});
|
||||
} finally {
|
||||
getGuildData.mockRestore();
|
||||
getGuildMember.mockRestore();
|
||||
checkPermission.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('allows bot message content up to 4000 characters', async () => {
|
||||
const {owner, guild, systemChannel} = await setupTestGuildWithMembers(harness, 0);
|
||||
const botAccount = await createTestBotAccount(harness);
|
||||
|
||||
Reference in new issue
Block a user