import { messageApi } from "@/api/message-api.service"; export class TeamsChatService { private readonly MY_NAME = "Apactech com"; private lastMessage?: IMessage; private initialHistories: IMessage[] = []; public elTags = { container_scroll: "/html/body/div[1]/div/div/div/div[5]/div[1]/div[1]/div[2]/div[1]/div[1]/div", conatainer_conversations: "/html/body/div[1]/div/div/div/div[5]/div[1]/div[1]/div[2]/div[1]/div[1]/div/div[1]", container_chat: "/html/body/div[1]/div/div/div/div[6]/div[4]/div/div[1]/div/div[1]/div/div/div/div[1]/div/div/div[4]", root_id: '[aria-selected="true"] [id^="chat-list-item"]', room_name: '[data-tid="chat-title"]', close_reply_btn: "/html/body/div[1]/div/div/div/div[6]/div[4]/div/div[1]/div/div[3]/div/div[3]/div/div[2]/div/div[2]/div/div/card/div/div/div[2]/div/div[1]/button", reply_btn: '[aria-label="Reply"]', chat_input: // "/html/body/div[1]/div/div/div/div[6]/div[4]/div/div[1]/div/div[3]/div/div[3]/div/div[2]/div/div[2]/div[1]/div", '[placeholder="Type a message"]', }; 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 }; } private _getMessageByEl(el: HTMLElement | null): string { if (!el) return ""; // Lấy text ban đầu (nếu có) let message = el.innerText || ""; // Nếu có ảnh gửi kèm (ảnh chia sẻ), thì ưu tiên trả về ảnh const sharedImage = el.querySelector( "img[data-gallery-src]" ) as HTMLImageElement | null; if (sharedImage) { return sharedImage.getAttribute("data-gallery-src") || ""; } // Tìm tất cả emoji theo itemtype const emojiImgs = Array.from( el.querySelectorAll("img[itemtype]") ) as HTMLImageElement[]; const emojiAlts = emojiImgs .map((img) => img.getAttribute("alt") || "") .filter(Boolean); // Nối emoji vào cuối chuỗi gốc (hoặc có thể chèn theo vị trí nâng cao) if (emojiAlts.length) { message += emojiAlts.join(""); } return message.trim(); } 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; 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(), }; } 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); } private 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(); } 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."); return []; } // Lọc phần tử con có role="none" const matchedChildren = Array.from(result.children).filter( (child: Element) => { return child.getAttribute("role") === "none"; } ); const data: ChatItem[] = matchedChildren.map((child: Element): ChatItem => { const id = child.id || 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); 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(); } }