272 lines
8.1 KiB
TypeScript
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();
|
|
}
|
|
}
|