teams-bots/composer-bot-extensions/src/services/content.service.ts

596 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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) {
const el = document.getElementById(`chat-list-item_${id}`);
if (el) {
el.scrollIntoView({
behavior: "smooth", // hoặc "auto"
block: "center", // cuộn sao cho phần tử nằm giữa view
});
// Đợi 1 chút cho scroll xong (nếu cần)
setTimeout(() => el.click(), 200); // delay 200ms là đủ mượt
}
return el?.click();
}
private async _waitForMessagesToAppear(
timeoutMs = 10000
): Promise<HTMLElement[]> {
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<HTMLElement[]> {
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<any> {
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);
return;
}
let element = document.querySelector<HTMLElement>(`#${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<HTMLElement>(`#${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<boolean> {
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<any>) {
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<any>);
});
}
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<any>);
});
}
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<any>);
});
}
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();
}
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);
// if (!initialMessages) return;
// // Sroll xuống
// this.service._scrollToBottomByXPath();
// // Theo dỗi lấy detech new message
// 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<any>);
});
}
fixedHeightChatInput(retry = 20, interval = 1000) {
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");
}
};
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);
}
}
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();
},
60000
);
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(() => {
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(2000);
const currentRoom = this.service.getCurrentRoomInfo();
if (!currentRoom || currentRoom.room_id !== chat.id) return;
const messages = this.service.extractAllMessages();
await messageApi.createAndSendToZulip(messages);
await delay(5000);
}
} catch (err) {
console.error("❌ autoSyncConversationPrefixMessages error:", err);
}
});
}, 30000);
console.log("✅ autoSyncConversationPrefixMessages running with PQueue");
} else {
console.log(" autoSyncConversationPrefixMessages already running");
}
}
}