fix(api): exempt bots from being considered unclaimed users (#45)
This commit is contained in:
parent
1cef2290fe
commit
6f21a7e37b
@ -97,7 +97,7 @@ async function verifySudoMode(
|
||||
return {verified: true, sudoToken, method: 'mfa'};
|
||||
}
|
||||
|
||||
const isUnclaimedAccount = !user.passwordHash;
|
||||
const isUnclaimedAccount = user.isUnclaimedAccount();
|
||||
if (isUnclaimedAccount && !hasMfa) {
|
||||
return {verified: true, method: 'password'};
|
||||
}
|
||||
|
||||
@ -306,7 +306,7 @@ export const MessageController = (app: HonoApp) => {
|
||||
const channelId = createChannelID(ctx.req.valid('param').channel_id);
|
||||
const requestCache = ctx.get('requestCache');
|
||||
|
||||
if (!user.passwordHash && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||
if (user.isUnclaimedAccount() && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||
throw new UnclaimedAccountRestrictedError('send messages');
|
||||
}
|
||||
|
||||
|
||||
@ -166,7 +166,7 @@ export const MessageInteractionController = (app: HonoApp) => {
|
||||
const sessionId = ctx.req.valid('query').session_id;
|
||||
const requestCache = ctx.get('requestCache');
|
||||
|
||||
if (!user.passwordHash && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||
if (user.isUnclaimedAccount() && !isPersonalNotesChannel({userId: user.id, channelId})) {
|
||||
throw new UnclaimedAccountRestrictedError('add reactions');
|
||||
}
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ export class CallService {
|
||||
}
|
||||
|
||||
const caller = await this.userRepository.findUnique(userId);
|
||||
const isUnclaimedCaller = caller != null && !caller.passwordHash && !caller.isBot;
|
||||
const isUnclaimedCaller = caller?.isUnclaimedAccount() ?? false;
|
||||
if (isUnclaimedCaller && channel.type === ChannelTypes.DM) {
|
||||
return {ringable: false};
|
||||
}
|
||||
|
||||
@ -35,17 +35,13 @@ export class DMPermissionValidator {
|
||||
|
||||
async validate({recipients, userId}: {recipients: Array<User>; userId: UserID}): Promise<void> {
|
||||
const senderUser = await this.deps.userRepository.findUnique(userId);
|
||||
if (senderUser && !senderUser.passwordHash && !senderUser.isBot) {
|
||||
if (senderUser && senderUser.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('send direct messages');
|
||||
}
|
||||
|
||||
const targetUser = recipients.find((recipient) => recipient.id !== userId);
|
||||
if (!targetUser) return;
|
||||
|
||||
if (!targetUser.passwordHash && !targetUser.isBot) {
|
||||
throw new UnclaimedAccountRestrictedError('receive direct messages');
|
||||
}
|
||||
|
||||
const senderBlockedTarget = await this.deps.userRepository.getRelationship(
|
||||
userId,
|
||||
targetUser.id,
|
||||
|
||||
@ -51,7 +51,7 @@ import type {
|
||||
PackInviteMetadataResponse,
|
||||
} from '~/invite/InviteModel';
|
||||
import {Logger} from '~/Logger';
|
||||
import type {Channel, Invite} from '~/Models';
|
||||
import {Channel, Invite} from '~/Models';
|
||||
import type {RequestCache} from '~/middleware/RequestCacheMiddleware';
|
||||
import type {PackRepository, PackType} from '~/pack/PackRepository';
|
||||
import type {PackService} from '~/pack/PackService';
|
||||
@ -352,7 +352,7 @@ export class InviteService {
|
||||
if (!invite.channelId) throw new UnknownInviteError();
|
||||
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
if (user && !user.passwordHash) {
|
||||
if (user && user.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('join group DMs');
|
||||
}
|
||||
|
||||
@ -376,7 +376,7 @@ export class InviteService {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
}
|
||||
|
||||
return invite;
|
||||
return this.cloneInviteWithUses(invite, newUses);
|
||||
}
|
||||
|
||||
if (invite.type === InviteTypes.EMOJI_PACK || invite.type === InviteTypes.STICKER_PACK) {
|
||||
@ -389,7 +389,7 @@ export class InviteService {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
}
|
||||
|
||||
return invite;
|
||||
return this.cloneInviteWithUses(invite, newUses);
|
||||
}
|
||||
|
||||
if (!invite.guildId) throw new UnknownInviteError();
|
||||
@ -406,7 +406,7 @@ export class InviteService {
|
||||
|
||||
if (guild.features.has(GuildFeatures.DISALLOW_UNCLAIMED_ACCOUNTS)) {
|
||||
const user = await this.userRepository.findUnique(userId);
|
||||
if (user && !user.passwordHash) {
|
||||
if (user && user.isUnclaimedAccount()) {
|
||||
throw new GuildDisallowsUnclaimedAccountsError();
|
||||
}
|
||||
}
|
||||
@ -455,7 +455,15 @@ export class InviteService {
|
||||
await this.inviteRepository.delete(inviteCode);
|
||||
}
|
||||
|
||||
return invite;
|
||||
return this.cloneInviteWithUses(invite, newUses);
|
||||
}
|
||||
|
||||
private cloneInviteWithUses(invite: Invite, uses: number): Invite {
|
||||
const row = invite.toRow();
|
||||
return new Invite({
|
||||
...row,
|
||||
uses,
|
||||
});
|
||||
}
|
||||
|
||||
async deleteInvite({userId, inviteCode}: DeleteInviteParams, auditLogReason?: string | null): Promise<void> {
|
||||
|
||||
@ -139,6 +139,10 @@ export class User {
|
||||
return checkIsPremium(this);
|
||||
}
|
||||
|
||||
isUnclaimedAccount(): boolean {
|
||||
return this.passwordHash === null && !this.isBot;
|
||||
}
|
||||
|
||||
canUseGlobalExpressions(): boolean {
|
||||
return this.isPremium() || this.isBot;
|
||||
}
|
||||
|
||||
@ -135,7 +135,7 @@ export class ApplicationService {
|
||||
const owner = await this.deps.userRepository.findUniqueAssert(args.ownerUserId);
|
||||
const botIsPublic = args.botPublic ?? true;
|
||||
|
||||
if (!owner.passwordHash && !owner.isBot) {
|
||||
if (owner.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('create applications');
|
||||
}
|
||||
|
||||
|
||||
@ -218,7 +218,7 @@ export class StripeCheckoutService {
|
||||
}
|
||||
|
||||
validateUserCanPurchase(user: User): void {
|
||||
if (!user.passwordHash && !user.isBot) {
|
||||
if (user.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('make purchases');
|
||||
}
|
||||
|
||||
|
||||
@ -90,7 +90,7 @@ const requiresSensitiveUserVerification = (
|
||||
data: UserUpdateRequest,
|
||||
emailTokenProvided: boolean,
|
||||
): boolean => {
|
||||
const isUnclaimed = !user.passwordHash;
|
||||
const isUnclaimed = user.isUnclaimedAccount();
|
||||
const usernameChanged = data.username !== undefined && data.username !== user.username;
|
||||
const discriminatorChanged = data.discriminator !== undefined && data.discriminator !== user.discriminator;
|
||||
const emailChanged = data.email !== undefined && data.email !== user.email;
|
||||
@ -204,7 +204,7 @@ export const UserAccountController = (app: HonoApp) => {
|
||||
throw InputValidationError.create('email', 'Email must be changed via email_token');
|
||||
}
|
||||
const emailTokenProvided = emailToken !== undefined;
|
||||
const isUnclaimed = !user.passwordHash;
|
||||
const isUnclaimed = user.isUnclaimedAccount();
|
||||
if (isUnclaimed) {
|
||||
const {username: _ignoredUsername, discriminator: _ignoredDiscriminator, ...rest} = userUpdateData;
|
||||
userUpdateData = rest;
|
||||
|
||||
@ -60,7 +60,7 @@ export class EmailChangeService {
|
||||
) {}
|
||||
|
||||
async start(user: User): Promise<StartEmailChangeResult> {
|
||||
const isUnclaimed = !user.passwordHash;
|
||||
const isUnclaimed = user.isUnclaimedAccount();
|
||||
const hasEmail = !!user.email;
|
||||
if (!hasEmail && !isUnclaimed) {
|
||||
throw InputValidationError.create('email', 'You must have an email to change it.');
|
||||
|
||||
@ -60,7 +60,7 @@ export class UserAccountSecurityService {
|
||||
invalidateAuthSessions: false,
|
||||
};
|
||||
|
||||
const isUnclaimedAccount = !user.passwordHash;
|
||||
const isUnclaimedAccount = user.isUnclaimedAccount();
|
||||
const identityVerifiedViaSudo = sudoContext?.method === 'mfa' || sudoContext?.method === 'sudo_token';
|
||||
const identityVerifiedViaPassword = sudoContext?.method === 'password';
|
||||
const hasMfa = userHasMfa(user);
|
||||
|
||||
@ -480,17 +480,12 @@ export class UserChannelService {
|
||||
}
|
||||
}
|
||||
|
||||
private async validateDmPermission(userId: UserID, recipientId: UserID, recipientUser?: User | null): Promise<void> {
|
||||
private async validateDmPermission(userId: UserID, recipientId: UserID, _recipientUser?: User | null): Promise<void> {
|
||||
const senderUser = await this.userAccountRepository.findUnique(userId);
|
||||
if (senderUser && !senderUser.passwordHash && !senderUser.isBot) {
|
||||
if (senderUser && senderUser.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('send direct messages');
|
||||
}
|
||||
|
||||
const resolvedRecipient = recipientUser ?? (await this.userAccountRepository.findUnique(recipientId));
|
||||
if (resolvedRecipient && !resolvedRecipient.passwordHash && !resolvedRecipient.isBot) {
|
||||
throw new UnclaimedAccountRestrictedError('receive direct messages');
|
||||
}
|
||||
|
||||
const userBlockedRecipient = await this.userRelationshipRepository.getRelationship(
|
||||
userId,
|
||||
recipientId,
|
||||
|
||||
@ -121,7 +121,7 @@ export class UserContentService {
|
||||
throw new UnknownUserError();
|
||||
}
|
||||
|
||||
if (!user.passwordHash && !user.isBot) {
|
||||
if (user.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('create beta codes');
|
||||
}
|
||||
|
||||
|
||||
@ -137,7 +137,7 @@ export class UserRelationshipService {
|
||||
requestCache: RequestCache;
|
||||
}): Promise<Relationship> {
|
||||
const user = await this.userAccountRepository.findUnique(userId);
|
||||
if (user && !user.passwordHash) {
|
||||
if (user && user.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('accept friend requests');
|
||||
}
|
||||
|
||||
@ -341,7 +341,7 @@ export class UserRelationshipService {
|
||||
}
|
||||
|
||||
const requesterUser = await this.userAccountRepository.findUnique(userId);
|
||||
if (requesterUser && !requesterUser.passwordHash) {
|
||||
if (requesterUser && requesterUser.isUnclaimedAccount()) {
|
||||
throw new UnclaimedAccountRestrictedError('send friend requests');
|
||||
}
|
||||
|
||||
|
||||
@ -116,7 +116,7 @@ export class VoiceService {
|
||||
throw new UnknownChannelError();
|
||||
}
|
||||
|
||||
const isUnclaimed = !user.passwordHash && !user.isBot;
|
||||
const isUnclaimed = user.isUnclaimedAccount();
|
||||
if (isUnclaimed) {
|
||||
if (channel.type === ChannelTypes.DM) {
|
||||
throw new UnclaimedAccountRestrictedError('join 1:1 voice calls');
|
||||
|
||||
@ -20,14 +20,13 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestUnclaimedAccountCannotReceiveDM verifies that claimed accounts
|
||||
// cannot send DMs to unclaimed accounts.
|
||||
func TestUnclaimedAccountCannotReceiveDM(t *testing.T) {
|
||||
// TestUnclaimedAccountCanReceiveDM verifies that claimed accounts can send DMs
|
||||
// to unclaimed accounts.
|
||||
func TestUnclaimedAccountCanReceiveDM(t *testing.T) {
|
||||
client := newTestClient(t)
|
||||
|
||||
claimedAccount := createTestAccount(t, client)
|
||||
@ -47,21 +46,13 @@ func TestUnclaimedAccountCannotReceiveDM(t *testing.T) {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400 for DM to unclaimed account, got %d: %s", resp.StatusCode, readResponseBody(resp))
|
||||
assertStatus(t, resp, http.StatusOK)
|
||||
|
||||
var channel minimalChannelResponse
|
||||
decodeJSONResponse(t, resp, &channel)
|
||||
if channel.ID == "" {
|
||||
t.Fatalf("expected channel ID in response")
|
||||
}
|
||||
|
||||
// Verify the error code is UNCLAIMED_ACCOUNT_RESTRICTED
|
||||
var errorResp struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&errorResp); err != nil {
|
||||
t.Fatalf("failed to decode error response: %v", err)
|
||||
}
|
||||
|
||||
if errorResp.Code != "UNCLAIMED_ACCOUNT_RESTRICTED" {
|
||||
t.Fatalf("expected error code UNCLAIMED_ACCOUNT_RESTRICTED, got %s", errorResp.Code)
|
||||
}
|
||||
|
||||
t.Log("Unclaimed account cannot receive DM test passed")
|
||||
t.Log("Unclaimed account can receive DM test passed")
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user