update view message

This commit is contained in:
Admin 2025-09-16 10:03:12 +07:00
parent ebd81e11a2
commit 14747053d2
14 changed files with 3169 additions and 80 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -44,3 +44,11 @@ contentService.startSyncConversations();
// AUTO SYNC MESAGE PREFIX (INTERNAL)
contentService.autoSyncConversationPrefixMessages();
// setTimeout(() => {
// const a = new TeamsChatService();
// const message = a.extractAllMessages();
// console.log({ message });
// }, 10000);

View File

@ -53,53 +53,137 @@ export class TeamsChatService {
return { room_id: roomId, room_name: roomName };
}
private _getMessageByEl(el: HTMLElement | null): string | string[] {
if (!el) return "";
_getFileLinks(el: HTMLElement) {
const parentId = `attachments-${(el as any)?.date_time}`;
const parent = document.getElementById(parentId);
if (!parent) return [] as { name: string; url: string }[];
// Lấy tất cả con trực tiếp
const children = Array.from(parent.children);
// Lấy title, lọc link và trả về {name, url}
const results = children.flatMap((child) => {
const title = child.getAttribute("title");
if (!title) return [];
// Giả sử format: "tên file \r\n link"
const lines = title
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const name = lines[0] || null;
const urlMatch = lines.find((line) => /^https?:\/\//.test(line));
if (!urlMatch) return [];
return [{ name, url: urlMatch }];
});
return results;
}
// private _getMessageByEl(el: HTMLElement | null): string | string[] {
// if (!el) return "";
// // Lấy text ban đầu và loại bỏ khoảng trắng dư
// let message = el.innerText?.trim() || "";
// // Lấy danh sách ảnh (nếu có)
// const sharedImages = this._getImageFormEl(el) || [];
// if (sharedImages.length > 0) {
// const arrMessage = sharedImages.map(
// (img) => img.getAttribute("src") || ""
// );
// if (message.length > 0) {
// // Kiểm tra overlay
// const overlay = document
// .getElementById(`message-body-${(el as any)?.date_time}`)
// ?.querySelector('[data-tid="overlay-count-text"]');
// // Nếu overlay text === message gốc → chỉ trả về ảnh
// if (overlay && (overlay as HTMLElement).innerText === message) {
// return arrMessage;
// }
// // Nếu không, thêm message vào cuối
// arrMessage.push(message);
// }
// return arrMessage;
// }
// // Tìm tất cả emoji trong element
// const emojiImgs = Array.from(
// el.querySelectorAll("img[itemtype]")
// ) as HTMLImageElement[];
// const emojiAlts = emojiImgs
// .map((img) => img.getAttribute("alt") || "")
// .filter(Boolean);
// // Nếu có emoji thì nối vào message
// if (emojiAlts.length > 0) {
// message = `${message}${emojiAlts.join("")}`;
// }
// return message.trim();
// }
private _getMessageByEl(el: HTMLElement | null): string[] {
if (!el) return [];
const messages: string[] = [];
// Lấy text ban đầu và loại bỏ khoảng trắng dư
let message = el.innerText?.trim() || "";
const messageText = el.innerText?.trim() || "";
// Lấy danh sách ảnh (nếu có)
const sharedImages = this._getImageFormEl(el) || [];
if (sharedImages.length > 0) {
const arrMessage = sharedImages.map(
(img) => img.getAttribute("src") || ""
);
const imageSrcs = sharedImages
.map((img) => img.getAttribute("src"))
.filter(Boolean) as string[];
if (message.length > 0) {
// Kiểm tra overlay
const overlay = document
.getElementById(`message-body-${(el as any)?.date_time}`)
?.querySelector('[data-tid="overlay-count-text"]');
if (imageSrcs.length > 0) {
// Kiểm tra overlay
const overlay = document
.getElementById(`message-body-${(el as any)?.date_time}`)
?.querySelector('[data-tid="overlay-count-text"]');
// Nếu overlay text === message gốc → chỉ trả về ảnh
if (overlay && (overlay as HTMLElement).innerText === message) {
return arrMessage;
}
// Nếu không, thêm message vào cuối
arrMessage.push(message);
if (overlay && (overlay as HTMLElement).innerText === messageText) {
messages.push(...imageSrcs);
} else {
messages.push(...imageSrcs);
if (messageText) messages.push(messageText);
}
return arrMessage;
} else if (messageText) {
messages.push(messageText);
}
// Tìm tất cả emoji trong element
const fileLinks = this._getFileLinks(el) || [];
if (fileLinks.length) {
fileLinks.forEach((link) => {
messages.push(JSON.stringify({ ...link, type: "file" }));
});
}
// Lấy emoji
const emojiImgs = Array.from(
el.querySelectorAll("img[itemtype]")
) as HTMLImageElement[];
const emojiAlts = emojiImgs
.map((img) => img.getAttribute("alt") || "")
.filter(Boolean);
// Nếu có emoji thì nối vào message
if (emojiAlts.length > 0) {
message = `${message}${emojiAlts.join("")}`;
messages.push(emojiAlts.join(""));
}
return message.trim();
return messages;
}
public parseMessageElement(el: Element, isMine = false): IMessage | null {
@ -156,11 +240,11 @@ export class TeamsChatService {
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 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();

2929
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@
"dependencies": {
"@faker-js/faker": "^9.9.0",
"@keyv/redis": "^5.0.0",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/axios": "^4.0.1",
"@nestjs/cache-manager": "^3.0.1",
"@nestjs/class-transformer": "^0.4.0",
@ -56,6 +57,7 @@
"multer": "^2.0.2",
"mysql2": "^3.14.3",
"nestjs-paginate": "^12.5.0",
"nodemailer": "^7.0.6",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"socket.io": "^4.8.1"

View File

@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { MessagesModule } from './modules/messages/messages.module';
import { AppConfigsModule } from './modules/app-configs/app-configs.module';
import { ConversationsModule } from './modules/conversations/conversations.module';
import { MailerModule } from './modules/mailer/mailer.module';
@Module({
imports: [MessagesModule, AppConfigsModule, ConversationsModule],
imports: [MessagesModule, AppConfigsModule, ConversationsModule, MailerModule],
controllers: [],
providers: [],
})

View File

@ -0,0 +1,32 @@
import { MailerModule as MLM } from '@nestjs-modules/mailer';
import { Module } from '@nestjs/common';
import { MailerService } from './mailer.service';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
MLM.forRootAsync({
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
transport: {
host: config.get<string>('MAIL_HOST'),
port: config.get<string>('MAIL_PORT'),
// secure: true, // true nếu port là 465
auth: {
user: config.get<string>('MAIL_USERNAME'),
pass: config.get<string>('MAIL_PASSWORD'),
},
},
defaults: {
from: `"Bids" <${config.get<string>('MAIL_FROM')}>`,
},
}),
inject: [ConfigService],
}),
],
providers: [MailerService],
exports: [MailerService],
})
export class MailerModule {}

View File

@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common';
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
@Injectable()
export class MailerService {
constructor(private readonly mailerService: NestMailerService) {}
/**
* Lấy danh sách email từ biến môi trường MAILS
*/
private getMailList(): string[] {
const mailList = (process.env.MAILS || '')
.split(',')
.map((email) => email.trim())
.filter(Boolean); // loại bỏ email rỗng
if (mailList.length === 0) {
throw new Error('No valid email addresses found in MAILS env');
}
return mailList;
}
/**
* Gửi email đến tất cả người nhận trong MAILS env
*/
async sendMail(subject: string, htmlContent: string) {
const mailList = this.getMailList(); // lấy danh sách email từ hàm
await this.mailerService.sendMail({
to: mailList,
subject,
html: htmlContent,
});
}
}

View File

@ -6,13 +6,14 @@ import { SendLogDto } from './dtos/send-log.dto';
import { ZulipService } from './zulip.service';
import AppResponse from '@/system/filters/response/app-response';
import { SystemLang } from '@/system/lang/system.lang';
import { MailerService } from '../mailer/mailer.service';
@Injectable()
export class LogsService {
constructor(
@InjectRepository(Log)
readonly repo: Repository<Log>,
private readonly zulipService: ZulipService,
private readonly malerService: MailerService,
) {}
async saveLog(data: SendLogDto) {
@ -28,11 +29,17 @@ export class LogsService {
}),
);
await this.zulipService.sendMessageToTopic(
process.env.ZULIP_STREAMS_NAME,
process.env.ZULIP_TOPPIC_LOG_NAME,
`[${result.type.toUpperCase()}] - ${result.message}`,
);
// 2. Build nội dung email
const subject = `[${result.type.toUpperCase()}] - New Log Message`;
const content = `
<h3>Log Notification</h3>
<p><b>Type:</b> ${result.type.toUpperCase()}</p>
<p><b>Message:</b> ${result.message}</p>
<p><b>Created At:</b> ${new Date().toLocaleString()}</p>
`;
// 3. Gửi email
await this.malerService.sendMail(subject, content);
return AppResponse.toResponse(result);
}

View File

@ -11,9 +11,14 @@ import { ZulipService } from './zulip.service';
import { HttpModule } from '@nestjs/axios';
import { Log } from '@/entities/log.entity';
import { LogsService } from './logs.service';
import { MailerModule } from '../mailer/mailer.module';
@Module({
imports: [TypeOrmModule.forFeature([Message, Conversation, Log]), HttpModule],
imports: [
TypeOrmModule.forFeature([Message, Conversation, Log]),
HttpModule,
MailerModule,
],
providers: [
MessagesService,
MessagesGateway,

View File

@ -190,32 +190,83 @@ export class MessagesService {
}
// 6. Build message để gửi lên Zulip
// const buildZulipMessageContent = (
// msgs: string[],
// result: Message,
// ): string => {
// const imageUris: string[] = [];
// const textMessages: string[] = [];
// for (const msg of msgs) {
// // Nếu là link `/user_uploads/...` thì render ảnh
// if (/\/user_uploads\//.test(msg)) {
// imageUris.push(`[image](${msg.replace(/^\/api\/v1/, '')})`);
// } else {
// textMessages.push(formatTextIfValid(msg));
// }
// }
// let finalContent = `** :rocket: ${result.name} sent - ${formatTimeAU(result.time_raw)}:**\n`;
// // Nếu có text → thêm block code
// if (textMessages.length > 0) {
// finalContent += `\`\`\`\n${textMessages.join('\n')}\n\`\`\`\n`;
// }
// // Nếu có ảnh → thêm danh sách ảnh ở cuối
// if (imageUris.length > 0) {
// finalContent += imageUris.join('\n');
// }
// return finalContent.trim();
// };
const buildZulipMessageContent = (
msgs: string[],
result: Message,
): string => {
const imageUris: string[] = [];
const textMessages: string[] = [];
const fileLinks: string[] = []; // Chứa danh sách file dạng [name](url)
for (const msg of msgs) {
// Nếu là link `/user_uploads/...` thì render ảnh
// 1. Nếu là link ảnh upload → hiển thị dạng image
if (/\/user_uploads\//.test(msg)) {
imageUris.push(`[image](${msg.replace(/^\/api\/v1/, '')})`);
} else {
textMessages.push(formatTextIfValid(msg));
continue;
}
// 2. Nếu là JSON và có type === 'file'
try {
const parsed = JSON.parse(msg);
if (parsed?.type === 'file' && parsed.url && parsed.name) {
fileLinks.push(`[${parsed.name}](${parsed.url})`);
continue;
}
} catch {
// Không phải JSON → bỏ qua, xử lý như text
}
// 3. Còn lại là text thường
textMessages.push(formatTextIfValid(msg));
}
// ==== Build nội dung cuối ====
let finalContent = `** :rocket: ${result.name} sent - ${formatTimeAU(result.time_raw)}:**\n`;
// Nếu có text → thêm block code
// Text chính → code block
if (textMessages.length > 0) {
finalContent += `\`\`\`\n${textMessages.join('\n')}\n\`\`\`\n`;
}
// Nếu có ảnh → thêm danh sách ảnh ở cuối
// Ảnh → dưới text
if (imageUris.length > 0) {
finalContent += imageUris.join('\n');
finalContent += `${imageUris.join('\n')}\n`;
}
// File → phần cuối cùng
if (fileLinks.length > 0) {
finalContent += `\n**Files:**\n${fileLinks.join('\n')}`;
}
return finalContent.trim();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB