global update and global fix
This commit is contained in:
@@ -7,6 +7,11 @@ import { SettingsService } from './settings.service';
|
||||
|
||||
import { NotificationsService } from './notifications.service';
|
||||
import { MinioService } from '../infra/minio.service';
|
||||
import { BotFatherSeedService } from './bot/bot-father-seed.service';
|
||||
import { BotFatherService } from './bot/bot-father.service';
|
||||
import { BOTFATHER_BOT_USERNAME, BOTFATHER_DISPLAY_NAME, BOTFATHER_USER_USERNAME } from './bot/bot-father.constants';
|
||||
import { normalizeBotUsername } from './bot/bot-token.util';
|
||||
import { HUMAN_USER_WHERE, isProtectedSystemAccount } from './system-account.util';
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +31,11 @@ export class FamilyService {
|
||||
|
||||
private readonly notifications: NotificationsService,
|
||||
|
||||
private readonly minio: MinioService
|
||||
private readonly minio: MinioService,
|
||||
|
||||
private readonly botFatherSeed: BotFatherSeedService,
|
||||
|
||||
private readonly botFather: BotFatherService
|
||||
|
||||
) {}
|
||||
|
||||
@@ -58,7 +67,7 @@ export class FamilyService {
|
||||
|
||||
},
|
||||
|
||||
include: { members: { include: { user: true } } }
|
||||
include: { members: { include: this.memberUserInclude() } }
|
||||
|
||||
});
|
||||
|
||||
@@ -102,7 +111,7 @@ export class FamilyService {
|
||||
|
||||
where: { OR: [{ ownerId: userId }, { members: { some: { userId } } }] },
|
||||
|
||||
include: { members: { include: { user: true } } },
|
||||
include: { members: { include: this.memberUserInclude() } },
|
||||
|
||||
orderBy: { updatedAt: 'desc' }
|
||||
|
||||
@@ -146,7 +155,7 @@ export class FamilyService {
|
||||
|
||||
data: { name: trimmedName },
|
||||
|
||||
include: { members: { include: { user: true } } }
|
||||
include: { members: { include: this.memberUserInclude() } }
|
||||
|
||||
});
|
||||
|
||||
@@ -174,7 +183,7 @@ export class FamilyService {
|
||||
|
||||
where: { ownerId: userId },
|
||||
|
||||
include: { members: { include: { user: true } } }
|
||||
include: { members: { include: this.memberUserInclude() } }
|
||||
|
||||
});
|
||||
|
||||
@@ -322,6 +331,21 @@ export class FamilyService {
|
||||
|
||||
}
|
||||
|
||||
const targetUser = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { isSystemAccount: true, linkedBotId: true }
|
||||
});
|
||||
|
||||
if (!targetUser) {
|
||||
throw new BadRequestException('Пользователь не найден');
|
||||
}
|
||||
|
||||
if (isProtectedSystemAccount(targetUser)) {
|
||||
if (role !== 'bot' || !targetUser.linkedBotId) {
|
||||
throw new BadRequestException('Системные учётные записи нельзя добавить в семью напрямую');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const member = await this.prisma.$transaction(async (tx) => {
|
||||
@@ -332,7 +356,7 @@ export class FamilyService {
|
||||
|
||||
for (const room of rooms) {
|
||||
|
||||
if (room.type === 'GENERAL') {
|
||||
if (room.type === 'GENERAL' && role !== 'bot') {
|
||||
|
||||
await tx.chatRoomMember.upsert({
|
||||
|
||||
@@ -354,8 +378,53 @@ export class FamilyService {
|
||||
|
||||
|
||||
|
||||
return this.toMember(member);
|
||||
const user = await this.loadMemberUser(member.userId);
|
||||
|
||||
return this.toMember({ ...member, user: user ?? undefined });
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async addBotFatherToFamily(requesterId: string, groupId: string) {
|
||||
|
||||
const group = await this.getGroupWithMembers(groupId);
|
||||
|
||||
this.assertMember(group, requesterId);
|
||||
|
||||
const botFatherUserId = await this.botFatherSeed.getBotFatherUserId();
|
||||
|
||||
if (group.members.some((member) => member.userId === botFatherUserId)) {
|
||||
|
||||
throw new BadRequestException('BotFather уже добавлен в эту семью');
|
||||
|
||||
}
|
||||
|
||||
return this.addMember(groupId, botFatherUserId, 'bot');
|
||||
|
||||
}
|
||||
|
||||
async addBotToFamily(requesterId: string, groupId: string, botId: string) {
|
||||
const group = await this.getGroupWithMembers(groupId);
|
||||
this.assertMember(group, requesterId);
|
||||
|
||||
const bot = await this.prisma.bot.findUnique({
|
||||
where: { id: botId },
|
||||
select: { id: true, isActive: true, username: true, name: true }
|
||||
});
|
||||
if (!bot) {
|
||||
throw new NotFoundException('Бот не найден');
|
||||
}
|
||||
if (!bot.isActive) {
|
||||
throw new BadRequestException('Бот деактивирован и не может быть добавлен в семью');
|
||||
}
|
||||
|
||||
const { userId: botSystemUserId } = await this.botFather.ensureBotSystemUser(bot.id);
|
||||
if (group.members.some((member) => member.userId === botSystemUserId)) {
|
||||
throw new BadRequestException('Этот бот уже добавлен в семью');
|
||||
}
|
||||
|
||||
return this.addMember(groupId, botSystemUserId, 'bot');
|
||||
}
|
||||
|
||||
|
||||
@@ -414,7 +483,7 @@ export class FamilyService {
|
||||
|
||||
const invitee = options.inviteeUserId
|
||||
? await this.prisma.user.findFirst({
|
||||
where: { id: options.inviteeUserId, status: 'ACTIVE', deletedAt: null }
|
||||
where: { id: options.inviteeUserId, status: 'ACTIVE', deletedAt: null, ...HUMAN_USER_WHERE }
|
||||
})
|
||||
: options.target
|
||||
? await this.findUserByContact(options.target)
|
||||
@@ -424,6 +493,10 @@ export class FamilyService {
|
||||
throw new BadRequestException('Пользователь не найден. Выберите его из результатов поиска.');
|
||||
}
|
||||
|
||||
if (isProtectedSystemAccount(invitee)) {
|
||||
throw new BadRequestException('BotFather добавляется через отдельное действие «Добавить BotFather»');
|
||||
}
|
||||
|
||||
if (invitee.id === requesterId) {
|
||||
throw new BadRequestException('Нельзя пригласить самого себя');
|
||||
}
|
||||
@@ -476,44 +549,137 @@ export class FamilyService {
|
||||
}
|
||||
|
||||
const excludedIds = [...group.members.map((member) => member.userId), requesterId];
|
||||
const excludedBotUsernames = new Set(
|
||||
group.members
|
||||
.map((member) => member.user?.linkedBot?.username)
|
||||
.filter((username): username is string => Boolean(username))
|
||||
);
|
||||
const botFatherUserId = await this.botFatherSeed.getBotFatherUserId();
|
||||
const botFatherMatches =
|
||||
/bot\s*father|бот\s*father|botfather/i.test(trimmed) &&
|
||||
!excludedIds.includes(botFatherUserId) &&
|
||||
!excludedBotUsernames.has(BOTFATHER_BOT_USERNAME);
|
||||
const digits = trimmed.replace(/\D/g, '');
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
deletedAt: null,
|
||||
id: { notIn: excludedIds },
|
||||
OR: [
|
||||
{ displayName: { contains: trimmed, mode: 'insensitive' } },
|
||||
{ username: { contains: trimmed, mode: 'insensitive' } },
|
||||
...(trimmed.includes('@') ? [{ email: { contains: trimmed, mode: 'insensitive' as const } }] : []),
|
||||
...(digits.length >= 4 ? [{ phone: { contains: digits } }] : [])
|
||||
]
|
||||
},
|
||||
orderBy: { displayName: 'asc' },
|
||||
take: 8,
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
username: true,
|
||||
avatarStorageKey: true,
|
||||
const normalizedBotQuery = normalizeBotUsername(trimmed.replace(/^@/, ''));
|
||||
|
||||
const [users, matchedBots] = await Promise.all([
|
||||
this.prisma.user.findMany({
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
deletedAt: null,
|
||||
isSystemAccount: false,
|
||||
linkedBotId: null,
|
||||
id: { notIn: excludedIds },
|
||||
OR: [
|
||||
{ displayName: { contains: trimmed, mode: 'insensitive' } },
|
||||
{ username: { contains: trimmed, mode: 'insensitive' } },
|
||||
...(trimmed.includes('@') ? [{ email: { contains: trimmed, mode: 'insensitive' as const } }] : []),
|
||||
...(digits.length >= 4 ? [{ phone: { contains: digits } }] : [])
|
||||
]
|
||||
},
|
||||
orderBy: { displayName: 'asc' },
|
||||
take: 8,
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
email: true,
|
||||
phone: true,
|
||||
username: true,
|
||||
avatarStorageKey: true,
|
||||
isVerified: true,
|
||||
verificationIcon: true
|
||||
}
|
||||
}),
|
||||
normalizedBotQuery.length >= 2
|
||||
? this.prisma.bot.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
OR: [
|
||||
{ name: { contains: trimmed, mode: 'insensitive' } },
|
||||
{ username: { contains: normalizedBotQuery, mode: 'insensitive' } }
|
||||
]
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
take: 8,
|
||||
include: {
|
||||
owner: { select: { id: true, displayName: true } },
|
||||
linkedSystemUser: { select: { id: true } }
|
||||
}
|
||||
})
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
|
||||
const botCandidates: Array<{
|
||||
id: string;
|
||||
botId: string;
|
||||
displayName: string;
|
||||
username?: string;
|
||||
hasAvatar: boolean;
|
||||
isVerified: boolean;
|
||||
verificationIcon?: string;
|
||||
isBot: boolean;
|
||||
botUsername: string;
|
||||
ownerDisplayName?: string;
|
||||
}> = [];
|
||||
|
||||
for (const bot of matchedBots) {
|
||||
if (excludedBotUsernames.has(bot.username)) continue;
|
||||
const { userId } = bot.linkedSystemUser
|
||||
? { userId: bot.linkedSystemUser.id }
|
||||
: await this.botFather.ensureBotSystemUser(bot.id);
|
||||
if (excludedIds.includes(userId)) continue;
|
||||
botCandidates.push({
|
||||
id: userId,
|
||||
botId: bot.id,
|
||||
displayName: bot.name,
|
||||
username: `@${bot.username}`,
|
||||
hasAvatar: false,
|
||||
isVerified: true,
|
||||
verificationIcon: true
|
||||
}
|
||||
});
|
||||
verificationIcon: resolveVerificationIcon('bot'),
|
||||
isBot: true,
|
||||
botUsername: bot.username,
|
||||
ownerDisplayName: bot.owner.displayName
|
||||
});
|
||||
}
|
||||
|
||||
const humanCandidates = users.map((user) => ({
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
username: user.username,
|
||||
hasAvatar: Boolean(user.avatarStorageKey),
|
||||
isVerified: user.isVerified,
|
||||
verificationIcon: user.isVerified ? resolveVerificationIcon(user.verificationIcon) : undefined,
|
||||
isBot: false,
|
||||
botUsername: undefined,
|
||||
botId: undefined,
|
||||
ownerDisplayName: undefined
|
||||
}));
|
||||
|
||||
return {
|
||||
users: users.map((user) => ({
|
||||
id: user.id,
|
||||
displayName: user.displayName,
|
||||
email: user.email,
|
||||
phone: user.phone,
|
||||
username: user.username,
|
||||
hasAvatar: Boolean(user.avatarStorageKey),
|
||||
isVerified: user.isVerified,
|
||||
verificationIcon: user.isVerified ? resolveVerificationIcon(user.verificationIcon) : undefined
|
||||
}))
|
||||
users: [
|
||||
...(botFatherMatches
|
||||
? [
|
||||
{
|
||||
id: botFatherUserId,
|
||||
displayName: BOTFATHER_DISPLAY_NAME,
|
||||
email: undefined,
|
||||
phone: undefined,
|
||||
username: BOTFATHER_USER_USERNAME,
|
||||
hasAvatar: false,
|
||||
isVerified: true,
|
||||
verificationIcon: resolveVerificationIcon('bot'),
|
||||
isBot: true,
|
||||
botUsername: BOTFATHER_BOT_USERNAME,
|
||||
botId: undefined,
|
||||
ownerDisplayName: undefined
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...botCandidates,
|
||||
...humanCandidates.filter((candidate) => !botCandidates.some((bot) => bot.id === candidate.id))
|
||||
].slice(0, 12)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -702,7 +868,7 @@ export class FamilyService {
|
||||
|
||||
where: { id: groupId },
|
||||
|
||||
include: { members: { include: { user: true } } }
|
||||
include: { members: { include: this.memberUserInclude() } }
|
||||
|
||||
});
|
||||
|
||||
@@ -750,7 +916,7 @@ export class FamilyService {
|
||||
|
||||
if (trimmed.includes('@')) {
|
||||
return this.prisma.user.findFirst({
|
||||
where: { email: { equals: trimmed, mode: 'insensitive' }, status: 'ACTIVE', deletedAt: null }
|
||||
where: { email: { equals: trimmed, mode: 'insensitive' }, status: 'ACTIVE', deletedAt: null, ...HUMAN_USER_WHERE }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -760,6 +926,7 @@ export class FamilyService {
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
deletedAt: null,
|
||||
...HUMAN_USER_WHERE,
|
||||
OR: [{ phone: trimmed }, { phone: { endsWith: digits.slice(-10) } }]
|
||||
}
|
||||
});
|
||||
@@ -769,6 +936,7 @@ export class FamilyService {
|
||||
where: {
|
||||
status: 'ACTIVE',
|
||||
deletedAt: null,
|
||||
...HUMAN_USER_WHERE,
|
||||
OR: [
|
||||
{ username: { equals: trimmed, mode: 'insensitive' } },
|
||||
{ displayName: { equals: trimmed, mode: 'insensitive' } }
|
||||
@@ -805,7 +973,14 @@ export class FamilyService {
|
||||
|
||||
createdAt: Date;
|
||||
|
||||
user?: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null };
|
||||
user?: {
|
||||
displayName: string;
|
||||
avatarStorageKey: string | null;
|
||||
isVerified: boolean;
|
||||
verificationIcon: string | null;
|
||||
linkedBotId?: string | null;
|
||||
linkedBot?: { username: string } | null;
|
||||
};
|
||||
|
||||
}>;
|
||||
|
||||
@@ -825,7 +1000,11 @@ export class FamilyService {
|
||||
|
||||
updatedAt: group.updatedAt.toISOString(),
|
||||
|
||||
members: group.members.map((member) => this.toMember(member))
|
||||
members: group.members.map((member) => this.toMember(member)),
|
||||
|
||||
botFatherAvailable: !group.members.some(
|
||||
(member) => member.user?.linkedBot?.username === BOTFATHER_BOT_USERNAME
|
||||
)
|
||||
|
||||
};
|
||||
|
||||
@@ -845,7 +1024,14 @@ export class FamilyService {
|
||||
|
||||
createdAt: Date;
|
||||
|
||||
user?: { displayName: string; avatarStorageKey: string | null; isVerified: boolean; verificationIcon: string | null };
|
||||
user?: {
|
||||
displayName: string;
|
||||
avatarStorageKey: string | null;
|
||||
isVerified: boolean;
|
||||
verificationIcon: string | null;
|
||||
linkedBotId?: string | null;
|
||||
linkedBot?: { username: string } | null;
|
||||
};
|
||||
|
||||
}) {
|
||||
|
||||
@@ -867,6 +1053,10 @@ export class FamilyService {
|
||||
|
||||
verificationIcon: member.user?.isVerified ? resolveVerificationIcon(member.user?.verificationIcon) : undefined,
|
||||
|
||||
isBot: Boolean(member.user?.linkedBotId),
|
||||
|
||||
botUsername: member.user?.linkedBot?.username ?? undefined,
|
||||
|
||||
createdAt: member.createdAt.toISOString()
|
||||
|
||||
};
|
||||
@@ -875,6 +1065,40 @@ export class FamilyService {
|
||||
|
||||
|
||||
|
||||
private memberUserInclude() {
|
||||
|
||||
return {
|
||||
|
||||
user: {
|
||||
|
||||
include: {
|
||||
|
||||
linkedBot: { select: { username: true } }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private loadMemberUser(userId: string) {
|
||||
|
||||
return this.prisma.user.findUnique({
|
||||
|
||||
where: { id: userId },
|
||||
|
||||
include: { linkedBot: { select: { username: true } } }
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
private toInvite(invite: {
|
||||
|
||||
id: string;
|
||||
|
||||
Reference in New Issue
Block a user