418 lines
12 KiB
TypeScript
418 lines
12 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: '//*[@data-testid="simple-collab-rail"]',
|
|
conatainer_conversations: '//*[@data-testid="simple-collab-dnd-rail"]',
|
|
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[];
|
|
}
|
|
|
|
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 };
|
|
}
|
|
|
|
_getFileLinks(el: HTMLElement) {
|
|
const parentId = `attachments-${(el as any)?.date_time}`;
|
|
|
|
const parent = document.getElementById(parentId);
|
|
|
|
if (!parent) return [] as { name: string; url: string }[];
|
|
|
|
// Lấy tất cả con trực tiếp
|
|
const children = Array.from(parent.children);
|
|
|
|
// Lấy title, lọc link và trả về {name, url}
|
|
const results = children.flatMap((child) => {
|
|
const token = this.getAssetToken();
|
|
const title = child.getAttribute("title");
|
|
if (!title) return [];
|
|
|
|
// Giả sử format: "tên file \r\n link"
|
|
const lines = title
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
const name = lines[0] || null;
|
|
const urlMatch = lines.find((line) => /^https?:\/\//.test(line));
|
|
if (!urlMatch) return [];
|
|
|
|
return [{ name, url: urlMatch, token }];
|
|
});
|
|
|
|
return results;
|
|
}
|
|
|
|
// 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();
|
|
// }
|
|
|
|
private _getMessageByEl(el: HTMLElement | null): string[] {
|
|
if (!el) return [];
|
|
|
|
const messages: string[] = [];
|
|
|
|
// Lấy text ban đầu và loại bỏ khoảng trắng dư
|
|
const messageText = el.innerText?.trim() || "";
|
|
|
|
// Lấy danh sách ảnh (nếu có)
|
|
const sharedImages = this._getImageFormEl(el) || [];
|
|
|
|
const imageSrcs = sharedImages
|
|
.map((img) => img.getAttribute("src"))
|
|
.filter(Boolean) as string[];
|
|
|
|
if (imageSrcs.length > 0) {
|
|
// Kiểm tra overlay
|
|
const overlay = document
|
|
.getElementById(`message-body-${(el as any)?.date_time}`)
|
|
?.querySelector('[data-tid="overlay-count-text"]');
|
|
|
|
if (overlay && (overlay as HTMLElement).innerText === messageText) {
|
|
messages.push(...imageSrcs);
|
|
} else {
|
|
messages.push(...imageSrcs);
|
|
if (messageText) messages.push(messageText);
|
|
}
|
|
} else if (messageText) {
|
|
messages.push(messageText);
|
|
}
|
|
|
|
const fileLinks = this._getFileLinks(el) || [];
|
|
|
|
if (fileLinks.length) {
|
|
fileLinks.forEach((link) => {
|
|
messages.push(JSON.stringify({ ...link, type: "file" }));
|
|
});
|
|
}
|
|
|
|
// Lấy emoji
|
|
const emojiImgs = Array.from(
|
|
el.querySelectorAll("img[itemtype]"),
|
|
) as HTMLImageElement[];
|
|
const emojiAlts = emojiImgs
|
|
.map((img) => img.getAttribute("alt") || "")
|
|
.filter(Boolean);
|
|
|
|
if (emojiAlts.length > 0) {
|
|
messages.push(emojiAlts.join(""));
|
|
}
|
|
|
|
return messages;
|
|
}
|
|
|
|
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(),
|
|
};
|
|
}
|
|
|
|
getAssetToken() {
|
|
const tokenKey = Object.keys(localStorage).find((key) => {
|
|
const value = localStorage[key];
|
|
return (
|
|
value.includes('"credentialType":"AccessToken"') &&
|
|
value.includes('"target":"https://graph.microsoft.com/.default')
|
|
);
|
|
});
|
|
|
|
const accessToken = tokenKey
|
|
? JSON.parse(localStorage[tokenKey]).secret
|
|
: null;
|
|
|
|
return accessToken;
|
|
}
|
|
|
|
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 [];
|
|
}
|
|
|
|
const chatItems = Array.from(
|
|
result.querySelectorAll('[data-item-type="chat"]'),
|
|
);
|
|
|
|
const data: ChatItem[] = chatItems.map((child: Element): ChatItem => {
|
|
const treeItemValue =
|
|
child.getAttribute("data-fui-tree-item-value") || "";
|
|
const lastSegment = treeItemValue.split("/").pop() || "";
|
|
const id = lastSegment.includes("|") ? lastSegment.split("|")[1] : 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();
|
|
}
|
|
}
|