/* eslint-disable @typescript-eslint/no-explicit-any */ import { messageApi } from "@/api/message-api.service"; export class TeamsChatService { // private readonly MY_NAME = "Apactech com"; public lastMessage?: IMessage; public initialHistories: IMessage[] = []; public elTags = { container_scroll: '//*[@data-testid="simple-collab-rail"]', conatainer_conversations: '//*[@data-testid="simple-collab-dnd-rail"]', container_chat: '[data-testid="message-wrapper"]', root_id: '[aria-selected="true"] [id^="chat-list-item"]', room_name: '[data-tid="chat-title"]', close_reply_btn: '[data-track-action-scenario="messageQuotedReplyDismissed"]', reply_btn: '[aria-label="Reply"]', chat_input: '[placeholder="Type a message"]', }; private _getImageFormEl(el: HTMLElement): HTMLImageElement[] { // Tìm tất cả img có data-gallery-src trong el let sharedImages = Array.from( el.querySelectorAll("img[data-gallery-src]"), ) as HTMLImageElement[]; // Nếu không tìm thấy thì thử tìm trong parentElement if (sharedImages.length === 0 && el.parentElement) { sharedImages = Array.from( el.parentElement.querySelectorAll("img[data-gallery-src]"), ) as HTMLImageElement[]; } return sharedImages; // Luôn trả về array (có thể rỗng) } public getCurrentRoomInfo(): { room_id?: string; room_name?: string } { // const roomId = document // .querySelector(this.elTags.root_id) // ?.id?.split(":")[1]; const roomId = document .querySelector(this.elTags.root_id) ?.id?.replace("chat-list-item_", ""); const roomName = ( document.querySelector(this.elTags.room_name) as HTMLElement )?.innerText; return { room_id: roomId, room_name: roomName }; } _getFileLinks(el: HTMLElement) { const parentId = `attachments-${(el as any)?.date_time}`; const parent = document.getElementById(parentId); if (!parent) return [] as { name: string; url: string }[]; // Lấy tất cả con trực tiếp const children = Array.from(parent.children); // Lấy title, lọc link và trả về {name, url} const results = children.flatMap((child) => { const token = this.getAssetToken(); const title = child.getAttribute("title"); if (!title) return []; // Giả sử format: "tên file \r\n link" const lines = title .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); const name = lines[0] || null; const urlMatch = lines.find((line) => /^https?:\/\//.test(line)); if (!urlMatch) return []; return [{ name, url: urlMatch, token }]; }); return results; } // private _getMessageByEl(el: HTMLElement | null): string | string[] { // if (!el) return ""; // // Lấy text ban đầu và loại bỏ khoảng trắng dư // let message = el.innerText?.trim() || ""; // // Lấy danh sách ảnh (nếu có) // const sharedImages = this._getImageFormEl(el) || []; // if (sharedImages.length > 0) { // const arrMessage = sharedImages.map( // (img) => img.getAttribute("src") || "" // ); // if (message.length > 0) { // // Kiểm tra overlay // const overlay = document // .getElementById(`message-body-${(el as any)?.date_time}`) // ?.querySelector('[data-tid="overlay-count-text"]'); // // Nếu overlay text === message gốc → chỉ trả về ảnh // if (overlay && (overlay as HTMLElement).innerText === message) { // return arrMessage; // } // // Nếu không, thêm message vào cuối // arrMessage.push(message); // } // return arrMessage; // } // // Tìm tất cả emoji trong element // const emojiImgs = Array.from( // el.querySelectorAll("img[itemtype]") // ) as HTMLImageElement[]; // const emojiAlts = emojiImgs // .map((img) => img.getAttribute("alt") || "") // .filter(Boolean); // // Nếu có emoji thì nối vào message // if (emojiAlts.length > 0) { // message = `${message}${emojiAlts.join("")}`; // } // return message.trim(); // } private _getMessageByEl(el: HTMLElement | null): string[] { if (!el) return []; const messages: string[] = []; // Lấy text ban đầu và loại bỏ khoảng trắng dư const messageText = el.innerText?.trim() || ""; // Lấy danh sách ảnh (nếu có) const sharedImages = this._getImageFormEl(el) || []; const imageSrcs = sharedImages .map((img) => img.getAttribute("src")) .filter(Boolean) as string[]; if (imageSrcs.length > 0) { // Kiểm tra overlay const overlay = document .getElementById(`message-body-${(el as any)?.date_time}`) ?.querySelector('[data-tid="overlay-count-text"]'); if (overlay && (overlay as HTMLElement).innerText === messageText) { messages.push(...imageSrcs); } else { messages.push(...imageSrcs); if (messageText) messages.push(messageText); } } else if (messageText) { messages.push(messageText); } const fileLinks = this._getFileLinks(el) || []; if (fileLinks.length) { fileLinks.forEach((link) => { messages.push(JSON.stringify({ ...link, type: "file" })); }); } // Lấy emoji const emojiImgs = Array.from( el.querySelectorAll("img[itemtype]"), ) as HTMLImageElement[]; const emojiAlts = emojiImgs .map((img) => img.getAttribute("alt") || "") .filter(Boolean); if (emojiAlts.length > 0) { messages.push(emojiAlts.join("")); } return messages; } public parseMessageElement(el: Element, isMine = false): IMessage | null { const timestampEl = el.querySelector( isMine ? ".fui-ChatMyMessage__timestamp" : ".fui-ChatMessage__timestamp", ) as HTMLElement | null; const authorEl = el.querySelector( isMine ? ".fui-ChatMyMessage__author" : ".fui-ChatMessage__author", ) as HTMLElement | null; if (!timestampEl) return null; const datetimeAttr = timestampEl.getAttribute("datetime"); if (!datetimeAttr) return null; // const dateTime = new Date(datetimeAttr).getTime(); const dateTime = Number.isNaN(timestampEl.id.replace("timestamp-", "")) ? new Date(datetimeAttr).getTime() : Number(timestampEl.id.replace("timestamp-", "")); const contentEl = document.querySelector( `#content-${dateTime}`, ) as HTMLElement | null; (contentEl as any)["date_time"] = dateTime; const { room_id, room_name } = this.getCurrentRoomInfo(); return { name: authorEl?.innerText, message: this._getMessageByEl(contentEl), time: dateTime, room_id, room_name, date_time: new Date(datetimeAttr).getTime(), }; } getAssetToken() { const tokenKey = Object.keys(localStorage).find((key) => { const value = localStorage[key]; return ( value.includes('"credentialType":"AccessToken"') && value.includes('"target":"https://graph.microsoft.com/.default') ); }); const accessToken = tokenKey ? JSON.parse(localStorage[tokenKey]).secret : null; return accessToken; } extractAllMessages(): IMessage[] { const myMessages: IMessage[] = Array.from( document.querySelectorAll(".fui-ChatMyMessage"), ) .map((el) => this.parseMessageElement(el, true)) .filter((msg): msg is IMessage => msg !== null); const otherMessages: IMessage[] = Array.from( document.querySelectorAll(".fui-ChatMessage"), ) .map((el) => this.parseMessageElement(el, false)) .filter((msg): msg is IMessage => msg !== null); console.log({ myMessages, otherMessages }); return [...myMessages, ...otherMessages].sort((a, b) => a.time - b.time); } // private handleNewMessage(message: IMessage) { // // Gửi tới một server AI để phản hồi (nếu cần) // // fetch('https://127.0.0.1:8443/reply', { ... }) // console.log("%c[New incoming message]", "color: #007acc;", message); // } public async detectNewMessages() { const allMessages = this.extractAllMessages(); const lastIndex = allMessages.findIndex( (msg) => msg.time === this.lastMessage?.time, ); const newMessages = allMessages.slice(lastIndex + 1); if (newMessages.length === 0) { console.log("[Monitor] No new messages..."); return; } const newMsg = newMessages[0]; // if (newMsg.name === this.MY_NAME) { // console.log("[Monitor] My new message:", newMsg); // await messageApi.sendSingleMessage(newMsg); // } else { // console.log("[Monitor] New incoming message:", newMsg); // this.handleNewMessage(newMsg); // messageApi.sendSingleMessage(newMsg); // } this.lastMessage = allMessages.pop(); return newMsg; } public async start(interval = 10000) { console.log("[Monitor] Starting..."); this.initialHistories = this.extractAllMessages(); this.lastMessage = this.initialHistories.pop(); // await messageApi.sendBulkMessages(this.initialHistories); setInterval(async () => await this.detectNewMessages(), interval); } private async _getConversationsInfo( xpath: string = this.elTags.conatainer_conversations, ): Promise { const result = document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null, ).singleNodeValue as HTMLElement | null; if (!result) { console.log("Không tìm thấy phần tử theo XPath."); messageApi.sendLog({ type: "error", message: `Not found selector: ${xpath}`, }); return []; } const chatItems = Array.from( result.querySelectorAll('[data-item-type="chat"]'), ); const data: ChatItem[] = chatItems.map((child: Element): ChatItem => { const treeItemValue = child.getAttribute("data-fui-tree-item-value") || ""; const lastSegment = treeItemValue.split("/").pop() || ""; const id = lastSegment.includes("|") ? lastSegment.split("|")[1] : null; const titleId = `title-chat-list-item_${id}`; const titleElement = document.getElementById(titleId); const spanText = titleElement?.innerText || null; let type: ChatItemType = null; if (id?.includes("@thread.skype")) { type = "group"; } else if (id?.includes("@oneToOne.skype")) { type = "personal"; } else { type = "group"; } return { id, name: spanText, type, }; }); return data; } public async _scrollToBottomByXPath( xpath: string = this.elTags.container_scroll, options?: { maxStableRounds?: number; // Số vòng scroll không thay đổi trước khi dừng delay?: number; // Thời gian chờ giữa mỗi lần scroll (ms) maxScrolls?: number; // Giới hạn số lần scroll tối đa }, ): Promise { const { maxStableRounds = 5, delay = 300, maxScrolls = 100, } = options || {}; const container = document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null, ).singleNodeValue as HTMLElement | null; if (!container) { console.warn("❌ Không tìm thấy phần tử với XPath:", xpath); messageApi.sendLog({ type: "error", message: `Not found selector: ${xpath}`, }); return; } return new Promise((resolve) => { let lastHeight = -1; let stableCount = 0; let scrolls = 0; const interval = setInterval(() => { const currentHeight = container.scrollHeight; container.scrollTop = currentHeight; if (currentHeight === lastHeight) { stableCount++; } else { stableCount = 0; lastHeight = currentHeight; } scrolls++; if (stableCount >= maxStableRounds || scrolls >= maxScrolls) { clearInterval(interval); resolve(); } }, delay); }); } async handleGetConversations() { await this._scrollToBottomByXPath(); return this._getConversationsInfo(); } }