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

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();
}
}