mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +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 {assertGuildMemberCanCommunicate} from '../../../utils/GuildCommunicationUtils';
|
||||||
import type {MessageUpdateRequest} from '../../MessageTypes';
|
import type {MessageUpdateRequest} from '../../MessageTypes';
|
||||||
import type {IChannelRepositoryAggregate} from '../../repositories/IChannelRepositoryAggregate';
|
import type {IChannelRepositoryAggregate} from '../../repositories/IChannelRepositoryAggregate';
|
||||||
|
import type {AuthenticatedChannel} from '../AuthenticatedChannel';
|
||||||
import type {MessageChannelAuthService} from './MessageChannelAuthService';
|
import type {MessageChannelAuthService} from './MessageChannelAuthService';
|
||||||
import type {MessageDispatchService} from './MessageDispatchService';
|
import type {MessageDispatchService} from './MessageDispatchService';
|
||||||
import type {MessageEmbedAttachmentResolver} from './MessageEmbedAttachmentResolver';
|
import type {MessageEmbedAttachmentResolver} from './MessageEmbedAttachmentResolver';
|
||||||
@@ -32,6 +33,11 @@ const MESSAGE_LOCK_TTL_SECONDS = 5;
|
|||||||
const MESSAGE_LOCK_ACQUIRE_ATTEMPTS = 6;
|
const MESSAGE_LOCK_ACQUIRE_ATTEMPTS = 6;
|
||||||
const MESSAGE_LOCK_RETRY_DELAY_MS = 50;
|
const MESSAGE_LOCK_RETRY_DELAY_MS = 50;
|
||||||
|
|
||||||
|
interface EditMessageResult {
|
||||||
|
message: Message;
|
||||||
|
authChannel: AuthenticatedChannel;
|
||||||
|
}
|
||||||
|
|
||||||
interface MessageEditServiceDeps {
|
interface MessageEditServiceDeps {
|
||||||
channelRepository: IChannelRepositoryAggregate;
|
channelRepository: IChannelRepositoryAggregate;
|
||||||
userRepository: IUserRepository;
|
userRepository: IUserRepository;
|
||||||
@@ -61,11 +67,12 @@ export class MessageEditService {
|
|||||||
messageId: MessageID;
|
messageId: MessageID;
|
||||||
data: MessageUpdateRequest;
|
data: MessageUpdateRequest;
|
||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<Message> {
|
}): Promise<EditMessageResult> {
|
||||||
const {channel, guild, hasPermission, member} = await this.deps.channelAuthService.getChannelAuthenticated({
|
const authChannel = await this.deps.channelAuthService.getChannelAuthenticated({
|
||||||
userId,
|
userId,
|
||||||
channelId,
|
channelId,
|
||||||
});
|
});
|
||||||
|
const {channel, guild, hasPermission, member} = authChannel;
|
||||||
const hasNewAttachments =
|
const hasNewAttachments =
|
||||||
data.attachments?.some(
|
data.attachments?.some(
|
||||||
(attachment) =>
|
(attachment) =>
|
||||||
@@ -131,7 +138,7 @@ export class MessageEditService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (message.authorId !== userId) {
|
if (message.authorId !== userId) {
|
||||||
return await this.withMessageLock(channelId, messageId, () =>
|
const editedMessage = await this.withMessageLock(channelId, messageId, () =>
|
||||||
this.deps.processingService.handleNonAuthorEdit({
|
this.deps.processingService.handleNonAuthorEdit({
|
||||||
message,
|
message,
|
||||||
messageId,
|
messageId,
|
||||||
@@ -144,6 +151,7 @@ export class MessageEditService {
|
|||||||
dispatchService: this.deps.dispatchService,
|
dispatchService: this.deps.dispatchService,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
return {message: editedMessage, authChannel};
|
||||||
}
|
}
|
||||||
const isBugHunterBot = !!user?.isBot && (user.flags & UserFlags.BUG_HUNTER) !== 0n;
|
const isBugHunterBot = !!user?.isBot && (user.flags & UserFlags.BUG_HUNTER) !== 0n;
|
||||||
const updateResult = await this.withMessageLock(channelId, messageId, () =>
|
const updateResult = await this.withMessageLock(channelId, messageId, () =>
|
||||||
@@ -184,7 +192,7 @@ export class MessageEditService {
|
|||||||
if (channel.indexedAt != null) {
|
if (channel.indexedAt != null) {
|
||||||
void this.deps.searchService.updateMessageIndex(updatedMessage);
|
void this.deps.searchService.updateMessageIndex(updatedMessage);
|
||||||
}
|
}
|
||||||
return updatedMessage;
|
return {message: updatedMessage, authChannel};
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEffectiveAllowedMentionsForEdit({
|
private getEffectiveAllowedMentionsForEdit({
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export class MessageRequestService {
|
|||||||
) {
|
) {
|
||||||
throw new UnclaimedAccountCannotSendMessagesError();
|
throw new UnclaimedAccountCannotSendMessagesError();
|
||||||
}
|
}
|
||||||
const message = await this.channelService.messages.send.sendMessage({
|
const {message, authChannel} = await this.channelService.messages.send.sendMessage({
|
||||||
user: params.user,
|
user: params.user,
|
||||||
channelId: params.channelId,
|
channelId: params.channelId,
|
||||||
data: params.data,
|
data: params.data,
|
||||||
@@ -115,6 +115,7 @@ export class MessageRequestService {
|
|||||||
const access = await this.channelService.messages.retrieval.getResponseAccessContext({
|
const access = await this.channelService.messages.retrieval.getResponseAccessContext({
|
||||||
userId: params.user.id,
|
userId: params.user.id,
|
||||||
channelId: params.channelId,
|
channelId: params.channelId,
|
||||||
|
authChannel,
|
||||||
});
|
});
|
||||||
return this.responseDataService.buildMessage({
|
return this.responseDataService.buildMessage({
|
||||||
userId: params.user.id,
|
userId: params.user.id,
|
||||||
@@ -132,7 +133,7 @@ export class MessageRequestService {
|
|||||||
data: MessageUpdateRequest;
|
data: MessageUpdateRequest;
|
||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<MessageResponse> {
|
}): Promise<MessageResponse> {
|
||||||
const message = await this.channelService.messages.edit.editMessage({
|
const {message, authChannel} = await this.channelService.messages.edit.editMessage({
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
channelId: params.channelId,
|
channelId: params.channelId,
|
||||||
messageId: params.messageId,
|
messageId: params.messageId,
|
||||||
@@ -143,6 +144,7 @@ export class MessageRequestService {
|
|||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
channelId: params.channelId,
|
channelId: params.channelId,
|
||||||
messageId: message.id,
|
messageId: message.id,
|
||||||
|
authChannel,
|
||||||
});
|
});
|
||||||
return this.responseDataService.buildMessage({
|
return this.responseDataService.buildMessage({
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
|
|||||||
@@ -63,11 +63,14 @@ export class MessageRetrievalService {
|
|||||||
userId: UserID;
|
userId: UserID;
|
||||||
channelId: ChannelID;
|
channelId: ChannelID;
|
||||||
messageId?: MessageID;
|
messageId?: MessageID;
|
||||||
|
authChannel?: AuthenticatedChannel;
|
||||||
}): Promise<MessageResponseAccessContext> {
|
}): Promise<MessageResponseAccessContext> {
|
||||||
const authChannel = await this.channelAuthService.getChannelAuthenticated({
|
const authChannel =
|
||||||
userId: params.userId,
|
params.authChannel ??
|
||||||
channelId: params.channelId,
|
(await this.channelAuthService.getChannelAuthenticated({
|
||||||
});
|
userId: params.userId,
|
||||||
|
channelId: params.channelId,
|
||||||
|
}));
|
||||||
if (params.messageId && !(await this.canAccessMessage(authChannel, params.messageId))) {
|
if (params.messageId && !(await this.canAccessMessage(authChannel, params.messageId))) {
|
||||||
throw new UnknownMessageError();
|
throw new UnknownMessageError();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ interface MessageSendServiceDeps {
|
|||||||
directMessageSpamMitigationService: DirectMessageSpamMitigationService;
|
directMessageSpamMitigationService: DirectMessageSpamMitigationService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SendMessageResult {
|
||||||
|
message: Message;
|
||||||
|
authChannel: AuthenticatedChannel;
|
||||||
|
}
|
||||||
|
|
||||||
interface SendMentionData {
|
interface SendMentionData {
|
||||||
flags: number;
|
flags: number;
|
||||||
mentionUserIds: Array<UserID>;
|
mentionUserIds: Array<UserID>;
|
||||||
@@ -783,7 +788,7 @@ export class MessageSendService {
|
|||||||
channelId: ChannelID;
|
channelId: ChannelID;
|
||||||
data: MessageRequest;
|
data: MessageRequest;
|
||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<Message> {
|
}): Promise<SendMessageResult> {
|
||||||
const authChannel = await this.deps.channelAuthService.getChannelAuthenticated({
|
const authChannel = await this.deps.channelAuthService.getChannelAuthenticated({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
channelId,
|
channelId,
|
||||||
@@ -792,7 +797,8 @@ export class MessageSendService {
|
|||||||
throw InputValidationError.fromCode('content', ValidationErrorCodes.MUST_START_SESSION_BEFORE_SENDING);
|
throw InputValidationError.fromCode('content', ValidationErrorCodes.MUST_START_SESSION_BEFORE_SENDING);
|
||||||
}
|
}
|
||||||
if (isPersonalNotesChannel({userId: user.id, channelId})) {
|
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 {channel, guild, checkPermission, hasPermission, member} = authChannel;
|
||||||
const {canEmbedLinks, canMentionEveryone, canAttachFiles} = await this.checkMessageSendPermissions({
|
const {canEmbedLinks, canMentionEveryone, canAttachFiles} = await this.checkMessageSendPermissions({
|
||||||
@@ -820,7 +826,7 @@ export class MessageSendService {
|
|||||||
expectedChannelId: channelId,
|
expectedChannelId: channelId,
|
||||||
});
|
});
|
||||||
if (existingMessage) {
|
if (existingMessage) {
|
||||||
return existingMessage;
|
return {message: existingMessage, authChannel};
|
||||||
}
|
}
|
||||||
const referenceContext = await this.resolveReferenceContext({
|
const referenceContext = await this.resolveReferenceContext({
|
||||||
data,
|
data,
|
||||||
@@ -1031,7 +1037,7 @@ export class MessageSendService {
|
|||||||
if (searchIndexOptions && !suppressDmRecipientDelivery) {
|
if (searchIndexOptions && !suppressDmRecipientDelivery) {
|
||||||
void this.deps.searchService.indexMessage(message, user.isBot, searchIndexOptions);
|
void this.deps.searchService.indexMessage(message, user.isBot, searchIndexOptions);
|
||||||
}
|
}
|
||||||
return message;
|
return {message, authChannel};
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendWebhookMessage({
|
async sendWebhookMessage({
|
||||||
@@ -1270,17 +1276,19 @@ export class MessageSendService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async sendPersonalNoteMessage({
|
private async sendPersonalNoteMessage({
|
||||||
|
authChannel,
|
||||||
user,
|
user,
|
||||||
channelId,
|
channelId,
|
||||||
data,
|
data,
|
||||||
requestCache,
|
requestCache,
|
||||||
}: {
|
}: {
|
||||||
|
authChannel: AuthenticatedChannel;
|
||||||
user: User;
|
user: User;
|
||||||
channelId: ChannelID;
|
channelId: ChannelID;
|
||||||
data: MessageRequest;
|
data: MessageRequest;
|
||||||
requestCache: RequestCache;
|
requestCache: RequestCache;
|
||||||
}): Promise<Message> {
|
}): Promise<Message> {
|
||||||
const {channel} = await this.deps.channelAuthService.getChannelAuthenticated({userId: user.id, channelId});
|
const {channel} = authChannel;
|
||||||
const isForwardMessage = this.ensureMessageRequestIsValid({user, data, guildFeatures: null});
|
const isForwardMessage = this.ensureMessageRequestIsValid({user, data, guildFeatures: null});
|
||||||
this.deps.embedAttachmentResolver.validateAttachmentReferences({
|
this.deps.embedAttachmentResolver.validateAttachmentReferences({
|
||||||
embeds: data.embeds,
|
embeds: data.embeds,
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
|
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||||
import {MAX_MESSAGE_LENGTH_PREMIUM} from '@fluxer/constants/src/LimitConstants';
|
import {MAX_MESSAGE_LENGTH_PREMIUM} from '@fluxer/constants/src/LimitConstants';
|
||||||
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
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 {authorizeBot, createTestBotAccount} from '../../bot/tests/BotTestUtils';
|
||||||
import {ensureSessionStarted} from '../../message/tests/MessageTestUtils';
|
import {ensureSessionStarted} from '../../message/tests/MessageTestUtils';
|
||||||
|
import {getGatewayService} from '../../middleware/ServiceRegistry';
|
||||||
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
|
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
|
||||||
import {HTTP_STATUS} from '../../test/TestConstants';
|
import {HTTP_STATUS} from '../../test/TestConstants';
|
||||||
import {createBuilder} from '../../test/TestRequestBuilder';
|
import {createBuilder} from '../../test/TestRequestBuilder';
|
||||||
@@ -46,6 +47,101 @@ describe('Message send permissions', () => {
|
|||||||
expect(sentMessage.author.id).toBe(member.userId);
|
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 () => {
|
it('allows bot message content up to 4000 characters', async () => {
|
||||||
const {owner, guild, systemChannel} = await setupTestGuildWithMembers(harness, 0);
|
const {owner, guild, systemChannel} = await setupTestGuildWithMembers(harness, 0);
|
||||||
const botAccount = await createTestBotAccount(harness);
|
const botAccount = await createTestBotAccount(harness);
|
||||||
|
|||||||
Reference in new issue
Block a user