update detech new message and auto sync conversations

This commit is contained in:
Admin 2025-09-08 13:37:03 +07:00
parent b6d6a6dd79
commit 9dfa87e2c0
13 changed files with 225 additions and 35 deletions

View File

@ -1,3 +0,0 @@
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

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,9 @@ port.onMessage.addListener((msg: IMsg<any>) => {
// Set height input chat tager cho nó ra dễ click
contentService.fixedHeightChatInput();
if (msg.type !== "socket") return;
if (msg.type !== "socket") {
return;
}
switch (msg.event) {
case EVENTS.GET_CONVERSATIONS: {
@ -36,3 +38,9 @@ port.onMessage.addListener((msg: IMsg<any>) => {
}
}
});
// DETECH NEW MESSAGE (INTERVAl)
contentService.detectNewMessage();
// SYNC CONVERSASIONS (INTERVAL)
contentService.startSyncConversations();

View File

@ -1,6 +1 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

View File

@ -5,6 +5,7 @@ import { TeamsChatService } from "./teams-chat.service";
import { EVENTS } from "@/lib/event";
import { typeingService } from "./typing.service";
import { delay } from "@/features/app";
import { messageApi } from "@/api/message-api.service";
export class ContentService {
service: TeamsChatService;
@ -283,7 +284,7 @@ export class ContentService {
});
}
async getConversations(_: IMsg<any>) {
async getConversations(_?: IMsg<any>) {
queue.add(async () => {
console.log("[Queue] Handling GET_CONVERSATIONS");
@ -381,7 +382,7 @@ export class ContentService {
await this._waitToloadMessages();
}
this._clickIfExists(this.service.elTags.close_reply_btn);
await this._clickIfExists(this.service.elTags.close_reply_btn);
await this._rightClickMessage(time);
@ -443,4 +444,53 @@ export class ContentService {
tryFind();
}
async detectNewMessage(interval = 2000) {
console.log("[Monitor] Starting...");
// this.initialHistories = this.extractAllMessages();
// this.lastMessage = this.initialHistories.pop();
// await messageApi.sendBulkMessages(this.initialHistories);
setInterval(async () => {
const aria_value = document
.querySelector(
'[aria-labelledby^="cn-normal-notification-main-content-"]'
)
?.getAttribute("aria-labelledby");
if (!aria_value) {
console.log("No new message...");
return;
}
const room_id = aria_value
.split(" ")[0]
.replaceAll("cn-normal-notification-main-content-", "");
if (!room_id) return;
console.log({ room_id, aria_value });
queue.add(async () => {
console.log("[Queue] Handling SYNC NEW MESSAGE");
this._clickToConversation(room_id);
await delay(2000);
const allMessages = this.service.extractAllMessages();
const lastMessage = allMessages.at(-1);
if (!lastMessage) return;
await messageApi.sendSingleMessage(lastMessage);
});
}, interval);
}
startSyncConversations() {
setInterval(() => this.getConversations(), 20000);
}
}

View File

@ -10,18 +10,14 @@ export class TeamsChatService {
"/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]",
'[data-testid="message-wrapper"]',
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:
// "/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"]',
chat_input: '[placeholder="Type a message"]',
};
public getCurrentRoomInfo(): { room_id?: string; room_name?: string } {
@ -160,7 +156,7 @@ export class TeamsChatService {
this.initialHistories = this.extractAllMessages();
this.lastMessage = this.initialHistories.pop();
await messageApi.sendBulkMessages(this.initialHistories);
// await messageApi.sendBulkMessages(this.initialHistories);
setInterval(async () => await this.detectNewMessages(), interval);
}

View File

@ -11,6 +11,7 @@
"dependencies": {
"@faker-js/faker": "^9.9.0",
"@keyv/redis": "^5.0.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/class-transformer": "^0.4.0",
"@nestjs/common": "^11.0.1",
@ -2428,6 +2429,17 @@
"node": ">= 10"
}
},
"node_modules/@nestjs/axios": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-4.0.1.tgz",
"integrity": "sha512-68pFJgu+/AZbWkGu65Z3r55bTsCPlgyKaV4BSG8yUAD72q1PPuyVRgUwFv6BxdnibTUHlyxm06FmYWNC+bjN7A==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^10.0.0 || ^11.0.0",
"axios": "^1.3.1",
"rxjs": "^7.0.0"
}
},
"node_modules/@nestjs/cache-manager": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.1.tgz",
@ -4862,7 +4874,6 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT"
},
"node_modules/atomic-sleep": {
@ -4911,6 +4922,18 @@
"node": ">= 6.0.0"
}
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@ -5725,7 +5748,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
@ -6094,7 +6116,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.4.0"
@ -6420,7 +6441,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -7257,6 +7277,27 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -7321,7 +7362,6 @@
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true,
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
@ -7348,7 +7388,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@ -7358,7 +7397,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@ -10625,6 +10663,13 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT",
"peer": true
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -27,6 +27,7 @@
"dependencies": {
"@faker-js/faker": "^9.9.0",
"@keyv/redis": "^5.0.0",
"@nestjs/axios": "^4.0.1",
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/class-transformer": "^0.4.0",
"@nestjs/common": "^11.0.1",

View File

@ -1,5 +1,6 @@
import { IsString, IsNumber, IsOptional } from 'class-validator';
import { Expose } from '@nestjs/class-transformer';
import { Optional } from '@nestjs/common';
export class CreateMessageDto {
@IsOptional()
@ -16,6 +17,11 @@ export class CreateMessageDto {
@Expose()
time: number;
@IsNumber()
@Expose()
@Optional()
date_time: number;
@IsOptional()
@IsString()
@Expose()

View File

@ -20,6 +20,11 @@ export class MessagesController {
return this.service.sendMessage(data);
}
@Post('')
save(@Body() data: CreateMessageDto) {
return this.service.create(data);
}
@Post('reply-message')
replyMessage(@Body() data: ReplyMessageDto) {
return this.service.replyMessage(data);

View File

@ -7,14 +7,17 @@ import { MessagesController } from './messages.controller';
import { MessagesGateway } from './messages.gateway';
import { MessagesService } from './messages.service';
import { MessagesListener } from './messages.listener';
import { ZulipService } from './zulip.service';
import { HttpModule } from '@nestjs/axios';
@Module({
imports: [TypeOrmModule.forFeature([Message, Conversation])],
imports: [TypeOrmModule.forFeature([Message, Conversation]), HttpModule],
providers: [
MessagesService,
MessagesGateway,
MessagesEventService,
MessagesListener,
ZulipService,
],
controllers: [MessagesController],
exports: [MessagesGateway, MessagesService, MessagesEventService],

View File

@ -2,7 +2,12 @@ import { Conversation } from '@/entities/conversation.entity';
import { Message } from '@/entities/message.entity';
import AppResponse from '@/system/filters/response/app-response';
import { SystemLang } from '@/system/lang/system.lang';
import { HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
import {
BadRequestException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { paginate, PaginateQuery } from 'nestjs-paginate';
import { Repository } from 'typeorm';
@ -10,6 +15,7 @@ import { CreateMessageDto } from './dtos/create-message.dto';
import { ReplyMessageDto } from './dtos/reply-message.dto';
import { SendMessageDto } from './dtos/send-message.dto';
import { MessagesEventService } from './messages-event.service';
import { ZulipService } from './zulip.service';
@Injectable()
export class MessagesService {
constructor(
@ -17,7 +23,8 @@ export class MessagesService {
readonly repo: Repository<Message>,
@InjectRepository(Conversation)
readonly conversationRepo: Repository<Conversation>,
private event: MessagesEventService,
private readonly event: MessagesEventService,
private readonly zulupService: ZulipService,
) {}
async index(query: PaginateQuery) {
@ -48,16 +55,45 @@ export class MessagesService {
});
if (existing) {
return existing; // hoặc throw error nếu muốn
return existing;
}
const conversation = await this.conversationRepo.findOne({
where: { id: dto.room_id },
});
if (!conversation)
throw new BadRequestException(
AppResponse.toResponse(null, {
message: SystemLang.getText('messages', 'not_found'),
}),
);
const entity = this.repo.create({
...dto,
time,
time_raw: dto.time,
});
return this.repo.save(entity);
const result = await this.repo.save(entity);
if (result) {
if (
!conversation.name.includes(process.env.GROUP_PREFIX) ||
conversation.type !== 'group'
)
return;
if (!conversation) return;
await this.zulupService.sendMessageToTopic(
process.env.ZULIP_STREAMS_NAME,
conversation.name,
result.message,
);
}
return result;
}
async bulkCreate(dtos: CreateMessageDto[]): Promise<Message[]> {

View File

@ -0,0 +1,48 @@
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class ZulipService {
private readonly logger = new Logger(ZulipService.name);
constructor(private readonly httpService: HttpService) {}
async sendMessageToTopic(stream: string, topic: string, content: string) {
const url = process.env.ZULIP_API_URL;
const botEmail = process.env.ZULIP_BOT_EMAIL;
const apiKey = process.env.ZULIP_API_KEY;
try {
const response = await firstValueFrom(
this.httpService.post(
url,
new URLSearchParams({
type: 'stream',
to: stream,
topic: topic,
content: content,
}).toString(),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
auth: {
username: botEmail,
password: apiKey,
},
},
),
);
this.logger.log(`Message sent to stream "${stream}" topic "${topic}"`);
return response.data;
} catch (error) {
this.logger.error(
'Failed to send message to Zulip',
error?.response?.data || error.message,
);
throw error;
}
}
}