extensions
|
|
@ -0,0 +1,3 @@
|
|||
VITE_API_URL=https://notable-recently-seagull.ngrok-free.app/api/v1/
|
||||
VITE_WS_URL=wss://notable-recently-seagull.ngrok-free.app
|
||||
VITE_API_TYPE_URL=MyCoolApp
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
function rightClickElementById(id) {
|
||||
const element = document.getElementById(id);
|
||||
if (!element) {
|
||||
console.error("Element not found:", id);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = new MouseEvent("contextmenu", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
button: 2, // Chuột phải
|
||||
buttons: 2,
|
||||
clientX: element.getBoundingClientRect().left,
|
||||
clientY: element.getBoundingClientRect().top
|
||||
});
|
||||
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 522 B |
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Composer Bot",
|
||||
"version": "1.0",
|
||||
"description": "Composer Bot",
|
||||
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"16": "icons/16.png",
|
||||
"32": "icons/32.png",
|
||||
"128": "icons/128.png"
|
||||
}
|
||||
},
|
||||
|
||||
"permissions": ["storage", "tabs", "alarms"],
|
||||
|
||||
"host_permissions": ["https://teams.live.com/*"],
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://teams.live.com/*"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_idle",
|
||||
"type": "module"
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
|
||||
"icons": {
|
||||
"16": "icons/16.png",
|
||||
"32": "icons/32.png",
|
||||
"128": "icons/128.png"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,23 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "re-make-bid-extension",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"dev:build": "vite build --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.10.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"p-queue": "^8.1.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"socket.io-client": "^4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@types/chrome": "^0.1.0",
|
||||
"@types/node": "^24.0.13",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.35.1",
|
||||
"vite": "^7.0.4",
|
||||
"vite-plugin-static-copy": "^3.1.1"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 305 B |
|
After Width: | Height: | Size: 522 B |
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Composer Bot",
|
||||
"version": "1.0",
|
||||
"description": "Composer Bot",
|
||||
|
||||
"action": {
|
||||
"default_icon": {
|
||||
"16": "icons/16.png",
|
||||
"32": "icons/32.png",
|
||||
"128": "icons/128.png"
|
||||
}
|
||||
},
|
||||
|
||||
"permissions": ["storage", "tabs", "alarms"],
|
||||
|
||||
"host_permissions": ["https://teams.live.com/*"],
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://teams.live.com/*"],
|
||||
"js": ["content.js"],
|
||||
"run_at": "document_idle",
|
||||
"type": "module"
|
||||
}
|
||||
],
|
||||
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
|
||||
"icons": {
|
||||
"16": "icons/16.png",
|
||||
"32": "icons/32.png",
|
||||
"128": "icons/128.png"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,27 @@
|
|||
import axios from "@/lib/axios";
|
||||
|
||||
class MessageApiService {
|
||||
async sendSingleMessage(message: IMessage) {
|
||||
try {
|
||||
const { data } = await axios.post("/messages", message);
|
||||
console.log("[NestJS] Response (single):", data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error("[NestJS] Error (single):", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async sendBulkMessages(messages: IMessage[]) {
|
||||
try {
|
||||
const { data } = await axios.post("/messages/bulk", { data: messages });
|
||||
console.log("[NestJS] Response (bulk):", data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error("[NestJS] Error (bulk):", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const messageApi = new MessageApiService();
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { io, Socket } from "socket.io-client";
|
||||
|
||||
const EVENTS = {
|
||||
GET_CONVERSATIONS: "messages.get-conversations",
|
||||
GET_CONVERSATION: "messages.get-conversation",
|
||||
RECEIVE_CONVERSATIONS: "messages.receive-conversations",
|
||||
RECEIVE_CONVERSATION: "messages.receive-conversation",
|
||||
SEND_MESSAGE: "messages.send-messsage",
|
||||
REPLY_MESSAGE: "messages.reply-messsage",
|
||||
};
|
||||
|
||||
let socket: Socket | null = null;
|
||||
let ports: chrome.runtime.Port[] = [];
|
||||
|
||||
function initSocket() {
|
||||
if (!socket) {
|
||||
socket = io(`${import.meta.env.VITE_WS_URL}`, {
|
||||
transports: ["websocket"],
|
||||
// path: "/socket.io", // đảm bảo path đúng
|
||||
});
|
||||
|
||||
// Kết nối port từ content script
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === "message") {
|
||||
ports.push(port);
|
||||
port.postMessage({ type: "status", msg: "Connected to background" });
|
||||
|
||||
port.onDisconnect.addListener(() => {
|
||||
ports = ports.filter((p) => p !== port);
|
||||
});
|
||||
}
|
||||
|
||||
port.onMessage.addListener((msg: IMsg<any>) => {
|
||||
console.log(`[${msg.event}] Received from content:`, msg);
|
||||
|
||||
if (msg.type !== "socket-response") return;
|
||||
|
||||
socket?.emit(msg.event, msg?.data);
|
||||
});
|
||||
});
|
||||
|
||||
// Socket.IO events
|
||||
socket.on("connect", () => {
|
||||
console.log("✅ Socket.IO connected");
|
||||
broadcastToPorts({
|
||||
type: "socket",
|
||||
event: "connect",
|
||||
msg: "Socket.IO connected",
|
||||
});
|
||||
});
|
||||
|
||||
// Listent events
|
||||
const eventsToListen = [
|
||||
EVENTS.GET_CONVERSATIONS,
|
||||
EVENTS.GET_CONVERSATION,
|
||||
EVENTS.SEND_MESSAGE,
|
||||
EVENTS.REPLY_MESSAGE,
|
||||
];
|
||||
|
||||
eventsToListen.forEach((event) => {
|
||||
socket?.on(event, (data: any) => {
|
||||
broadcastToPorts({
|
||||
type: "socket",
|
||||
event,
|
||||
data,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastToPorts(message: any) {
|
||||
ports.forEach((port) => {
|
||||
try {
|
||||
port.postMessage(message);
|
||||
} catch (err) {
|
||||
console.warn("❌ Failed to send message:", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initSocket();
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { EVENTS } from "./lib/event";
|
||||
import { ContentService } from "./services/content.service";
|
||||
|
||||
const port = chrome.runtime.connect({ name: "message" });
|
||||
|
||||
const contentService = new ContentService(port);
|
||||
|
||||
console.log({ a: import.meta.env.VITE_API_URL });
|
||||
|
||||
port.onMessage.addListener((msg: IMsg<any>) => {
|
||||
console.log({ msg });
|
||||
|
||||
if (msg.type !== "socket") return;
|
||||
|
||||
switch (msg.event) {
|
||||
case EVENTS.GET_CONVERSATIONS: {
|
||||
contentService.getConversations(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case EVENTS.GET_CONVERSATION: {
|
||||
contentService.getConversation(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case EVENTS.SEND_MESSAGE: {
|
||||
contentService.sendMessage(msg);
|
||||
break;
|
||||
}
|
||||
|
||||
case EVENTS.REPLY_MESSAGE: {
|
||||
contentService.replyMessage(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
export const EXTENTION_ROOT_ID = "bid-extensions";
|
||||
|
||||
export function extractModelId(url: string) {
|
||||
switch (extractDomain(url)) {
|
||||
case webs.grays: {
|
||||
const match = url.match(/\/lot\/([\d-]+)\//);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
case webs.langtons: {
|
||||
const match = url.match(/auc-var-\d+/);
|
||||
return match?.[0] || null;
|
||||
}
|
||||
case webs.lawsons: {
|
||||
const match = url.split("_");
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
case webs.pickles: {
|
||||
const model = url.split("/").pop();
|
||||
return model ? model : null;
|
||||
}
|
||||
case webs.allbids: {
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const match = url.match(/-(\d+)(?:[\?#]|$)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function extractDomain(url: string) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
return parsedUrl.origin;
|
||||
} catch (error: any) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const webs = {
|
||||
grays: "https://www.grays.com",
|
||||
langtons: "https://www.langtons.com.au",
|
||||
lawsons: "https://www.lawsons.com.au",
|
||||
pickles: "https://www.pickles.com.au",
|
||||
allbids: "https://www.allbids.com.au",
|
||||
};
|
||||
|
||||
export const getMode = (data: { metadata: any[] }) => {
|
||||
return (
|
||||
data.metadata.find((item) => item.key_name === "mode_key")?.value || "live"
|
||||
);
|
||||
};
|
||||
|
||||
export const getEarlyTrackingSeconds = (
|
||||
data: { metadata: any; web_bid?: any },
|
||||
outsiteMode = null
|
||||
) => {
|
||||
const mode = outsiteMode ? outsiteMode : getMode(data);
|
||||
|
||||
return (
|
||||
data.metadata.find(
|
||||
(item: { key_name: string }) =>
|
||||
item.key_name === `early_tracking_seconds_${mode}`
|
||||
)?.value || data.web_bid.early_tracking_seconds
|
||||
);
|
||||
};
|
||||
|
||||
export const getArrivalOffsetSeconds = (
|
||||
data: { metadata: any; web_bid?: any },
|
||||
outsiteMode = null
|
||||
) => {
|
||||
const mode = outsiteMode ? outsiteMode : getMode(data);
|
||||
|
||||
return (
|
||||
data.metadata.find(
|
||||
(item: { key_name: string }) =>
|
||||
item.key_name === `arrival_offset_seconds_${mode}`
|
||||
)?.value || data.web_bid.arrival_offset_seconds
|
||||
);
|
||||
};
|
||||
|
||||
export function removeFalsyValues<T extends Record<string, any>>(
|
||||
obj: T,
|
||||
excludeKeys: (keyof T)[] = []
|
||||
): Partial<T> {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (value || excludeKeys.includes(key as keyof T)) {
|
||||
acc[key as keyof T] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Partial<T>);
|
||||
}
|
||||
|
||||
export function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export function getSecondsFromNow(datetime: {
|
||||
date: Date | undefined;
|
||||
time: string;
|
||||
}): number | null {
|
||||
if (!datetime.date) {
|
||||
return null; // Trả về null nếu date không được chọn
|
||||
}
|
||||
|
||||
// Tách giờ, phút, giây từ time string (HH:mm:ss)
|
||||
const [hours, minutes, seconds] = datetime.time.split(":").map(Number);
|
||||
|
||||
// Tạo Date object mới từ date và time
|
||||
const targetDate = new Date(datetime.date);
|
||||
targetDate.setHours(hours, minutes, seconds, 0);
|
||||
|
||||
// Lấy thời điểm hiện tại
|
||||
const now = new Date();
|
||||
|
||||
// Tính khoảng cách thời gian (mili giây)
|
||||
const diffInMs = targetDate.getTime() - now.getTime();
|
||||
|
||||
// Chuyển sang giây (làm tròn xuống)
|
||||
const diffInSeconds = Math.floor(diffInMs / 1000);
|
||||
|
||||
return diffInSeconds;
|
||||
}
|
||||
|
||||
export function formatTimeFromMinutes(minutes: number): string {
|
||||
// Tính ngày, giờ, phút từ số phút
|
||||
const days = Math.floor(minutes / (60 * 24));
|
||||
const hours = Math.floor((minutes % (60 * 24)) / 60);
|
||||
const mins = minutes % 60;
|
||||
|
||||
let result = "";
|
||||
|
||||
if (days > 0) result += `${days} ${days > 1 ? "days" : "day"} `;
|
||||
if (hours > 0) result += `${hours} ${hours > 1 ? "hours" : "hour"} `;
|
||||
if (mins > 0 || result === "") result += `${mins} minutes`;
|
||||
|
||||
return result.trim();
|
||||
}
|
||||
|
||||
export function getDatetimeFromSeconds(seconds: number): {
|
||||
date: Date | undefined;
|
||||
time: string;
|
||||
} {
|
||||
// Lấy thời điểm hiện tại
|
||||
const now = new Date();
|
||||
|
||||
// Tính thời điểm tương lai bằng cách cộng số giây vào hiện tại
|
||||
const targetDate = new Date(now.getTime() + seconds * 1000);
|
||||
|
||||
// Lấy giờ, phút, giây và định dạng thành chuỗi "HH:mm:ss"
|
||||
const hours = String(targetDate.getHours()).padStart(2, "0");
|
||||
const minutes = String(targetDate.getMinutes()).padStart(2, "0");
|
||||
const secondsStr = String(targetDate.getSeconds()).padStart(2, "0");
|
||||
const time = `${hours}:${minutes}:${secondsStr}`;
|
||||
|
||||
return {
|
||||
date: targetDate,
|
||||
time,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const saveKey = (payload: any) => {
|
||||
window.postMessage(
|
||||
{
|
||||
direction: "to-content",
|
||||
type: "SAVE_KEY",
|
||||
payload: payload,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
||||
export const getKey = () => {
|
||||
window.postMessage(
|
||||
{
|
||||
direction: "to-content",
|
||||
type: "GET_KEY",
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const saveStateLogin = (payload: any) => {
|
||||
window.postMessage(
|
||||
{
|
||||
direction: "to-content",
|
||||
type: "SAVE_STATE_LOGIN",
|
||||
payload: payload,
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
||||
export const getStateLogin = () => {
|
||||
window.postMessage(
|
||||
{
|
||||
direction: "to-content",
|
||||
type: "GET_STATE_LOGIN",
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// import { TeamsChatService } from "./services/teams-chat.service";
|
||||
|
||||
// const service = new TeamsChatService();
|
||||
// service.start(); // Mỗi 10 giây sẽ quét tin nhắn mới
|
||||
|
||||
// setTimeout(async () => {
|
||||
// await service.scrollToBottomByXPath.bind(service)();
|
||||
// await service.getConversationsInfo.bind(service)();
|
||||
// }, 5000);
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
interface IMessage {
|
||||
name?: string;
|
||||
message?: string;
|
||||
time: number;
|
||||
room_id?: string;
|
||||
room_name?: string;
|
||||
|
||||
date_time?: number;
|
||||
}
|
||||
|
||||
interface IInputGeo {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
type ChatItemType = "group" | "personal" | null;
|
||||
|
||||
interface ChatItem {
|
||||
id: string | null;
|
||||
name: string | null;
|
||||
type: ChatItemType;
|
||||
}
|
||||
|
||||
interface IMsg<T> {
|
||||
type: string;
|
||||
event: string;
|
||||
data: T;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import ax from "axios";
|
||||
|
||||
const axios = ax.create({
|
||||
// Dev
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
export default axios;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export const EVENTS = {
|
||||
GET_CONVERSATIONS: "messages.get-conversations",
|
||||
GET_CONVERSATION: "messages.get-conversation",
|
||||
RECEIVE_CONVERSATIONS: "messages.receive-conversations",
|
||||
RECEIVE_CONVERSATION: "messages.receive-conversation",
|
||||
SEND_MESSAGE: "messages.send-messsage",
|
||||
REPLY_MESSAGE: "messages.reply-messsage",
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import PQueue from "p-queue";
|
||||
// Khởi tạo queue xử lý tuần tự
|
||||
export const queue = new PQueue({ concurrency: 1 });
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -0,0 +1,273 @@
|
|||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { queue } from "@/lib/queue";
|
||||
import { TeamsChatService } from "./teams-chat.service";
|
||||
import { EVENTS } from "@/lib/event";
|
||||
import { typeingService } from "./typing.service";
|
||||
import { delay } from "@/features/app";
|
||||
|
||||
export class ContentService {
|
||||
service: TeamsChatService;
|
||||
port;
|
||||
constructor(port: any) {
|
||||
this.service = new TeamsChatService();
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
private _clickToConversation(id: string) {
|
||||
const el = document.getElementById(`chat-list-item_${id}`);
|
||||
|
||||
if (el) {
|
||||
el.scrollIntoView({
|
||||
behavior: "smooth", // hoặc "auto"
|
||||
block: "center", // cuộn sao cho phần tử nằm giữa view
|
||||
});
|
||||
|
||||
// Đợi 1 chút cho scroll xong (nếu cần)
|
||||
setTimeout(() => el.click(), 200); // delay 200ms là đủ mượt
|
||||
}
|
||||
return el?.click();
|
||||
}
|
||||
|
||||
private async _waitToloadMessages(stableTime = 300): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
const chatPaneList = document.getElementById("chat-pane-list");
|
||||
|
||||
if (!chatPaneList) {
|
||||
throw new Error("#chat-pane-list not found");
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
let lastChildren: HTMLElement[] = [];
|
||||
|
||||
const observer = new MutationObserver(() => {
|
||||
// Reset lại mỗi khi có thay đổi
|
||||
if (timeout) clearTimeout(timeout);
|
||||
|
||||
// Cập nhật danh sách item mới nhất
|
||||
lastChildren = Array.from(chatPaneList.children) as HTMLElement[];
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
observer.disconnect(); // Khi không có thay đổi trong khoảng stableTime
|
||||
resolve(lastChildren);
|
||||
}, stableTime);
|
||||
});
|
||||
|
||||
// Quan sát cả subtree nếu có nested thay đổi
|
||||
observer.observe(chatPaneList, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
characterData: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _getTypeGeo() {
|
||||
const el = document.querySelector(".ck-placeholder");
|
||||
|
||||
if (el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
top: rect.top,
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async _rightClickMessage(date_time: number) {
|
||||
const xpath = this.service.elTags.container_chat;
|
||||
const itemId = `content-${date_time}`;
|
||||
|
||||
const findElementByXPath = (xpath: string): HTMLElement | null => {
|
||||
const result = document.evaluate(
|
||||
xpath,
|
||||
document,
|
||||
null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||
null
|
||||
);
|
||||
return result.singleNodeValue as HTMLElement | null;
|
||||
};
|
||||
|
||||
const maxScrollAttempts = 30;
|
||||
const scrollStep = 200;
|
||||
let attempts = 0;
|
||||
|
||||
let wrapper = findElementByXPath(xpath);
|
||||
if (!wrapper) {
|
||||
console.error("Wrapper not found:", xpath);
|
||||
return;
|
||||
}
|
||||
|
||||
let element = wrapper.querySelector<HTMLElement>(`#${itemId}`);
|
||||
|
||||
while (!element && attempts < maxScrollAttempts) {
|
||||
const scrollContainer = wrapper;
|
||||
const prevScrollTop = scrollContainer.scrollTop;
|
||||
|
||||
scrollContainer.scrollTop = Math.max(
|
||||
0,
|
||||
scrollContainer.scrollTop - scrollStep
|
||||
);
|
||||
await delay(200); // Đợi scroll và DOM render lại
|
||||
|
||||
wrapper = findElementByXPath(xpath);
|
||||
if (!wrapper) break;
|
||||
|
||||
element = wrapper.querySelector<HTMLElement>(`#${itemId}`);
|
||||
|
||||
if (scrollContainer.scrollTop === prevScrollTop) break;
|
||||
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
console.error("Không tìm thấy phần tử:", itemId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Scroll phần tử vào giữa màn hình
|
||||
element.scrollIntoView({ behavior: "auto", block: "center" });
|
||||
await delay(100); // Đợi scroll
|
||||
|
||||
const rect = element.getBoundingClientRect();
|
||||
const event = new MouseEvent("contextmenu", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
button: 2,
|
||||
buttons: 2,
|
||||
clientX: rect.left + rect.width / 2,
|
||||
clientY: rect.top + rect.height / 2,
|
||||
});
|
||||
|
||||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
private _clickIfExists(xpath: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const result = document.evaluate(
|
||||
xpath,
|
||||
document,
|
||||
null,
|
||||
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||
null
|
||||
);
|
||||
|
||||
const element = result.singleNodeValue as HTMLElement | null;
|
||||
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "auto", block: "center" });
|
||||
setTimeout(() => {
|
||||
element.click();
|
||||
resolve(true);
|
||||
}, 100); // Đợi scroll một chút rồi click
|
||||
} else {
|
||||
console.warn("Không tìm thấy phần tử:", xpath);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getConversations(_: IMsg<any>) {
|
||||
queue.add(async () => {
|
||||
console.log("[Queue] Handling GET_CONVERSATIONS");
|
||||
|
||||
const data = await this.service.handleGetConversations();
|
||||
|
||||
// Gửi dữ liệu ngược về background
|
||||
this.port.postMessage({
|
||||
type: "socket-response",
|
||||
event: EVENTS.RECEIVE_CONVERSATIONS,
|
||||
data,
|
||||
} as IMsg<any>);
|
||||
});
|
||||
}
|
||||
|
||||
async getConversation(msg: IMsg<{ id: string }>) {
|
||||
queue.add(async () => {
|
||||
console.log("[Queue] Handling GET_CONVERSATION");
|
||||
|
||||
if (!msg.data?.id) return;
|
||||
|
||||
const { room_id } = this.service.getCurrentRoomInfo();
|
||||
|
||||
// Nếu đang active room thì không cần chờ load
|
||||
if (room_id != msg.data.id) {
|
||||
this._clickToConversation(msg.data.id);
|
||||
|
||||
await this._waitToloadMessages();
|
||||
}
|
||||
|
||||
const data = this.service.extractAllMessages();
|
||||
|
||||
this.port.postMessage({
|
||||
type: "socket-response",
|
||||
event: EVENTS.RECEIVE_CONVERSATION,
|
||||
data,
|
||||
} as IMsg<any>);
|
||||
});
|
||||
}
|
||||
|
||||
async sendMessage({
|
||||
data: { conversation_id, message },
|
||||
}: IMsg<{ conversation_id: string; message: string }>) {
|
||||
queue.add(async () => {
|
||||
console.log("[Queue] Handling SEND_MESSAGE");
|
||||
|
||||
const { room_id } = this.service.getCurrentRoomInfo();
|
||||
|
||||
// Nếu đang active room thì không cần chờ load
|
||||
if (room_id != conversation_id) {
|
||||
this._clickToConversation(conversation_id);
|
||||
}
|
||||
|
||||
await delay(200);
|
||||
|
||||
await typeingService.send(message);
|
||||
});
|
||||
}
|
||||
|
||||
async replyMessage({
|
||||
data: { conversation_id, message, time },
|
||||
}: IMsg<{ conversation_id: string; message: string; time: number }>) {
|
||||
queue.add(async () => {
|
||||
console.log("[Queue] Handling REPLY_MESSAGE");
|
||||
|
||||
const { room_id } = this.service.getCurrentRoomInfo();
|
||||
|
||||
// Nếu đang active room thì không cần chờ load
|
||||
if (room_id != conversation_id) {
|
||||
this._clickToConversation(conversation_id);
|
||||
|
||||
await this._waitToloadMessages();
|
||||
}
|
||||
|
||||
this._clickIfExists(this.service.elTags.close_reply_btn);
|
||||
|
||||
await this._rightClickMessage(time);
|
||||
|
||||
const replyBtn: any = document.querySelector(
|
||||
this.service.elTags.reply_btn
|
||||
);
|
||||
|
||||
if (replyBtn) {
|
||||
replyBtn.click();
|
||||
}
|
||||
|
||||
await delay(200);
|
||||
|
||||
console.log({ message });
|
||||
|
||||
await typeingService.send(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,268 @@
|
|||
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"]',
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import axios, { type AxiosInstance } from "axios";
|
||||
|
||||
class TypingService {
|
||||
axios: AxiosInstance | null = null;
|
||||
|
||||
constructor() {
|
||||
this.axios = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_TYPE_URL,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async send(
|
||||
message: string
|
||||
): Promise<{ status: boolean; typed: string } | null> {
|
||||
if (!this.axios) return null;
|
||||
|
||||
const { data } = await this.axios({
|
||||
method: "POST",
|
||||
url: "type",
|
||||
data: {
|
||||
message,
|
||||
},
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
export const typeingService = new TypingService();
|
||||
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "es2020",
|
||||
"module": "es2020",
|
||||
"moduleResolution": "bundler", // hoặc "node" nếu bạn dùng tsc không bundler
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: "composer-bot-extensions",
|
||||
cssCodeSplit: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
content: "src/content.ts",
|
||||
background: "src/background.ts",
|
||||
},
|
||||
output: {
|
||||
entryFileNames: "[name].js",
|
||||
},
|
||||
},
|
||||
emptyOutDir: false,
|
||||
},
|
||||
});
|
||||