596 lines
17 KiB
TypeScript
596 lines
17 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";
|
||
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");
|
||
}
|
||
}
|
||
}
|