global update and global fix

This commit is contained in:
lendry
2026-06-25 23:48:57 +03:00
parent 3880c68d59
commit b0ea87e898
121 changed files with 13759 additions and 663 deletions

View File

@@ -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;