import type { ChatMemberRepository } from "../repositories/chat-member-repository.js"; import type { ChatMember, MemberProfile } from "../types.js"; interface ChatMemberServiceDeps { chatMemberRepository: ChatMemberRepository; } export interface MentionQuery { /** Ники из `@username` без собаки. */ usernames: string[]; /** ID людей, упомянутых по имени-ссылке (у них может не быть ника). */ userIds: number[]; } export interface ResolvedMentions { members: ChatMember[]; /** Ники, которых бот в этом чате не встречал. */ unknownUsernames: string[]; } /** Регистр и «ё» не важны: «Алёна» найдётся по «алена». */ const normalize = (value: string): string => value.toLocaleLowerCase("ru").replaceAll("ё", "е").trim(); const words = (member: ChatMember): string[] => [member.firstName, member.lastName ?? "", member.username ?? ""].flatMap((part) => normalize(part).split(/\s+/)); const usernameIs = (username: string) => (member: ChatMember) => member.username !== null && normalize(member.username) === normalize(username); /** Каждое слово запроса — начало одного из слов имени или ника: «петр» найдёт «Петров». */ const nameMatches = (query: string) => (member: ChatMember) => { const memberWords = words(member); return normalize(query) .split(/\s+/) .filter(Boolean) .every((token) => memberWords.some((word) => word.startsWith(token))); }; export const createChatMemberService = ({ chatMemberRepository }: ChatMemberServiceDeps) => { /** Запоминаем актуальные имя и ник: автор сообщения или человек, упомянутый по ссылке. */ const remember = (chatId: number, profile: MemberProfile): void => chatMemberRepository.upsert(chatId, profile); /** Участники с активностью по сохранённой истории; сначала — недавно писавшие. */ const list = (chatId: number): ChatMember[] => chatMemberRepository.listByChat(chatId); /** Поиск по нику (с @ или без) или по имени/фамилии. */ const search = (chatId: number, query: string): ChatMember[] => { const cleaned = query.trim().replace(/^@/, ""); if (!cleaned) return []; const members = list(chatId); const byUsername = members.filter(usernameIs(cleaned)); return byUsername.length > 0 ? byUsername : members.filter(nameMatches(cleaned)); }; /** Превращает упоминания из вопроса в известных участников чата. */ const resolveMentions = (chatId: number, { usernames, userIds }: MentionQuery): ResolvedMentions => { if (usernames.length === 0 && userIds.length === 0) return { members: [], unknownUsernames: [] }; const members = list(chatId); const unknownUsernames = usernames.filter((username) => !members.some(usernameIs(username))); const mentioned = members.filter( (member) => userIds.includes(member.userId) || usernames.some((username) => usernameIs(username)(member)), ); return { members: mentioned, unknownUsernames }; }; return { remember, list, search, resolveMentions }; }; export type ChatMemberService = ReturnType;