fix(api): always allow opening a dm channel (#2423)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-03 16:35:01 +02:00
1 parent 902dd60ff9
commit 9c1b38aeaf
4 files changed
+22 -51

No files matched your search

@@ -31,7 +31,7 @@ describe('DM Blocking Behaviors', () => {
await harness?.shutdown(); await harness?.shutdown();
}); });
describe('DM Creation Blocking', () => { describe('DM Creation Blocking', () => {
it('prevents DM creation when the other user has blocked you, even with mutual guild', async () => { it('allows DM creation when the other user has blocked you', async () => {
const user1 = await createTestAccount(harness); const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness); const user2 = await createTestAccount(harness);
const guild = await createGuild(harness, user1.token, 'Test Community'); const guild = await createGuild(harness, user1.token, 'Test Community');
@@ -42,10 +42,10 @@ describe('DM Blocking Behaviors', () => {
await createBuilder(harness, user2.token) await createBuilder(harness, user2.token)
.post('/users/@me/channels') .post('/users/@me/channels')
.body({recipient_id: user1.userId}) .body({recipient_id: user1.userId})
.expect(HTTP_STATUS.BAD_REQUEST) .expect(HTTP_STATUS.OK)
.execute(); .execute();
}); });
it('prevents DM creation with someone you have blocked, even with mutual guild', async () => { it('allows DM creation with someone you have blocked', async () => {
const user1 = await createTestAccount(harness); const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness); const user2 = await createTestAccount(harness);
const guild = await createGuild(harness, user1.token, 'Test Community'); const guild = await createGuild(harness, user1.token, 'Test Community');
@@ -56,7 +56,7 @@ describe('DM Blocking Behaviors', () => {
await createBuilder(harness, user1.token) await createBuilder(harness, user1.token)
.post('/users/@me/channels') .post('/users/@me/channels')
.body({recipient_id: user2.userId}) .body({recipient_id: user2.userId})
.expect(HTTP_STATUS.BAD_REQUEST) .expect(HTTP_STATUS.OK)
.execute(); .execute();
}); });
}); });
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils'; import {createTestAccount} from '../../auth/tests/AuthTestUtils';
import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness'; import {type ApiTestHarness, createApiTestHarness} from '../../test/ApiTestHarness';
import {createBuilder} from '../../test/TestRequestBuilder'; import {createDmChannel} from './ChannelTestUtils';
describe('DM creation requires friendship or mutual guild', () => { describe('DM creation allowed with strangers', () => {
let harness: ApiTestHarness; let harness: ApiTestHarness;
beforeAll(async () => { beforeAll(async () => {
harness = await createApiTestHarness(); harness = await createApiTestHarness();
@@ -16,13 +16,11 @@ describe('DM creation requires friendship or mutual guild', () => {
afterAll(async () => { afterAll(async () => {
await harness?.shutdown(); await harness?.shutdown();
}); });
it('prevents DM creation with strangers who you do not share a guild with and are not friends with', async () => { it('allows opening a DM with a stranger who shares no guild and is not a friend', async () => {
const user1 = await createTestAccount(harness); const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness); const user2 = await createTestAccount(harness);
await createBuilder(harness, user1.token) const dm = await createDmChannel(harness, user1.token, user2.userId);
.post('/users/@me/channels') expect(dm.id).toBeTruthy();
.body({recipient_id: user2.userId}) expect(dm.id.length).toBeGreaterThan(0);
.expect(400)
.execute();
}); });
}); });
@@ -5,7 +5,6 @@ import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata';
import {MAX_GROUP_DM_RECIPIENTS, MAX_GROUP_DMS_PER_USER} from '@fluxer/constants/src/LimitConstants'; import {MAX_GROUP_DM_RECIPIENTS, MAX_GROUP_DMS_PER_USER} from '@fluxer/constants/src/LimitConstants';
import {RelationshipTypes} from '@fluxer/constants/src/UserConstants'; import {RelationshipTypes} from '@fluxer/constants/src/UserConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {CannotSendMessagesToUserError} from '@fluxer/errors/src/domains/channel/CannotSendMessagesToUserError';
import type {GroupDmUnaddableRecipient} from '@fluxer/errors/src/domains/channel/GroupDmRecipientsNotAddableError'; import type {GroupDmUnaddableRecipient} from '@fluxer/errors/src/domains/channel/GroupDmRecipientsNotAddableError';
import {GroupDmRecipientsNotAddableError} from '@fluxer/errors/src/domains/channel/GroupDmRecipientsNotAddableError'; import {GroupDmRecipientsNotAddableError} from '@fluxer/errors/src/domains/channel/GroupDmRecipientsNotAddableError';
import {MaxGroupDmRecipientsError} from '@fluxer/errors/src/domains/channel/MaxGroupDmRecipientsError'; import {MaxGroupDmRecipientsError} from '@fluxer/errors/src/domains/channel/MaxGroupDmRecipientsError';
@@ -30,7 +29,6 @@ import {
createMessageResponseDataService, createMessageResponseDataService,
messageResponseAccessForGuild, messageResponseAccessForGuild,
} from '../../channel/services/message/MessageResponseDataService'; } from '../../channel/services/message/MessageResponseDataService';
import {SYSTEM_USER_ID} from '../../constants/Core';
import type {IGatewayService} from '../../infrastructure/IGatewayService'; import type {IGatewayService} from '../../infrastructure/IGatewayService';
import type {ISnowflakeService} from '../../infrastructure/ISnowflakeService'; import type {ISnowflakeService} from '../../infrastructure/ISnowflakeService';
import type {UserCacheService} from '../../infrastructure/UserCacheService'; import type {UserCacheService} from '../../infrastructure/UserCacheService';
@@ -45,7 +43,6 @@ import type {UserPermissionUtils} from '../../utils/UserPermissionUtils';
import type {IUserAccountRepository} from '../repositories/IUserAccountRepository'; import type {IUserAccountRepository} from '../repositories/IUserAccountRepository';
import type {IUserChannelRepository} from '../repositories/IUserChannelRepository'; import type {IUserChannelRepository} from '../repositories/IUserChannelRepository';
import type {IUserRelationshipRepository} from '../repositories/IUserRelationshipRepository'; import type {IUserRelationshipRepository} from '../repositories/IUserRelationshipRepository';
import {isBugHunterBotUser} from '../UserHelpers';
import type {DirectMessageSpamMitigationService} from './DirectMessageSpamMitigationService'; import type {DirectMessageSpamMitigationService} from './DirectMessageSpamMitigationService';
import {createDirectMessageSpamMitigationService} from './DirectMessageSpamMitigationService'; import {createDirectMessageSpamMitigationService} from './DirectMessageSpamMitigationService';
@@ -180,13 +177,8 @@ export class UserChannelService {
} }
const targetUser = await this.userRepository.findUnique(recipientId); const targetUser = await this.userRepository.findUnique(recipientId);
if (!targetUser) throw new UnknownUserError(); if (!targetUser) throw new UnknownUserError();
if (recipientId === SYSTEM_USER_ID) {
return await this.createNewDMChannel({userId, recipientId, userCacheService, requestCache}); return await this.createNewDMChannel({userId, recipientId, userCacheService, requestCache});
} }
await this.validateNewDmAllowed({sender: callingUser, recipient: targetUser});
const channel = await this.createNewDMChannel({userId, recipientId, userCacheService, requestCache});
return channel;
}
async pinDmChannel({userId, channelId}: {userId: UserID; channelId: ChannelID}): Promise<void> { async pinDmChannel({userId, channelId}: {userId: UserID; channelId: ChannelID}): Promise<void> {
const channel = await this.channelService.channelData.operations.getChannel({userId, channelId}); const channel = await this.channelService.channelData.operations.getChannel({userId, channelId});
@@ -258,30 +250,6 @@ export class UserChannelService {
return await this.userRepository.findExistingDmState(userId, recipientId); return await this.userRepository.findExistingDmState(userId, recipientId);
} }
private async validateNewDmAllowed({sender, recipient}: {sender: User; recipient: User}): Promise<void> {
if (isBugHunterBotUser(sender)) {
return;
}
const [senderBlockedRecipient, recipientBlockedSender, friendship] = await Promise.all([
this.userRepository.getRelationship(sender.id, recipient.id, RelationshipTypes.BLOCKED),
this.userRepository.getRelationship(recipient.id, sender.id, RelationshipTypes.BLOCKED),
this.userRepository.getRelationship(sender.id, recipient.id, RelationshipTypes.FRIEND),
]);
if (senderBlockedRecipient || recipientBlockedSender) {
throw new CannotSendMessagesToUserError();
}
if (friendship) {
return;
}
const hasMutualGuilds = await this.userPermissionUtils.checkMutualGuildsForDmAccessAsync({
userId: sender.id,
targetId: recipient.id,
});
if (!hasMutualGuilds) {
throw new CannotSendMessagesToUserError();
}
}
async ensureDmOpenForBothUsers({ async ensureDmOpenForBothUsers({
userId, userId,
recipientId, recipientId,
@@ -109,7 +109,7 @@ describe('UserChannelService', () => {
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_USER') .expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_USER')
.execute(); .execute();
}); });
test('cannot create DM with blocked user', async () => { test('can create DM with a user who blocked you', async () => {
const user1 = await createTestAccount(harness); const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness); const user2 = await createTestAccount(harness);
const guild = await createGuild(harness, user1.token, 'Test Community'); const guild = await createGuild(harness, user1.token, 'Test Community');
@@ -117,11 +117,16 @@ describe('UserChannelService', () => {
const invite = await createChannelInvite(harness, user1.token, systemChannel.id); const invite = await createChannelInvite(harness, user1.token, systemChannel.id);
await acceptInvite(harness, user2.token, invite.code); await acceptInvite(harness, user2.token, invite.code);
await blockUser(harness, user1, user2.userId); await blockUser(harness, user1, user2.userId);
await createBuilder(harness, user1.token) const channel = await createDmChannel(harness, user1.token, user2.userId);
.post('/users/@me/channels') expect(channel.id).toBeDefined();
.body({recipient_id: user2.userId}) expect(channel.type).toBe(ChannelTypes.DM);
.expect(HTTP_STATUS.BAD_REQUEST, 'CANNOT_SEND_MESSAGES_TO_USER') });
.execute(); test('can create DM with a user who shares no mutual community', async () => {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
const channel = await createDmChannel(harness, user1.token, user2.userId);
expect(channel.id).toBeDefined();
expect(channel.type).toBe(ChannelTypes.DM);
}); });
test('unclaimed account cannot create DM', async () => { test('unclaimed account cannot create DM', async () => {
const user1 = await createTestAccount(harness); const user1 = await createTestAccount(harness);