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

465 lines
13 KiB
TypeScript

/* 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";
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 xpath = this.service.elTags.container_chat;
const itemId = `content-${date_time}`;
const findElementByXPath = (xpath: string): HTMLElement | null => {
const result = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
return result.singleNodeValue as HTMLElement | null;
};
const maxScrollAttempts = 30;
const scrollStep = 200;
let attempts = 0;
let wrapper = findElementByXPath(xpath);
if (!wrapper) {
console.error("Wrapper not found:", xpath);
return;
}
let element = wrapper.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 = findElementByXPath(xpath);
if (!wrapper) break;
element = wrapper.querySelector<HTMLElement>(`#${itemId}`);
if (scrollContainer.scrollTop === prevScrollTop) break;
attempts++;
}
if (!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(xpath: string): Promise<boolean> {
return new Promise((resolve) => {
const result = document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
const element = result.singleNodeValue as HTMLElement | null;
if (element) {
element.scrollIntoView({ behavior: "auto", block: "center" });
setTimeout(() => {
element.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ử:", xpath);
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();
}
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 = this.getElementByXPath(
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();
}
}