270 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			270 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
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"]',
 | 
						|
  };
 | 
						|
 | 
						|
  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);
 | 
						|
  }
 | 
						|
 | 
						|
  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.");
 | 
						|
      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();
 | 
						|
  }
 | 
						|
}
 |