/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { queue } from "@/lib/queue"; import { TeamsChatService } from "./teams-chat.service"; import { EVENTS } from "@/lib/event"; import { typeingService } from "./typing.service"; import { delay } from "@/features/app"; import { messageApi } from "@/api/message-api.service"; import { conversationApi } from "@/api/conversation-api.service"; export class ContentService { service: TeamsChatService; port; constructor(port: any) { this.service = new TeamsChatService(); this.port = port; } _forceHeightObserver: any; getElementByXPath(xpath: string) { return document.evaluate( xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null, ).singleNodeValue; } forceHeight(el: HTMLElement, height = "100px") { if (!el) return; el.style.setProperty("height", height, "important"); // Gỡ bỏ auto-resize (nếu là textarea) el.style.setProperty("resize", "none", "important"); // Lặp lại khi element bị focus/input const keepHeight = () => { el.style.setProperty("height", height, "important"); }; el.addEventListener("focus", keepHeight); el.addEventListener("input", keepHeight); el.addEventListener("blur", keepHeight); // Thêm mutation observer nếu bị script khác chỉnh sửa const observer = new MutationObserver(() => { el.style.setProperty("height", height, "important"); }); observer.observe(el, { attributes: true, attributeFilter: ["style"], }); // Lưu lại để debug nếu cần this._forceHeightObserver = observer; } private _clickToConversation(id: string) { // New Teams DOM: find via title element → closest treeitem const titleEl = document.getElementById(`title-chat-list-item_${id}`); const el = titleEl?.closest('[data-testid="list-item"]') ?? document.getElementById(`chat-list-item_${id}`); if (el) { el.scrollIntoView({ behavior: "smooth", block: "center" }); setTimeout(() => el.click(), 200); } return el?.click(); } private async _waitForMessagesToAppear( timeoutMs = 10000, ): Promise { return new Promise((resolve, reject) => { const chatPaneList = document.getElementById("chat-pane-list"); let timeout: any = null; if (!chatPaneList) { return reject(new Error("#chat-pane-list not found")); } const getChildren = () => Array.from(chatPaneList.children) as HTMLElement[]; const checkAndResolve = () => { const children = getChildren(); if (children.length > 0) { observer.disconnect(); if (timeout) clearTimeout(timeout); resolve(children); } }; const observer = new MutationObserver(() => { checkAndResolve(); }); observer.observe(chatPaneList, { childList: true, subtree: true, }); // Kiểm tra ngay lập tức nếu đã có sẵn item checkAndResolve(); timeout = setTimeout(() => { observer.disconnect(); reject(new Error("Timeout waiting for chat messages to appear")); }, timeoutMs); }); } // private async _waitForNewMessages( // existingItems: HTMLElement[], // timeoutMs = 10000 // ): Promise { // return new Promise((resolve, reject) => { // const chatPaneList = document.getElementById("chat-pane-list"); // if (!chatPaneList) { // return reject(new Error("#chat-pane-list not found")); // } // const getChildren = () => // Array.from(chatPaneList.children) as HTMLElement[]; // const existingSet = new Set(existingItems); // let timeoutHandle: any = null; // const observer = new MutationObserver(() => { // const currentChildren = getChildren(); // const newItems = currentChildren.filter((el) => !existingSet.has(el)); // if (newItems.length > 0) { // observer.disconnect(); // if (timeoutHandle) clearTimeout(timeoutHandle); // resolve(newItems); // } // }); // observer.observe(chatPaneList, { // childList: true, // subtree: true, // }); // timeoutHandle = setTimeout(() => { // observer.disconnect(); // reject(new Error("Timeout waiting for new messages")); // }, timeoutMs); // }); // } private async _waitToloadMessages(stableTime = 300): Promise { return new Promise((resolve) => { const chatPaneList = document.getElementById("chat-pane-list"); if (!chatPaneList) { throw new Error("#chat-pane-list not found"); } let timeout: NodeJS.Timeout | null = null; let lastChildren: HTMLElement[] = []; const observer = new MutationObserver(() => { // Reset lại mỗi khi có thay đổi if (timeout) clearTimeout(timeout); // Cập nhật danh sách item mới nhất lastChildren = Array.from(chatPaneList.children) as HTMLElement[]; timeout = setTimeout(() => { observer.disconnect(); // Khi không có thay đổi trong khoảng stableTime resolve(lastChildren); }, stableTime); }); // Quan sát cả subtree nếu có nested thay đổi observer.observe(chatPaneList, { childList: true, subtree: true, attributes: true, characterData: true, }); }); } // private _getTypeGeo() { // const el = document.querySelector(".ck-placeholder"); // if (el) { // const rect = el.getBoundingClientRect(); // return { // top: rect.top, // left: rect.left, // right: rect.right, // bottom: rect.bottom, // width: rect.width, // height: rect.height, // }; // } // return null; // } private async _rightClickMessage(date_time: number) { const selector = this.service.elTags.container_chat; const itemId = `content-${date_time}`; const maxScrollAttempts = 30; const scrollStep = 200; let attempts = 0; let wrapper = document.querySelector(selector); if (!wrapper) { console.error("Wrapper not found:", selector); await messageApi.sendLog({ type: "error", message: `Not found selector: ${selector}`, }); return; } let element = document.querySelector(`#${itemId}`); while (!element && attempts < maxScrollAttempts) { const scrollContainer = wrapper; const prevScrollTop = scrollContainer.scrollTop; scrollContainer.scrollTop = Math.max( 0, scrollContainer.scrollTop - scrollStep, ); await delay(200); // Đợi scroll và DOM render lại wrapper = document.querySelector(selector); if (!wrapper) break; element = wrapper.querySelector(`#${itemId}`); if (scrollContainer.scrollTop === prevScrollTop) break; attempts++; } if (!element) { console.log({ element }); console.error("Không tìm thấy phần tử:", itemId); return; } // Scroll phần tử vào giữa màn hình element.scrollIntoView({ behavior: "auto", block: "center" }); await delay(100); // Đợi scroll const rect = element.getBoundingClientRect(); const event = new MouseEvent("contextmenu", { bubbles: true, cancelable: true, view: window, button: 2, buttons: 2, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2, }); element.dispatchEvent(event); } private _clickIfExists(selector: string): Promise { return new Promise((resolve) => { const element = document.querySelector(selector); if (element) { element.scrollIntoView({ behavior: "auto", block: "center" }); setTimeout(() => { (element as any)?.click(); resolve(true); }, 100); // Đợi scroll một chút rồi click } else { console.warn("Không tìm thấy phần tử:", selector); resolve(false); } }); } async getConversations(_?: IMsg) { queue.add(async () => { console.log("[Queue] Handling GET_CONVERSATIONS"); const data = await this.service.handleGetConversations(); // Gửi dữ liệu ngược về background this.port.postMessage({ type: "socket-response", event: EVENTS.RECEIVE_CONVERSATIONS, data, } as IMsg); }); } async getConversation(msg: IMsg<{ id: string }>) { queue.add(async () => { console.log("[Queue] Handling GET_CONVERSATION"); if (!msg.data?.id) return; const { room_id } = this.service.getCurrentRoomInfo(); // Nếu đang active room thì không cần chờ load if (room_id != msg.data.id) { this._clickToConversation(msg.data.id); await this._waitForMessagesToAppear(); await this._waitToloadMessages(); } const data = this.service.extractAllMessages(); this.port.postMessage({ type: "socket-response", event: EVENTS.RECEIVE_CONVERSATION, data, } as IMsg); }); } async sendMessage({ data: { conversation_id, message }, }: IMsg<{ conversation_id: string; message: string }>) { queue.add(async () => { console.log("[Queue] Handling SEND_MESSAGE"); const { room_id } = this.service.getCurrentRoomInfo(); // Nếu đang active room thì không cần chờ load if (room_id != conversation_id) { this._clickToConversation(conversation_id); } await delay(200); await typeingService.send(message); // // Sroll xuống // this.service._scrollToBottomByXPath(); // const initialMessages = await this._waitForMessagesToAppear(3000); // Đợi có item đầu tiên // console.log("Initial messages:", initialMessages); // const newMessages = await this._waitForNewMessages(initialMessages, 3000); // console.log("New messages appeared:", newMessages); // await this._waitToloadMessages(); // // Lấy hết message mới nhất // const data = this.service.extractAllMessages(); // // Gửi lên server cập nhật // this.port.postMessage({ // type: "socket-response", // event: EVENTS.RECEIVE_CONVERSATION, // data, // } as IMsg); }); } async replyMessage({ data: { conversation_id, message, time }, }: IMsg<{ conversation_id: string; message: string; time: number }>) { queue.add(async () => { console.log("[Queue] Handling REPLY_MESSAGE"); let initialMessages = null; const { room_id } = this.service.getCurrentRoomInfo(); // Nếu đang active room thì không cần chờ load if (room_id != conversation_id) { this._clickToConversation(conversation_id); initialMessages = await this._waitForMessagesToAppear(); // Đợi có item đầu tiên await this._waitToloadMessages(); console.log({ initialMessages }); } await this._clickIfExists(this.service.elTags.close_reply_btn); await this._rightClickMessage(time); const replyBtn: any = document.querySelector( this.service.elTags.reply_btn, ); if (replyBtn) { replyBtn.click(); } await delay(200); console.log({ message }); await typeingService.send(message); }); } fixedHeightChatInput(retry = 20, interval = 2000) { const tryFind = () => { const el = document.querySelector( this.service.elTags.chat_input, ) as HTMLElement; if (el) { this.forceHeight(el, "100px"); console.log("✔ Fixed height applied to chat input"); } else if (retry > 0) { setTimeout( () => this.fixedHeightChatInput(retry - 1, interval), interval, ); } else { console.warn("✘ Element not found with provided XPath after retries"); messageApi.sendLog({ type: "error", message: `Not found selector: ${this.service.elTags.chat_input}`, }); } }; tryFind(); } async detectNewMessage(interval = 2000) { console.log("[Monitor] Starting..."); // Lưu lịch sử ban đầu this.service.initialHistories = this.service.extractAllMessages(); this.service.lastMessage = this.service.initialHistories.pop(); setInterval(() => { queue.add(async () => { try { const currentRoom = this.service.getCurrentRoomInfo(); // Lấy attribute aria-labelledby const ariaValue = document .querySelector( '[aria-labelledby^="cn-normal-notification-main-content-"]', ) ?.getAttribute("aria-labelledby"); // **CASE 1: Không có popup notification** if (!ariaValue) { const newCurrentMessage = await this.service.detectNewMessages(); if (!newCurrentMessage) { console.log("[Monitor] No new message..."); return; } if (currentRoom.room_id !== newCurrentMessage?.room_id) { console.log("[Monitor] Message from another room → skip"); return; } console.log("[Monitor] Found new message:", newCurrentMessage); await this._sendLastMessage(newCurrentMessage); return; } // **CASE 2: Có popup → cần click để sync** const roomId = ariaValue .split(" ")[0] .replace("cn-normal-notification-main-content-", ""); if (!roomId) { console.warn( "[Monitor] Could not extract room_id from aria-labelledby", ); return; } console.log("[Monitor] Notification detected:", { roomId, ariaValue, }); // Click vào conversation và xử lý this._clickToConversation(roomId); await delay(2000); // chờ load messages const allMessages = this.service.extractAllMessages(); const lastMessage = allMessages.at(-1); if (!lastMessage) { console.log("[Queue] No last message found after sync"); return; } console.log("[Queue] Sending last message:", lastMessage); await this._sendLastMessage(lastMessage); } catch (error) { console.error("[Monitor] Error detecting new message:", error); } }); }, interval); } // Helper gửi tin nhắn cuối cùng async _sendLastMessage(message: IMessage) { try { await messageApi.sendSingleMessage(message); console.log("[API] Sent message successfully:", message); } catch (err) { console.error("[API] Failed to send message:", err); } } filesMessages() { const messages = this.service.extractAllMessages(); const result = messages.filter((item) => { // Đảm bảo item.message là mảng if (!Array.isArray(item.message)) return false; return item.message.some((raw) => { try { // Parse JSON const msg = typeof raw === "string" ? JSON.parse(raw) : raw; return msg?.type === "file"; } catch (e) { // Nếu JSON.parse lỗi thì bỏ qua phần tử này return false; } }); }); return result; } async clickToRefreshToken(message: IMessage[]) { const lastItem = message.at(-1); const optionsButton = document .getElementById(`attachments-${lastItem?.date_time}`) ?.querySelector('[aria-label="More attachment options"]'); (optionsButton as any)?.click(); await delay(500); } startSyncConversations() { // Tạo namespace nếu chưa tồn tại (window as any)._chatIntervals = (window as any)?._chatIntervals || {}; // Kiểm tra xem interval đã tồn tại chưa if (!(window as any)._chatIntervals.syncConversations) { (window as any)._chatIntervals.syncConversations = window.setInterval( () => { this.getConversations(); }, 120000, ); console.log("✅ startSyncConversations running"); } else { console.log("ℹ️ startSyncConversations already running"); } } // Interval chạy, chỉ add task vào queue // autoSyncConversationPrefixMessages() { // (window as any)._chatIntervals = (window as any)?._chatIntervals || {}; // if (!(window as any)._chatIntervals.syncPrefixMessages) { // (window as any)._chatIntervals.syncPrefixMessages = ( // window as any // ).setInterval(() => { // (window as any)["is_sync_conversation_prefix"] = true; // queue.add(async () => { // try { // const { data } = // (await conversationApi.getConversationByPrefix()) as { // data: ChatItem[]; // }; // if (!data) return; // for (const chat of data) { // this._clickToConversation(chat.id as string); // await delay(5000); // const currentRoom = this.service.getCurrentRoomInfo(); // if (!currentRoom || currentRoom.room_id !== chat.id) return; // const filesMessages = this.filesMessages(); // if (filesMessages.length) { // await this.clickToRefreshToken(filesMessages); // } // const messages = this.service.extractAllMessages(); // await messageApi.createAndSendToZulip(messages); // await delay(5000); // } // } catch (err) { // console.error( // "❌ autoSyncConversationPrefixMessages error:", // (err as any)?.message, // ); // messageApi.sendLog({ // type: "error", // message: `auto_to_sync_conersations: ${(err as any)?.message}`, // }); // } // }); // }, 60000); // console.log("✅ autoSyncConversationPrefixMessages running with PQueue"); // } else { // console.log("ℹ️ autoSyncConversationPrefixMessages already running"); // } // } autoSyncConversationPrefixMessages() { (window as any)._chatIntervals = (window as any)._chatIntervals || {}; if ((window as any)._chatIntervals.syncPrefixMessages) { console.log("autoSyncConversationPrefixMessages already running"); return; } let isRunning = false; (window as any)._chatIntervals.syncPrefixMessages = window.setInterval( () => { if (isRunning) { console.log("syncPrefixMessages skipped (still running)"); return; } isRunning = true; queue.add(async () => { try { const { data } = (await conversationApi.getConversationByPrefix()) as { data: ChatItem[]; }; if (!data?.length) return; for (const chat of data) { this._clickToConversation(chat.id as string); await delay(5000); const currentRoom = this.service.getCurrentRoomInfo(); if (!currentRoom || currentRoom.room_id !== chat.id) { console.warn("room mismatch, skip:", chat.id); continue; } const filesMessages = this.filesMessages(); if (filesMessages.length) { await this.clickToRefreshToken(filesMessages); } const messages = this.service.extractAllMessages(); await messageApi.createAndSendToZulip(messages); await delay(5000); } } catch (err) { console.error( "autoSyncConversationPrefixMessages error:", (err as any)?.message, ); messageApi.sendLog({ type: "error", message: `auto_to_sync_conersations: ${(err as any)?.message}`, }); } finally { isRunning = false; // 🔑 cực kỳ quan trọng } }); }, 60000, ); console.log("autoSyncConversationPrefixMessages running safely"); } }