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

272 lines
8.1 KiB
TypeScript

import { messageApi } from "@/api/message-api.service";
export class TeamsChatService {
private readonly MY_NAME = "Apactech com";
private lastMessage?: IMessage;
private 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:
"/html/body/div[1]/div/div/div/div[6]/div[4]/div/div[1]/div/div[1]/div/div/div/div[1]/div/div/div[4]",
root_id: '[aria-selected="true"] [id^="chat-list-item"]',
room_name: '[data-tid="chat-title"]',
close_reply_btn:
"/html/body/div[1]/div/div/div/div[6]/div[4]/div/div[1]/div/div[3]/div/div[3]/div/div[2]/div/div[2]/div/div/card/div/div/div[2]/div/div[1]/button",
reply_btn: '[aria-label="Reply"]',
chat_input:
// "/html/body/div[1]/div/div/div/div[6]/div[4]/div/div[1]/div/div[3]/div/div[3]/div/div[2]/div/div[2]/div[1]/div",
'[placeholder="Type a message"]',
};
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 {
if (!el) return "";
// Lấy text ban đầu (nếu có)
let message = el.innerText || "";
// Nếu có ảnh gửi kèm (ảnh chia sẻ), thì ưu tiên trả về ảnh
const sharedImage = el.querySelector(
"img[data-gallery-src]"
) as HTMLImageElement | null;
if (sharedImage) {
return sharedImage.getAttribute("data-gallery-src") || "";
}
// Tìm tất cả emoji theo itemtype
const emojiImgs = Array.from(
el.querySelectorAll("img[itemtype]")
) as HTMLImageElement[];
const emojiAlts = emojiImgs
.map((img) => img.getAttribute("alt") || "")
.filter(Boolean);
// Nối emoji vào cuối chuỗi gốc (hoặc có thể chèn theo vị trí nâng cao)
if (emojiAlts.length) {
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;
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);
}
private 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();
}
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.");
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);
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();
}
}