import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import type { ILivechatAgent, ILivechatVisitor, IMessage, IRoom, IUser, IAuditLog } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import type { Filter } from 'mongodb'; import { LivechatRooms, Messages, Rooms, Users, AuditLog } from '@rocket.chat/models'; import { hasPermissionAsync } from '../../../../app/authorization/server/functions/hasPermission'; import { updateCounter } from '../../../../app/statistics/server'; import { isTruthy } from '../../../../lib/isTruthy'; import { i18n } from '../../../../server/lib/i18n'; import { callbacks } from '../../../../lib/callbacks'; const getValue = (room: IRoom | null) => room && { rids: [room._id], name: room.name }; const getUsersIdFromUserName = async (usernames: IUser['username'][]) => { const users = usernames ? await Users.findByUsernames(usernames.filter(isTruthy)).toArray() : undefined; return users?.filter(isTruthy).map((userId) => userId._id); }; const getRoomInfoByAuditParams = async ({ type, roomId: rid, users: usernames, visitor, agent, }: { type: string; roomId: IRoom['_id']; users: NonNullable[]; visitor: ILivechatVisitor['_id']; agent: ILivechatAgent['_id']; }) => { if (rid) { return getValue(await Rooms.findOne({ _id: rid })); } if (type === 'd') { return getValue(await Rooms.findDirectRoomContainingAllUsernames(usernames)); } if (type === 'l') { console.warn('Deprecation Warning! This method will be removed in the next version (4.0.0)'); const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId( visitor, agent, { projection: { _id: 1 }, }, extraQuery, ).toArray(); return rooms?.length ? { rids: rooms.map(({ _id }) => _id), name: i18n.t('Omnichannel') } : undefined; } }; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { auditGetAuditions: (params: { startDate: Date; endDate: Date }) => IAuditLog[]; auditGetMessages: (params: { rid: IRoom['_id']; startDate: Date; endDate: Date; users: NonNullable[]; msg: IMessage['msg']; type: string; visitor: ILivechatVisitor['_id']; agent: ILivechatAgent['_id']; }) => IMessage[]; auditGetOmnichannelMessages: (params: { startDate: Date; endDate: Date; users: NonNullable[]; msg: IMessage['msg']; type: string; visitor?: ILivechatVisitor['_id']; agent?: ILivechatAgent['_id']; }) => IMessage[]; } } Meteor.methods({ async auditGetOmnichannelMessages({ startDate, endDate, users: usernames, msg, type, visitor, agent }) { check(startDate, Date); check(endDate, Date); const user = await Meteor.userAsync(); if (!user || !(await hasPermissionAsync(user._id, 'can-audit'))) { throw new Meteor.Error('Not allowed'); } const rooms: IRoom[] = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { projection: { _id: 1 }, }).toArray(); const rids = rooms?.length ? rooms.map(({ _id }) => _id) : undefined; const name = i18n.t('Omnichannel'); const query: Filter = { rid: { $in: rids }, ts: { $gt: startDate, $lt: endDate, }, }; if (msg) { const regex = new RegExp(escapeRegExp(msg).trim(), 'i'); query.msg = regex; } const messages = await Messages.find(query).toArray(); // Once the filter is applied, messages will be shown and a log containing all filters will be saved for further auditing. await AuditLog.insertOne({ ts: new Date(), results: messages.length, u: user, fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, }); return messages; }, async auditGetMessages({ rid, startDate, endDate, users: usernames, msg, type, visitor, agent }) { check(startDate, Date); check(endDate, Date); const user = await Meteor.userAsync(); if (!user || !(await hasPermissionAsync(user._id, 'can-audit'))) { throw new Meteor.Error('Not allowed'); } let rids; let name; const query: Filter = { ts: { $gt: startDate, $lt: endDate, }, }; if (type === 'u') { const usersId = await getUsersIdFromUserName(usernames); query['u._id'] = { $in: usersId }; } else { const roomInfo = await getRoomInfoByAuditParams({ type, roomId: rid, users: usernames, visitor, agent }); if (!roomInfo) { throw new Meteor.Error('Room doesn`t exist'); } rids = roomInfo.rids; name = roomInfo.name; query.rid = { $in: rids }; } if (msg) { const regex = new RegExp(escapeRegExp(msg).trim(), 'i'); query.msg = regex; } const messages = await Messages.find(query).toArray(); // Once the filter is applied, messages will be shown and a log containing all filters will be saved for further auditing. await AuditLog.insertOne({ ts: new Date(), results: messages.length, u: user, fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, }); updateCounter({ settingsId: 'Message_Auditing_Panel_Load_Count' }); return messages; }, async auditGetAuditions({ startDate, endDate }) { check(startDate, Date); check(endDate, Date); const uid = Meteor.userId(); if (!uid || !(await hasPermissionAsync(uid, 'can-audit-log'))) { throw new Meteor.Error('Not allowed'); } return AuditLog.find({ // 'u._id': userId, ts: { $gt: startDate, $lt: endDate, }, }).toArray(); }, }); DDPRateLimiter.addRule( { type: 'method', name: 'auditGetAuditions', userId(/* userId*/) { return true; }, }, 10, 60000, ); DDPRateLimiter.addRule( { type: 'method', name: 'auditGetMessages', userId(/* userId*/) { return true; }, }, 10, 60000, );