update view message
This commit is contained in:
parent
ebd81e11a2
commit
14747053d2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -44,3 +44,11 @@ contentService.startSyncConversations();
|
||||||
|
|
||||||
// AUTO SYNC MESAGE PREFIX (INTERNAL)
|
// AUTO SYNC MESAGE PREFIX (INTERNAL)
|
||||||
contentService.autoSyncConversationPrefixMessages();
|
contentService.autoSyncConversationPrefixMessages();
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// const a = new TeamsChatService();
|
||||||
|
|
||||||
|
// const message = a.extractAllMessages();
|
||||||
|
|
||||||
|
// console.log({ message });
|
||||||
|
// }, 10000);
|
||||||
|
|
|
||||||
|
|
@ -53,53 +53,137 @@ export class TeamsChatService {
|
||||||
return { room_id: roomId, room_name: roomName };
|
return { room_id: roomId, room_name: roomName };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getMessageByEl(el: HTMLElement | null): string | string[] {
|
_getFileLinks(el: HTMLElement) {
|
||||||
if (!el) return "";
|
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ư
|
// 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ó)
|
// Lấy danh sách ảnh (nếu có)
|
||||||
const sharedImages = this._getImageFormEl(el) || [];
|
const sharedImages = this._getImageFormEl(el) || [];
|
||||||
|
|
||||||
if (sharedImages.length > 0) {
|
const imageSrcs = sharedImages
|
||||||
const arrMessage = sharedImages.map(
|
.map((img) => img.getAttribute("src"))
|
||||||
(img) => img.getAttribute("src") || ""
|
.filter(Boolean) as string[];
|
||||||
);
|
|
||||||
|
|
||||||
if (message.length > 0) {
|
if (imageSrcs.length > 0) {
|
||||||
// Kiểm tra overlay
|
// Kiểm tra overlay
|
||||||
const overlay = document
|
const overlay = document
|
||||||
.getElementById(`message-body-${(el as any)?.date_time}`)
|
.getElementById(`message-body-${(el as any)?.date_time}`)
|
||||||
?.querySelector('[data-tid="overlay-count-text"]');
|
?.querySelector('[data-tid="overlay-count-text"]');
|
||||||
|
|
||||||
// Nếu overlay text === message gốc → chỉ trả về ảnh
|
if (overlay && (overlay as HTMLElement).innerText === messageText) {
|
||||||
if (overlay && (overlay as HTMLElement).innerText === message) {
|
messages.push(...imageSrcs);
|
||||||
return arrMessage;
|
} else {
|
||||||
}
|
messages.push(...imageSrcs);
|
||||||
|
if (messageText) messages.push(messageText);
|
||||||
// Nếu không, thêm message vào cuối
|
|
||||||
arrMessage.push(message);
|
|
||||||
}
|
}
|
||||||
|
} else if (messageText) {
|
||||||
return arrMessage;
|
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(
|
const emojiImgs = Array.from(
|
||||||
el.querySelectorAll("img[itemtype]")
|
el.querySelectorAll("img[itemtype]")
|
||||||
) as HTMLImageElement[];
|
) as HTMLImageElement[];
|
||||||
|
|
||||||
const emojiAlts = emojiImgs
|
const emojiAlts = emojiImgs
|
||||||
.map((img) => img.getAttribute("alt") || "")
|
.map((img) => img.getAttribute("alt") || "")
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
// Nếu có emoji thì nối vào message
|
|
||||||
if (emojiAlts.length > 0) {
|
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 {
|
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);
|
return [...myMessages, ...otherMessages].sort((a, b) => a.time - b.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleNewMessage(message: IMessage) {
|
// private handleNewMessage(message: IMessage) {
|
||||||
// Gửi tới một server AI để phản hồi (nếu cần)
|
// // Gửi tới một server AI để phản hồi (nếu cần)
|
||||||
// fetch('https://127.0.0.1:8443/reply', { ... })
|
// // fetch('https://127.0.0.1:8443/reply', { ... })
|
||||||
console.log("%c[New incoming message]", "color: #007acc;", message);
|
// console.log("%c[New incoming message]", "color: #007acc;", message);
|
||||||
}
|
// }
|
||||||
|
|
||||||
public async detectNewMessages() {
|
public async detectNewMessages() {
|
||||||
const allMessages = this.extractAllMessages();
|
const allMessages = this.extractAllMessages();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -27,6 +27,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^9.9.0",
|
"@faker-js/faker": "^9.9.0",
|
||||||
"@keyv/redis": "^5.0.0",
|
"@keyv/redis": "^5.0.0",
|
||||||
|
"@nestjs-modules/mailer": "^2.0.2",
|
||||||
"@nestjs/axios": "^4.0.1",
|
"@nestjs/axios": "^4.0.1",
|
||||||
"@nestjs/cache-manager": "^3.0.1",
|
"@nestjs/cache-manager": "^3.0.1",
|
||||||
"@nestjs/class-transformer": "^0.4.0",
|
"@nestjs/class-transformer": "^0.4.0",
|
||||||
|
|
@ -56,6 +57,7 @@
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"mysql2": "^3.14.3",
|
"mysql2": "^3.14.3",
|
||||||
"nestjs-paginate": "^12.5.0",
|
"nestjs-paginate": "^12.5.0",
|
||||||
|
"nodemailer": "^7.0.6",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"socket.io": "^4.8.1"
|
"socket.io": "^4.8.1"
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||||
import { MessagesModule } from './modules/messages/messages.module';
|
import { MessagesModule } from './modules/messages/messages.module';
|
||||||
import { AppConfigsModule } from './modules/app-configs/app-configs.module';
|
import { AppConfigsModule } from './modules/app-configs/app-configs.module';
|
||||||
import { ConversationsModule } from './modules/conversations/conversations.module';
|
import { ConversationsModule } from './modules/conversations/conversations.module';
|
||||||
|
import { MailerModule } from './modules/mailer/mailer.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MessagesModule, AppConfigsModule, ConversationsModule],
|
imports: [MessagesModule, AppConfigsModule, ConversationsModule, MailerModule],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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 {}
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,13 +6,14 @@ import { SendLogDto } from './dtos/send-log.dto';
|
||||||
import { ZulipService } from './zulip.service';
|
import { ZulipService } from './zulip.service';
|
||||||
import AppResponse from '@/system/filters/response/app-response';
|
import AppResponse from '@/system/filters/response/app-response';
|
||||||
import { SystemLang } from '@/system/lang/system.lang';
|
import { SystemLang } from '@/system/lang/system.lang';
|
||||||
|
import { MailerService } from '../mailer/mailer.service';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LogsService {
|
export class LogsService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Log)
|
@InjectRepository(Log)
|
||||||
readonly repo: Repository<Log>,
|
readonly repo: Repository<Log>,
|
||||||
|
|
||||||
private readonly zulipService: ZulipService,
|
private readonly zulipService: ZulipService,
|
||||||
|
private readonly malerService: MailerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async saveLog(data: SendLogDto) {
|
async saveLog(data: SendLogDto) {
|
||||||
|
|
@ -28,11 +29,17 @@ export class LogsService {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.zulipService.sendMessageToTopic(
|
// 2. Build nội dung email
|
||||||
process.env.ZULIP_STREAMS_NAME,
|
const subject = `[${result.type.toUpperCase()}] - New Log Message`;
|
||||||
process.env.ZULIP_TOPPIC_LOG_NAME,
|
const content = `
|
||||||
`[${result.type.toUpperCase()}] - ${result.message}`,
|
<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);
|
return AppResponse.toResponse(result);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,14 @@ import { ZulipService } from './zulip.service';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { Log } from '@/entities/log.entity';
|
import { Log } from '@/entities/log.entity';
|
||||||
import { LogsService } from './logs.service';
|
import { LogsService } from './logs.service';
|
||||||
|
import { MailerModule } from '../mailer/mailer.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Message, Conversation, Log]), HttpModule],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Message, Conversation, Log]),
|
||||||
|
HttpModule,
|
||||||
|
MailerModule,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
MessagesService,
|
MessagesService,
|
||||||
MessagesGateway,
|
MessagesGateway,
|
||||||
|
|
|
||||||
|
|
@ -190,32 +190,83 @@ export class MessagesService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Build message để gửi lên Zulip
|
// 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 = (
|
const buildZulipMessageContent = (
|
||||||
msgs: string[],
|
msgs: string[],
|
||||||
result: Message,
|
result: Message,
|
||||||
): string => {
|
): string => {
|
||||||
const imageUris: string[] = [];
|
const imageUris: string[] = [];
|
||||||
const textMessages: string[] = [];
|
const textMessages: string[] = [];
|
||||||
|
const fileLinks: string[] = []; // Chứa danh sách file dạng [name](url)
|
||||||
|
|
||||||
for (const msg of msgs) {
|
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)) {
|
if (/\/user_uploads\//.test(msg)) {
|
||||||
imageUris.push(`[image](${msg.replace(/^\/api\/v1/, '')})`);
|
imageUris.push(`[image](${msg.replace(/^\/api\/v1/, '')})`);
|
||||||
} else {
|
continue;
|
||||||
textMessages.push(formatTextIfValid(msg));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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`;
|
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) {
|
if (textMessages.length > 0) {
|
||||||
finalContent += `\`\`\`\n${textMessages.join('\n')}\n\`\`\`\n`;
|
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) {
|
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();
|
return finalContent.trim();
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Loading…
Reference in New Issue