325 lines
9.4 KiB
TypeScript
325 lines
9.4 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
import { messageApi } from "@/api/message-api.service";
|
|
|
|
export class TeamsChatService {
|
|
private readonly MY_NAME = "Apactech com";
|
|
public lastMessage?: IMessage;
|
|
public 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: '[data-testid="message-wrapper"]',
|
|
|
|
root_id: '[aria-selected="true"] [id^="chat-list-item"]',
|
|
room_name: '[data-tid="chat-title"]',
|
|
close_reply_btn:
|
|
'[data-track-action-scenario="messageQuotedReplyDismissed"]',
|
|
reply_btn: '[aria-label="Reply"]',
|
|
chat_input: '[placeholder="Type a message"]',
|
|
};
|
|
|
|
private _getImageFormEl(el: HTMLElement): HTMLImageElement[] {
|
|
// Tìm tất cả img có data-gallery-src trong el
|
|
let sharedImages = Array.from(
|
|
el.querySelectorAll("img[data-gallery-src]")
|
|
) as HTMLImageElement[];
|
|
|
|
// Nếu không tìm thấy thì thử tìm trong parentElement
|
|
if (sharedImages.length === 0 && el.parentElement) {
|
|
sharedImages = Array.from(
|
|
el.parentElement.querySelectorAll("img[data-gallery-src]")
|
|
) as HTMLImageElement[];
|
|
}
|
|
|
|
console.log({
|
|
el,
|
|
data_time: (el as any).date_time,
|
|
text: (el.querySelector('[data-tid="overlay-count-text"]') as any)
|
|
?.innerText,
|
|
});
|
|
|
|
return sharedImages; // Luôn trả về array (có thể rỗng)
|
|
}
|
|
|
|
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 | string[] {
|
|
if (!el) return "";
|
|
|
|
// Lấy text ban đầu và loại bỏ khoảng trắng dư
|
|
let message = el.innerText?.trim() || "";
|
|
|
|
// Lấy danh sách ảnh (nếu có)
|
|
const sharedImages = this._getImageFormEl(el) || [];
|
|
|
|
if (sharedImages.length > 0) {
|
|
const arrMessage = sharedImages.map(
|
|
(img) => img.getAttribute("src") || ""
|
|
);
|
|
|
|
if (message.length > 0) {
|
|
// Kiểm tra overlay
|
|
const overlay = document
|
|
.getElementById(`message-body-${(el as any)?.date_time}`)
|
|
?.querySelector('[data-tid="overlay-count-text"]');
|
|
|
|
// Nếu overlay text === message gốc → chỉ trả về ảnh
|
|
if (overlay && (overlay as HTMLElement).innerText === message) {
|
|
return arrMessage;
|
|
}
|
|
|
|
// Nếu không, thêm message vào cuối
|
|
arrMessage.push(message);
|
|
}
|
|
|
|
return arrMessage;
|
|
}
|
|
|
|
// Tìm tất cả emoji trong element
|
|
const emojiImgs = Array.from(
|
|
el.querySelectorAll("img[itemtype]")
|
|
) as HTMLImageElement[];
|
|
|
|
const emojiAlts = emojiImgs
|
|
.map((img) => img.getAttribute("alt") || "")
|
|
.filter(Boolean);
|
|
|
|
// Nếu có emoji thì nối vào message
|
|
if (emojiAlts.length > 0) {
|
|
message = `${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;
|
|
|
|
(contentEl as any)["date_time"] = dateTime;
|
|
|
|
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);
|
|
}
|
|
|
|
public 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();
|
|
|
|
return newMsg;
|
|
}
|
|
|
|
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<ChatItem[]> {
|
|
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.");
|
|
messageApi.sendLog({
|
|
type: "error",
|
|
message: `Not found selector: ${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<void> {
|
|
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);
|
|
messageApi.sendLog({
|
|
type: "error",
|
|
message: `Not found selector: ${xpath}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
return new Promise<void>((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();
|
|
}
|
|
}
|