fix docker for tauri app
This commit is contained in:
@@ -36,9 +36,9 @@ export class BotController {
|
|||||||
|
|
||||||
@Get('by-username/:botRef/messages')
|
@Get('by-username/:botRef/messages')
|
||||||
@ApiOperation({ summary: 'История чата с ботом', description: 'Возвращает сообщения пользователя с ботом в хронологическом порядке.' })
|
@ApiOperation({ summary: 'История чата с ботом', description: 'Возвращает сообщения пользователя с ботом в хронологическом порядке.' })
|
||||||
listBotMessages(@Headers('authorization') authorization: string | undefined, @Param('botRef') botRef: string) {
|
listBotMessages(@Headers('authorization') authorization: string | undefined, @Param('botRef') botRef: string, @Query('roomId') roomId?: string) {
|
||||||
return this.auth(authorization).then((userId) =>
|
return this.auth(authorization).then((userId) =>
|
||||||
firstValueFrom(this.core.bot.ListBotChatMessages({ userId, botRef }))
|
firstValueFrom(this.core.bot.ListBotChatMessages({ userId, botRef, roomId }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export class BotController {
|
|||||||
@Body() dto: SubmitBotMessageDto
|
@Body() dto: SubmitBotMessageDto
|
||||||
) {
|
) {
|
||||||
return this.auth(authorization).then((senderUserId) =>
|
return this.auth(authorization).then((senderUserId) =>
|
||||||
firstValueFrom(this.core.bot.SubmitBotInboundMessage({ senderUserId, botRef, text: dto.text }))
|
firstValueFrom(this.core.bot.SubmitBotInboundMessage({ senderUserId, botRef, text: dto.text, roomId: dto.roomId }))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ export class SubmitBotMessageDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(1, { message: 'Текст сообщения не может быть пустым' })
|
@MinLength(1, { message: 'Текст сообщения не может быть пустым' })
|
||||||
text!: string;
|
text!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
roomId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SubmitBotCallbackDto {
|
export class SubmitBotCallbackDto {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ interface FamilyBotChatProps {
|
|||||||
botRef: string;
|
botRef: string;
|
||||||
botName: string;
|
botName: string;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
|
roomId?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
onOpenMiniApp?: (url: string) => void;
|
onOpenMiniApp?: (url: string) => void;
|
||||||
@@ -56,6 +57,7 @@ export function FamilyBotChat({
|
|||||||
botRef,
|
botRef,
|
||||||
botName,
|
botName,
|
||||||
token,
|
token,
|
||||||
|
roomId,
|
||||||
className,
|
className,
|
||||||
inputPlaceholder = 'Сообщение боту',
|
inputPlaceholder = 'Сообщение боту',
|
||||||
onOpenMiniApp,
|
onOpenMiniApp,
|
||||||
@@ -98,7 +100,7 @@ export function FamilyBotChat({
|
|||||||
if (!token) return;
|
if (!token) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await fetchBotChatMessages(botRef, token);
|
const response = await fetchBotChatMessages(botRef, token, roomId);
|
||||||
setMessages(response.messages ?? []);
|
setMessages(response.messages ?? []);
|
||||||
const menuButton = resolveComposerMenuButton({
|
const menuButton = resolveComposerMenuButton({
|
||||||
composerMenuButtonJson: response.composerMenuButtonJson,
|
composerMenuButtonJson: response.composerMenuButtonJson,
|
||||||
@@ -119,7 +121,7 @@ export function FamilyBotChat({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [botRef, onBotMetaChange, token]);
|
}, [botRef, onBotMetaChange, roomId, token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void loadMessages();
|
void loadMessages();
|
||||||
@@ -240,7 +242,7 @@ export function FamilyBotChat({
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
try {
|
try {
|
||||||
await sendBotMessage(botRef, text, token);
|
await sendBotMessage(botRef, text, token, roomId);
|
||||||
await loadMessages();
|
await loadMessages();
|
||||||
} finally {
|
} finally {
|
||||||
setSending(false);
|
setSending(false);
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
|||||||
const myMembership = activeRoom?.members.find((member) => member.userId === user?.id);
|
const myMembership = activeRoom?.members.find((member) => member.userId === user?.id);
|
||||||
const isFamilyOwner = group?.ownerId === user?.id;
|
const isFamilyOwner = group?.ownerId === user?.id;
|
||||||
const isChatCreator = activeRoom?.createdById === user?.id;
|
const isChatCreator = activeRoom?.createdById === user?.id;
|
||||||
const canManageChatMembers = Boolean(isFamilyOwner || isChatCreator);
|
const canManageChatMembers = Boolean(isFamilyOwner || isChatCreator || activeRoomIsBot);
|
||||||
const canManageActiveBot = Boolean(
|
const canManageActiveBot = Boolean(
|
||||||
activeRoomIsBot && botChatMeta?.manageWebAppUrl && user?.id && botChatMeta.botOwnerId === user.id
|
activeRoomIsBot && botChatMeta?.manageWebAppUrl && user?.id && botChatMeta.botOwnerId === user.id
|
||||||
);
|
);
|
||||||
@@ -1518,6 +1518,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
|||||||
botRef={activeRoom.botUsername}
|
botRef={activeRoom.botUsername}
|
||||||
botName={activeRoomTitle}
|
botName={activeRoomTitle}
|
||||||
token={token}
|
token={token}
|
||||||
|
roomId={activeRoom.id}
|
||||||
onBotMetaChange={setBotChatMeta}
|
onBotMetaChange={setBotChatMeta}
|
||||||
onOpenMiniApp={(url) => {
|
onOpenMiniApp={(url) => {
|
||||||
setMiniAppTitle('Mini App');
|
setMiniAppTitle('Mini App');
|
||||||
@@ -2142,8 +2143,9 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
|||||||
const presence = presenceByUserId.get(member.userId);
|
const presence = presenceByUserId.get(member.userId);
|
||||||
const canRemoveFromChat =
|
const canRemoveFromChat =
|
||||||
canManageChatMembers &&
|
canManageChatMembers &&
|
||||||
activeRoom.type === 'GROUP' &&
|
(activeRoom.type === 'GROUP' || activeRoom.type === 'BOT') &&
|
||||||
member.userId !== user?.id &&
|
member.userId !== user?.id &&
|
||||||
|
member.userId !== activeRoom.peerUserId &&
|
||||||
!(member.isChatCreator && !isFamilyOwner);
|
!(member.isChatCreator && !isFamilyOwner);
|
||||||
const canRemoveFromFamily = isFamilyOwner && member.familyRole !== 'owner' && familyMember;
|
const canRemoveFromFamily = isFamilyOwner && member.familyRole !== 'owner' && familyMember;
|
||||||
|
|
||||||
@@ -2183,7 +2185,7 @@ export function FamilyGroupView({ groupId }: { groupId: string }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
{activeRoom?.type === 'GROUP' && familyMembersNotInRoom.length > 0 ? (
|
{(activeRoom?.type === 'GROUP' || activeRoom?.type === 'BOT') && familyMembersNotInRoom.length > 0 ? (
|
||||||
<Button className="mt-4 w-full rounded-xl" onClick={() => {
|
<Button className="mt-4 w-full rounded-xl" onClick={() => {
|
||||||
setMembersOpen(false);
|
setMembersOpen(false);
|
||||||
setAddMembersOpen(true);
|
setAddMembersOpen(true);
|
||||||
|
|||||||
@@ -897,7 +897,8 @@ export async function sendFamilyInvite(groupId: string, payload: { inviteeUserId
|
|||||||
return apiFetch<FamilyInvite>(`/family/groups/${groupId}/invites`, { method: 'POST', body: JSON.stringify(payload) }, token);
|
return apiFetch<FamilyInvite>(`/family/groups/${groupId}/invites`, { method: 'POST', body: JSON.stringify(payload) }, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchBotChatMessages(botRef: string, token?: string | null) {
|
export async function fetchBotChatMessages(botRef: string, token?: string | null, roomId?: string) {
|
||||||
|
const suffix = roomId ? `?roomId=${encodeURIComponent(roomId)}` : '';
|
||||||
return apiFetch<{
|
return apiFetch<{
|
||||||
messages?: BotChatMessage[];
|
messages?: BotChatMessage[];
|
||||||
botUsername?: string;
|
botUsername?: string;
|
||||||
@@ -907,13 +908,13 @@ export async function fetchBotChatMessages(botRef: string, token?: string | null
|
|||||||
composerWebAppUrl?: string;
|
composerWebAppUrl?: string;
|
||||||
composerMenuButtonJson?: string;
|
composerMenuButtonJson?: string;
|
||||||
manageWebAppUrl?: string;
|
manageWebAppUrl?: string;
|
||||||
}>(`/bots/by-username/${encodeURIComponent(botRef)}/messages`, {}, token);
|
}>(`/bots/by-username/${encodeURIComponent(botRef)}/messages${suffix}`, {}, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendBotMessage(botRef: string, text: string, token?: string | null) {
|
export async function sendBotMessage(botRef: string, text: string, token?: string | null, roomId?: string) {
|
||||||
return apiFetch<{ updateId?: number; messageId?: number; chatId?: string }>(
|
return apiFetch<{ updateId?: number; messageId?: number; chatId?: string }>(
|
||||||
`/bots/by-username/${encodeURIComponent(botRef)}/messages`,
|
`/bots/by-username/${encodeURIComponent(botRef)}/messages`,
|
||||||
{ method: 'POST', body: JSON.stringify({ text }) },
|
{ method: 'POST', body: JSON.stringify({ text, roomId }) },
|
||||||
token
|
token
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -914,7 +914,9 @@ export class AuthGrpcController {
|
|||||||
inviteeUserId: command.inviteeUserId
|
inviteeUserId: command.inviteeUserId
|
||||||
});
|
});
|
||||||
if (invite.status === 'ACCEPTED') {
|
if (invite.status === 'ACCEPTED') {
|
||||||
await this.chat.syncFamilyChats(command.groupId);
|
if (invite.inviteeUserId) {
|
||||||
|
await this.chat.ensureBotRoomForMember(command.groupId, command.requesterId, invite.inviteeUserId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return invite;
|
return invite;
|
||||||
}
|
}
|
||||||
@@ -1192,8 +1194,8 @@ export class AuthGrpcController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('BotService', 'SubmitBotInboundMessage')
|
@GrpcMethod('BotService', 'SubmitBotInboundMessage')
|
||||||
submitBotInboundMessage(command: { senderUserId: string; botRef: string; text: string }) {
|
submitBotInboundMessage(command: { senderUserId: string; botRef: string; text: string; roomId?: string }) {
|
||||||
return this.botInbound.submitUserMessage(command.senderUserId, command.botRef, command.text);
|
return this.botInbound.submitUserMessage(command.senderUserId, command.botRef, command.text, command.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('BotService', 'SubmitBotCallbackQuery')
|
@GrpcMethod('BotService', 'SubmitBotCallbackQuery')
|
||||||
@@ -1202,8 +1204,8 @@ export class AuthGrpcController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GrpcMethod('BotService', 'ListBotChatMessages')
|
@GrpcMethod('BotService', 'ListBotChatMessages')
|
||||||
listBotChatMessages(command: { userId: string; botRef: string }) {
|
listBotChatMessages(command: { userId: string; botRef: string; roomId?: string }) {
|
||||||
return this.botApi.listBotChatMessages(command.userId, command.botRef);
|
return this.botApi.listBotChatMessages(command.userId, command.botRef, command.roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private toSessionResponse(session: { id: string; userId: string; deviceId: string | null; pinVerified: boolean; status: string; ipAddress: string | null; userAgent: string | null; expiresAt: Date; createdAt: Date; updatedAt: Date }) {
|
private toSessionResponse(session: { id: string; userId: string; deviceId: string | null; pinVerified: boolean; status: string; ipAddress: string | null; userAgent: string | null; expiresAt: Date; createdAt: Date; updatedAt: Date }) {
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export class BotApiService {
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
async listBotChatMessages(userId: string, botRef: string) {
|
async listBotChatMessages(userId: string, botRef: string, roomId?: string) {
|
||||||
const sender = await this.prisma.user.findUnique({
|
const sender = await this.prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: { isSystemAccount: true, linkedBotId: true, deletedAt: true, status: true }
|
select: { isSystemAccount: true, linkedBotId: true, deletedAt: true, status: true }
|
||||||
@@ -247,7 +247,9 @@ export class BotApiService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const chat = await this.prisma.botChat.findUnique({
|
const chat = roomId
|
||||||
|
? await this.resolveSharedFamilyBotChat(bot.id, userId, roomId)
|
||||||
|
: await this.prisma.botChat.findUnique({
|
||||||
where: { botId_recipientUserId: { botId: bot.id, recipientUserId: userId } },
|
where: { botId_recipientUserId: { botId: bot.id, recipientUserId: userId } },
|
||||||
include: { menuButton: true }
|
include: { menuButton: true }
|
||||||
});
|
});
|
||||||
@@ -336,6 +338,30 @@ export class BotApiService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveSharedFamilyBotChat(botId: string, viewerUserId: string, roomId: string) {
|
||||||
|
const room = await this.prisma.chatRoom.findUnique({
|
||||||
|
where: { id: roomId },
|
||||||
|
include: {
|
||||||
|
members: { include: { user: { select: { linkedBotId: true } } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!room || room.type !== 'BOT' || !room.members.some((member) => member.userId === viewerUserId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const botMember = room.members.find((member) => member.user.linkedBotId === botId);
|
||||||
|
if (!botMember || room.peerUserId !== botMember.userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ownerUserId =
|
||||||
|
room.createdById ??
|
||||||
|
room.members.find((member) => !member.user.linkedBotId)?.userId ??
|
||||||
|
viewerUserId;
|
||||||
|
return this.prisma.botChat.findUnique({
|
||||||
|
where: { botId_recipientUserId: { botId, recipientUserId: ownerUserId } },
|
||||||
|
include: { menuButton: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async setChatMenuButton(bot: ValidatedBot, payload: SetChatMenuButtonPayload) {
|
private async setChatMenuButton(bot: ValidatedBot, payload: SetChatMenuButtonPayload) {
|
||||||
const chatIdRaw = payload.chat_id;
|
const chatIdRaw = payload.chat_id;
|
||||||
const parsedMenuButton = parseMenuButtonInput(payload.menu_button);
|
const parsedMenuButton = parseMenuButtonInput(payload.menu_button);
|
||||||
@@ -865,7 +891,7 @@ export class BotApiService {
|
|||||||
mediaUrl?: string
|
mediaUrl?: string
|
||||||
) {
|
) {
|
||||||
const payloadMarkup = this.serializer.readPayload(message.payload);
|
const payloadMarkup = this.serializer.readPayload(message.payload);
|
||||||
await this.notifications.publishRealtime(userId, 'bot_message', bot.name, message.text ?? '', {
|
const payload = {
|
||||||
botId: bot.id,
|
botId: bot.id,
|
||||||
botUsername: bot.username,
|
botUsername: bot.username,
|
||||||
chatId: chat.telegramChatId.toString(),
|
chatId: chat.telegramChatId.toString(),
|
||||||
@@ -875,7 +901,9 @@ export class BotApiService {
|
|||||||
mediaUrl: message.mediaUrl ?? mediaUrl ?? null,
|
mediaUrl: message.mediaUrl ?? mediaUrl ?? null,
|
||||||
replyMarkup: replyMarkup ?? payloadMarkup.replyMarkup ?? null,
|
replyMarkup: replyMarkup ?? payloadMarkup.replyMarkup ?? null,
|
||||||
telegramReplyMarkup: payloadMarkup.telegramReplyMarkup ?? null
|
telegramReplyMarkup: payloadMarkup.telegramReplyMarkup ?? null
|
||||||
});
|
};
|
||||||
|
await this.notifications.publishRealtime(userId, 'bot_message', bot.name, message.text ?? '', payload);
|
||||||
|
await this.publishToSharedBotRoomMembers(userId, bot, 'bot_message', message.text ?? '', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async publishBotMessagePinned(
|
private async publishBotMessagePinned(
|
||||||
@@ -888,14 +916,16 @@ export class BotApiService {
|
|||||||
pinnedAt?: Date | null;
|
pinnedAt?: Date | null;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
await this.notifications.publishRealtime(userId, 'bot_message_pinned', bot.name, '', {
|
const payload = {
|
||||||
botId: bot.id,
|
botId: bot.id,
|
||||||
botUsername: bot.username,
|
botUsername: bot.username,
|
||||||
chatId: chat.telegramChatId.toString(),
|
chatId: chat.telegramChatId.toString(),
|
||||||
messageId: message.telegramMsgId,
|
messageId: message.telegramMsgId,
|
||||||
isPinned: Boolean(message.isPinned),
|
isPinned: Boolean(message.isPinned),
|
||||||
pinnedAt: message.pinnedAt?.toISOString() ?? null
|
pinnedAt: message.pinnedAt?.toISOString() ?? null
|
||||||
});
|
};
|
||||||
|
await this.notifications.publishRealtime(userId, 'bot_message_pinned', bot.name, '', payload);
|
||||||
|
await this.publishToSharedBotRoomMembers(userId, bot, 'bot_message_pinned', '', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async publishBotMessageEdited(
|
private async publishBotMessageEdited(
|
||||||
@@ -913,7 +943,7 @@ export class BotApiService {
|
|||||||
replyMarkup: unknown
|
replyMarkup: unknown
|
||||||
) {
|
) {
|
||||||
const payloadMarkup = this.serializer.readPayload(message.payload);
|
const payloadMarkup = this.serializer.readPayload(message.payload);
|
||||||
await this.notifications.publishRealtime(userId, 'bot_message_edited', bot.name, message.text ?? '', {
|
const payload = {
|
||||||
botId: bot.id,
|
botId: bot.id,
|
||||||
botUsername: bot.username,
|
botUsername: bot.username,
|
||||||
chatId: chat.telegramChatId.toString(),
|
chatId: chat.telegramChatId.toString(),
|
||||||
@@ -924,7 +954,41 @@ export class BotApiService {
|
|||||||
replyMarkup: replyMarkup ?? payloadMarkup.replyMarkup ?? null,
|
replyMarkup: replyMarkup ?? payloadMarkup.replyMarkup ?? null,
|
||||||
telegramReplyMarkup: payloadMarkup.telegramReplyMarkup ?? null,
|
telegramReplyMarkup: payloadMarkup.telegramReplyMarkup ?? null,
|
||||||
editedAt: message.editedAt?.toISOString() ?? new Date().toISOString()
|
editedAt: message.editedAt?.toISOString() ?? new Date().toISOString()
|
||||||
|
};
|
||||||
|
await this.notifications.publishRealtime(userId, 'bot_message_edited', bot.name, message.text ?? '', payload);
|
||||||
|
await this.publishToSharedBotRoomMembers(userId, bot, 'bot_message_edited', message.text ?? '', payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async publishToSharedBotRoomMembers(
|
||||||
|
ownerUserId: string,
|
||||||
|
bot: ValidatedBot,
|
||||||
|
eventType: string,
|
||||||
|
message: string,
|
||||||
|
payload: Record<string, unknown>
|
||||||
|
) {
|
||||||
|
const rooms = await this.prisma.chatRoom.findMany({
|
||||||
|
where: {
|
||||||
|
type: 'BOT',
|
||||||
|
botUsername: bot.username,
|
||||||
|
createdById: ownerUserId
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: { include: { user: { select: { linkedBotId: true } } } }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const recipients = new Set<string>();
|
||||||
|
for (const room of rooms) {
|
||||||
|
for (const member of room.members) {
|
||||||
|
if (member.userId !== ownerUserId && !member.user.linkedBotId) {
|
||||||
|
recipients.add(member.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...recipients].map((recipientId) => this.notifications.publishRealtime(recipientId, eventType, bot.name, message, payload))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private mergePayload(existing: unknown, parsed: ReturnType<TelegramSerializerService['parseReplyMarkup']>) {
|
private mergePayload(existing: unknown, parsed: ReturnType<TelegramSerializerService['parseReplyMarkup']>) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class BotInboundService {
|
|||||||
private readonly seed: BotFatherSeedService,
|
private readonly seed: BotFatherSeedService,
|
||||||
private readonly assistant: BotFatherAssistantService
|
private readonly assistant: BotFatherAssistantService
|
||||||
) {}
|
) {}
|
||||||
async submitUserMessage(senderUserId: string, botRef: string, text: string) {
|
async submitUserMessage(senderUserId: string, botRef: string, text: string, roomId?: string) {
|
||||||
await this.assertHumanSender(senderUserId);
|
await this.assertHumanSender(senderUserId);
|
||||||
|
|
||||||
const trimmedText = text.trim();
|
const trimmedText = text.trim();
|
||||||
@@ -30,7 +30,9 @@ export class BotInboundService {
|
|||||||
throw new NotFoundException('Бот не найден или заблокирован');
|
throw new NotFoundException('Бот не найден или заблокирован');
|
||||||
}
|
}
|
||||||
|
|
||||||
const chat = await this.ensureBotChat(bot.id, senderUserId);
|
const chat = roomId
|
||||||
|
? await this.resolveSharedFamilyBotChat(bot.id, senderUserId, roomId)
|
||||||
|
: await this.ensureBotChat(bot.id, senderUserId);
|
||||||
const event = await this.createMessageEvent(bot.id, chat.id, senderUserId, chat.telegramChatId, trimmedText);
|
const event = await this.createMessageEvent(bot.id, chat.id, senderUserId, chat.telegramChatId, trimmedText);
|
||||||
await this.dispatchEvent(event);
|
await this.dispatchEvent(event);
|
||||||
if (bot.isSystemBot) {
|
if (bot.isSystemBot) {
|
||||||
@@ -210,4 +212,25 @@ export class BotInboundService {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveSharedFamilyBotChat(botId: string, senderUserId: string, roomId: string) {
|
||||||
|
const room = await this.prisma.chatRoom.findUnique({
|
||||||
|
where: { id: roomId },
|
||||||
|
include: {
|
||||||
|
members: { include: { user: { select: { linkedBotId: true } } } }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!room || room.type !== 'BOT' || !room.members.some((member) => member.userId === senderUserId)) {
|
||||||
|
throw new NotFoundException('Чат с ботом не найден');
|
||||||
|
}
|
||||||
|
const botMember = room.members.find((member) => member.user.linkedBotId === botId);
|
||||||
|
if (!botMember || room.peerUserId !== botMember.userId) {
|
||||||
|
throw new NotFoundException('Бот не найден в этом чате');
|
||||||
|
}
|
||||||
|
const ownerUserId =
|
||||||
|
room.createdById ??
|
||||||
|
room.members.find((member) => !member.user.linkedBotId)?.userId ??
|
||||||
|
senderUserId;
|
||||||
|
return this.ensureBotChat(botId, ownerUserId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,8 +127,6 @@ export class ChatService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const humans = members.filter((member) => !member.user.linkedBotId);
|
const humans = members.filter((member) => !member.user.linkedBotId);
|
||||||
const bots = members.filter((member) => member.user.linkedBotId);
|
|
||||||
|
|
||||||
const legacyRooms = await this.prisma.chatRoom.findMany({
|
const legacyRooms = await this.prisma.chatRoom.findMany({
|
||||||
where: { groupId, type: 'GROUP' },
|
where: { groupId, type: 'GROUP' },
|
||||||
include: { members: true }
|
include: { members: true }
|
||||||
@@ -177,18 +175,24 @@ export class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const botMember of bots) {
|
// BOT-чаты создаются только для тех людей, которым явно выдали доступ к боту.
|
||||||
for (const human of humans) {
|
// Не создаём комнаты "каждый человек x каждый бот", иначе бот становится видимым всей семье.
|
||||||
await this.ensureBotRoom(
|
}
|
||||||
|
|
||||||
|
async ensureBotRoomForMember(groupId: string, requesterId: string, botUserId: string) {
|
||||||
|
const group = await this.family.assertFamilyMember(groupId, requesterId);
|
||||||
|
const botMember = group.members.find((member) => member.userId === botUserId);
|
||||||
|
if (!botMember?.user?.linkedBotId || !botMember.user.linkedBot) {
|
||||||
|
throw new BadRequestException('Бот не найден в этой семье');
|
||||||
|
}
|
||||||
|
return this.ensureBotRoom(
|
||||||
groupId,
|
groupId,
|
||||||
human.userId,
|
requesterId,
|
||||||
botMember.userId,
|
botUserId,
|
||||||
botMember.user.linkedBot!.username,
|
botMember.user.linkedBot.username,
|
||||||
botMember.user.displayName
|
botMember.user.displayName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRoom(userId: string, groupId: string, name: string, memberUserIds: string[]) {
|
async createRoom(userId: string, groupId: string, name: string, memberUserIds: string[]) {
|
||||||
const group = await this.family.assertFamilyMember(groupId, userId);
|
const group = await this.family.assertFamilyMember(groupId, userId);
|
||||||
@@ -313,7 +317,7 @@ export class ChatService {
|
|||||||
if (room.type === 'GENERAL') {
|
if (room.type === 'GENERAL') {
|
||||||
throw new BadRequestException('В общий чат нельзя добавлять участников вручную');
|
throw new BadRequestException('В общий чат нельзя добавлять участников вручную');
|
||||||
}
|
}
|
||||||
if (room.type === 'DIRECT' || room.type === 'BOT' || room.type === 'E2E') {
|
if (room.type === 'DIRECT' || room.type === 'E2E') {
|
||||||
throw new BadRequestException('Нельзя изменять состав личного чата или чата с ботом');
|
throw new BadRequestException('Нельзя изменять состав личного чата или чата с ботом');
|
||||||
}
|
}
|
||||||
if (room.members.some((member) => member.userId === memberUserId)) {
|
if (room.members.some((member) => member.userId === memberUserId)) {
|
||||||
@@ -322,6 +326,15 @@ export class ChatService {
|
|||||||
if (!room.group.members.some((member) => member.userId === memberUserId)) {
|
if (!room.group.members.some((member) => member.userId === memberUserId)) {
|
||||||
throw new BadRequestException('Пользователь должен состоять в семье');
|
throw new BadRequestException('Пользователь должен состоять в семье');
|
||||||
}
|
}
|
||||||
|
if (room.type === 'BOT') {
|
||||||
|
const targetUser = await this.prisma.user.findUnique({
|
||||||
|
where: { id: memberUserId },
|
||||||
|
select: { linkedBotId: true, isSystemAccount: true }
|
||||||
|
});
|
||||||
|
if (!targetUser || targetUser.linkedBotId || targetUser.isSystemAccount) {
|
||||||
|
throw new BadRequestException('В чат с ботом можно добавить только обычного участника семьи');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.prisma.chatRoomMember.create({
|
await this.prisma.chatRoomMember.create({
|
||||||
data: { roomId, userId: memberUserId }
|
data: { roomId, userId: memberUserId }
|
||||||
@@ -335,21 +348,24 @@ export class ChatService {
|
|||||||
if (room.type === 'GENERAL') {
|
if (room.type === 'GENERAL') {
|
||||||
throw new BadRequestException('Нельзя удалить участника из общего чата');
|
throw new BadRequestException('Нельзя удалить участника из общего чата');
|
||||||
}
|
}
|
||||||
if (room.type === 'DIRECT' || room.type === 'BOT' || room.type === 'E2E') {
|
if (room.type === 'DIRECT' || room.type === 'E2E') {
|
||||||
throw new BadRequestException('Нельзя изменять состав личного чата или чата с ботом');
|
throw new BadRequestException('Нельзя изменять состав личного чата или чата с ботом');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFamilyOwner = room.group.ownerId === userId;
|
const isFamilyOwner = room.group.ownerId === userId;
|
||||||
const isChatCreator = room.createdById === userId;
|
const isChatCreator = room.createdById === userId;
|
||||||
if (!isFamilyOwner && !isChatCreator) {
|
if (room.type !== 'BOT' && !isFamilyOwner && !isChatCreator) {
|
||||||
throw new ForbiddenException('Недостаточно прав для удаления участника из чата');
|
throw new ForbiddenException('Недостаточно прав для удаления участника из чата');
|
||||||
}
|
}
|
||||||
|
if (room.type === 'BOT' && memberUserId === room.peerUserId) {
|
||||||
|
throw new BadRequestException('Нельзя удалить бота из его чата');
|
||||||
|
}
|
||||||
|
|
||||||
if (memberUserId === room.createdById && !isFamilyOwner) {
|
if (memberUserId === room.createdById && !isFamilyOwner) {
|
||||||
throw new BadRequestException('Создателя чата может удалить только создатель семьи');
|
throw new BadRequestException('Создателя чата может удалить только создатель семьи');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (memberUserId === userId && room.members.length <= 1) {
|
if (memberUserId === userId && room.members.length <= (room.type === 'BOT' ? 2 : 1)) {
|
||||||
throw new BadRequestException('В чате должен остаться хотя бы один участник');
|
throw new BadRequestException('В чате должен остаться хотя бы один участник');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,7 +931,7 @@ export class ChatService {
|
|||||||
userId: string;
|
userId: string;
|
||||||
notificationsMuted: boolean;
|
notificationsMuted: boolean;
|
||||||
pinnedAt?: Date | null;
|
pinnedAt?: Date | null;
|
||||||
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 };
|
||||||
}>;
|
}>;
|
||||||
},
|
},
|
||||||
familyRoleByUserId: Map<string, string>,
|
familyRoleByUserId: Map<string, string>,
|
||||||
@@ -924,7 +940,9 @@ export class ChatService {
|
|||||||
) {
|
) {
|
||||||
const viewerMembership = viewerId ? room.members.find((member) => member.userId === viewerId) : undefined;
|
const viewerMembership = viewerId ? room.members.find((member) => member.userId === viewerId) : undefined;
|
||||||
const peerMember =
|
const peerMember =
|
||||||
viewerId && (room.type === 'DIRECT' || room.type === 'BOT' || room.type === 'E2E')
|
room.type === 'BOT'
|
||||||
|
? room.members.find((member) => Boolean(member.user.linkedBotId))
|
||||||
|
: viewerId && (room.type === 'DIRECT' || room.type === 'E2E')
|
||||||
? room.members.find((member) => member.userId !== viewerId)
|
? room.members.find((member) => member.userId !== viewerId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -1195,6 +1213,7 @@ export class ChatService {
|
|||||||
name: botDisplayName,
|
name: botDisplayName,
|
||||||
peerUserId: botUserId,
|
peerUserId: botUserId,
|
||||||
botUsername,
|
botUsername,
|
||||||
|
createdById: humanUserId,
|
||||||
isE2E: false,
|
isE2E: false,
|
||||||
members: {
|
members: {
|
||||||
create: [{ userId: humanUserId }, { userId: botUserId }]
|
create: [{ userId: humanUserId }, { userId: botUserId }]
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export class FamilyService {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
return this.toGroup(group);
|
return this.toGroup(group, ownerId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ export class FamilyService {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return { groups: groups.map((group) => this.toGroup(group)) };
|
return { groups: await Promise.all(groups.map((group) => this.toGroup(group, userId))) };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ export class FamilyService {
|
|||||||
|
|
||||||
this.assertMember(group, requesterId);
|
this.assertMember(group, requesterId);
|
||||||
|
|
||||||
return this.toGroup(group);
|
return this.toGroup(group, requesterId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ export class FamilyService {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.toGroup(updated);
|
return this.toGroup(updated, requesterId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,23 +457,30 @@ export class FamilyService {
|
|||||||
throw new BadRequestException('Пользователь не найден. Выберите его из результатов поиска.');
|
throw new BadRequestException('Пользователь не найден. Выберите его из результатов поиска.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inviter = await this.prisma.user.findUnique({ where: { id: requesterId } });
|
||||||
|
const isBotInvite = isProtectedSystemAccount(invitee);
|
||||||
|
const inviteeAlreadyInFamily = group.members.some((member) => member.userId === invitee.id);
|
||||||
|
|
||||||
if (invitee.id === requesterId) {
|
if (invitee.id === requesterId) {
|
||||||
throw new BadRequestException('Нельзя пригласить самого себя');
|
throw new BadRequestException('Нельзя пригласить самого себя');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.members.some((member) => member.userId === invitee.id)) {
|
if (isBotInvite) {
|
||||||
|
const visibleBotUserIds = await this.getVisibleBotUserIds(groupId, requesterId);
|
||||||
|
if (visibleBotUserIds.has(invitee.id)) {
|
||||||
|
throw new BadRequestException('Этот бот уже доступен вам в семье');
|
||||||
|
}
|
||||||
|
} else if (inviteeAlreadyInFamily) {
|
||||||
throw new BadRequestException('Пользователь уже состоит в семье');
|
throw new BadRequestException('Пользователь уже состоит в семье');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingPending = await this.prisma.familyInvite.findFirst({
|
const existingPending = await this.prisma.familyInvite.findFirst({
|
||||||
where: { groupId, inviteeUserId: invitee.id, status: 'PENDING' }
|
where: { groupId, inviteeUserId: invitee.id, status: 'PENDING' }
|
||||||
});
|
});
|
||||||
if (existingPending) {
|
if (existingPending && !isBotInvite) {
|
||||||
throw new BadRequestException('Приглашение уже отправлено');
|
throw new BadRequestException('Приглашение уже отправлено');
|
||||||
}
|
}
|
||||||
|
|
||||||
const inviter = await this.prisma.user.findUnique({ where: { id: requesterId } });
|
|
||||||
const isBotInvite = isProtectedSystemAccount(invitee);
|
|
||||||
if (isBotInvite) {
|
if (isBotInvite) {
|
||||||
const invite = await this.prisma.familyInvite.create({
|
const invite = await this.prisma.familyInvite.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -490,7 +497,9 @@ export class FamilyService {
|
|||||||
inviter: true
|
inviter: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (!inviteeAlreadyInFamily) {
|
||||||
await this.addMember(groupId, invitee.id, 'bot');
|
await this.addMember(groupId, invitee.id, 'bot');
|
||||||
|
}
|
||||||
await this.notifications.create(
|
await this.notifications.create(
|
||||||
requesterId,
|
requesterId,
|
||||||
'family_bot_added',
|
'family_bot_added',
|
||||||
@@ -537,24 +546,18 @@ export class FamilyService {
|
|||||||
return { users: [] };
|
return { users: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const excludedIds = [...group.members.map((member) => member.userId), requesterId];
|
const excludedIds = [...group.members.filter((member) => !member.user?.linkedBotId).map((member) => member.userId), requesterId];
|
||||||
const excludedBotUsernames = new Set(
|
const visibleBotUserIds = await this.getVisibleBotUserIds(groupId, requesterId);
|
||||||
group.members
|
|
||||||
.map((member) => member.user?.linkedBot?.username)
|
|
||||||
.filter((username): username is string => Boolean(username))
|
|
||||||
);
|
|
||||||
const botFatherUserId = await this.botFatherSeed.getBotFatherUserId();
|
const botFatherUserId = await this.botFatherSeed.getBotFatherUserId();
|
||||||
const botFatherBotId = await this.botFatherSeed.getBotFatherBotId();
|
const botFatherBotId = await this.botFatherSeed.getBotFatherBotId();
|
||||||
const getMyIdUserId = await this.botFatherSeed.getGetMyIdBotUserId();
|
const getMyIdUserId = await this.botFatherSeed.getGetMyIdBotUserId();
|
||||||
const getMyIdBotId = await this.botFatherSeed.getGetMyIdBotId();
|
const getMyIdBotId = await this.botFatherSeed.getGetMyIdBotId();
|
||||||
const botFatherMatches =
|
const botFatherMatches =
|
||||||
/bot\s*father|бот\s*father|botfather/i.test(trimmed) &&
|
/bot\s*father|бот\s*father|botfather/i.test(trimmed) &&
|
||||||
!excludedIds.includes(botFatherUserId) &&
|
!visibleBotUserIds.has(botFatherUserId);
|
||||||
!excludedBotUsernames.has(BOTFATHER_BOT_USERNAME);
|
|
||||||
const getMyIdMatches =
|
const getMyIdMatches =
|
||||||
/get\s*my\s*id|getmyid|мой\s*id|айди|chat[_\s-]*id/i.test(trimmed) &&
|
/get\s*my\s*id|getmyid|мой\s*id|айди|chat[_\s-]*id/i.test(trimmed) &&
|
||||||
!excludedIds.includes(getMyIdUserId) &&
|
!visibleBotUserIds.has(getMyIdUserId);
|
||||||
!excludedBotUsernames.has(GET_MY_ID_BOT_USERNAME);
|
|
||||||
const digits = trimmed.replace(/\D/g, '');
|
const digits = trimmed.replace(/\D/g, '');
|
||||||
const normalizedBotQuery = normalizeBotUsername(trimmed.replace(/^@/, ''));
|
const normalizedBotQuery = normalizeBotUsername(trimmed.replace(/^@/, ''));
|
||||||
|
|
||||||
@@ -619,13 +622,12 @@ export class FamilyService {
|
|||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
for (const bot of matchedBots) {
|
for (const bot of matchedBots) {
|
||||||
if (excludedBotUsernames.has(bot.username)) continue;
|
|
||||||
if (botFatherMatches && bot.username === BOTFATHER_BOT_USERNAME) continue;
|
if (botFatherMatches && bot.username === BOTFATHER_BOT_USERNAME) continue;
|
||||||
if (getMyIdMatches && bot.username === GET_MY_ID_BOT_USERNAME) continue;
|
if (getMyIdMatches && bot.username === GET_MY_ID_BOT_USERNAME) continue;
|
||||||
const { userId } = bot.linkedSystemUser
|
const { userId } = bot.linkedSystemUser
|
||||||
? { userId: bot.linkedSystemUser.id }
|
? { userId: bot.linkedSystemUser.id }
|
||||||
: await this.botFather.ensureBotSystemUser(bot.id);
|
: await this.botFather.ensureBotSystemUser(bot.id);
|
||||||
if (excludedIds.includes(userId)) continue;
|
if (visibleBotUserIds.has(userId)) continue;
|
||||||
botCandidates.push({
|
botCandidates.push({
|
||||||
id: userId,
|
id: userId,
|
||||||
botId: bot.id,
|
botId: bot.id,
|
||||||
@@ -963,7 +965,7 @@ export class FamilyService {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
private toGroup(group: {
|
private async toGroup(group: {
|
||||||
|
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@@ -1000,7 +1002,9 @@ export class FamilyService {
|
|||||||
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
}) {
|
}, viewerId?: string) {
|
||||||
|
const visibleBotUserIds = viewerId ? await this.getVisibleBotUserIds(group.id, viewerId) : new Set<string>();
|
||||||
|
const visibleMembers = group.members.filter((member) => !member.user?.linkedBotId || visibleBotUserIds.has(member.userId));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
@@ -1016,13 +1020,13 @@ export class FamilyService {
|
|||||||
|
|
||||||
updatedAt: group.updatedAt.toISOString(),
|
updatedAt: group.updatedAt.toISOString(),
|
||||||
|
|
||||||
members: group.members.map((member) => this.toMember(member)),
|
members: visibleMembers.map((member) => this.toMember(member)),
|
||||||
|
|
||||||
botFatherAvailable: !group.members.some(
|
botFatherAvailable: !visibleMembers.some(
|
||||||
(member) => member.user?.linkedBot?.username === BOTFATHER_BOT_USERNAME
|
(member) => member.user?.linkedBot?.username === BOTFATHER_BOT_USERNAME
|
||||||
),
|
),
|
||||||
|
|
||||||
getMyIdBotAvailable: !group.members.some(
|
getMyIdBotAvailable: !visibleMembers.some(
|
||||||
(member) => member.user?.linkedBot?.username === GET_MY_ID_BOT_USERNAME
|
(member) => member.user?.linkedBot?.username === GET_MY_ID_BOT_USERNAME
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1030,6 +1034,27 @@ export class FamilyService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getVisibleBotUserIds(groupId: string, viewerId: string) {
|
||||||
|
const rooms = await this.prisma.chatRoom.findMany({
|
||||||
|
where: {
|
||||||
|
groupId,
|
||||||
|
type: 'BOT',
|
||||||
|
members: { some: { userId: viewerId } }
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
members: {
|
||||||
|
include: { user: { select: { linkedBotId: true } } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new Set(
|
||||||
|
rooms
|
||||||
|
.flatMap((room) => room.members)
|
||||||
|
.filter((member) => Boolean(member.user.linkedBotId))
|
||||||
|
.map((member) => member.userId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private toMember(member: {
|
private toMember(member: {
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ message SubmitBotInboundMessageRequest {
|
|||||||
string senderUserId = 1;
|
string senderUserId = 1;
|
||||||
string botRef = 2;
|
string botRef = 2;
|
||||||
string text = 3;
|
string text = 3;
|
||||||
|
optional string roomId = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SubmitBotInboundMessageResponse {
|
message SubmitBotInboundMessageResponse {
|
||||||
@@ -210,6 +211,7 @@ message SubmitBotCallbackQueryResponse {
|
|||||||
message ListBotChatMessagesRequest {
|
message ListBotChatMessagesRequest {
|
||||||
string userId = 1;
|
string userId = 1;
|
||||||
string botRef = 2;
|
string botRef = 2;
|
||||||
|
optional string roomId = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BotChatMessageItem {
|
message BotChatMessageItem {
|
||||||
|
|||||||
Reference in New Issue
Block a user