import type { ILivechatAgent, ILivechatVisitor, IMessage, IRoom, IUser, IAuditLog } from '@rocket.chat/core-typings'; import { LivechatRooms, Messages, Rooms, Users, AuditLog } from '@rocket.chat/models'; import { escapeRegExp, isTruthy } from '@rocket.chat/tools'; import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; import { hasPermissionAsync } from '../../../../server/lib/authorization/hasPermission'; import { callbacks } from '../../../../server/lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { updateCounter } from '../../../../server/lib/statistics'; 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, userId, }: { type: string; roomId: IRoom['_id']; users: NonNullable[]; visitor: ILivechatVisitor['_id']; agent: ILivechatAgent['_id']; userId: string; }) => { if (rid) { return getValue(await Rooms.findOne({ _id: rid, abacAttributes: { $exists: false } })); } if (type === 'd') { return getValue(await Rooms.findDirectRoomContainingAllUsernames(usernames)); } if (type === 'l') { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId }); const rooms = await LivechatRooms.findByVisitorIdAndAgentId( visitor, agent, { projection: { _id: 1 }, }, extraQuery, ).toArray(); return rooms?.length ? { rids: rooms.map(({ _id }) => _id), name: i18n.t('Omnichannel') } : undefined; } }; const requireAuditor = async (userId: string | null): Promise => { if (!userId) { throw new Meteor.Error('Not allowed'); } const user = await Users.findOneById(userId); if (!user || !(await hasPermissionAsync(user._id, 'can-audit'))) { throw new Meteor.Error('Not allowed'); } return user; }; type AuditMessagesParams = { rid?: IRoom['_id']; startDate: Date; endDate: Date; users: NonNullable[]; msg: IMessage['msg']; type: string; visitor?: ILivechatVisitor['_id']; agent?: ILivechatAgent['_id']; }; export const auditGetMessagesMethod = async (userId: string | null, params: AuditMessagesParams): Promise => { const { rid, startDate, endDate, users: usernames, msg, type, visitor, agent } = params; const user = await requireAuditor(userId); const userFields = { _id: user._id, username: user.username, ...(user.name && { name: user.name }), ...(user.avatarETag && { avatarETag: user.avatarETag }), }; 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 }; const abacRooms = await Rooms.findAllPrivateRoomsWithAbacAttributes({ projection: { _id: 1 } }) .map((doc) => doc._id) .toArray(); query.rid = { $nin: abacRooms }; } else { const roomInfo = await getRoomInfoByAuditParams({ type, roomId: rid ?? '', users: usernames, visitor: visitor ?? '', agent: agent ?? '', userId: user._id, }); 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(); await AuditLog.insertOne( { ts: new Date(), results: messages.length, u: userFields, fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, }, { ignoreUndefined: true }, ); updateCounter({ settingsId: 'Message_Auditing_Panel_Load_Count' }); return messages; }; type AuditOmnichannelMessagesParams = { startDate: Date; endDate: Date; users: NonNullable[]; msg: IMessage['msg']; type: string; visitor?: ILivechatVisitor['_id']; agent?: ILivechatAgent['_id']; }; export const auditGetOmnichannelMessagesMethod = async ( userId: string | null, params: AuditOmnichannelMessagesParams, ): Promise => { const { startDate, endDate, users: usernames, msg, type, visitor, agent } = params; const user = await requireAuditor(userId); const userFields = { _id: user._id, username: user.username, ...(user.name && { name: user.name }), ...(user.avatarETag && { avatarETag: user.avatarETag }), }; // No livechat.applyRoomRestrictions here (unlike the type 'l' path in getRoomInfoByAuditParams): this // mirrors the original DDP auditGetOmnichannelMessages. Access is gated by the can-audit permission and // audit is intended to span all omnichannel rooms, so unit/visibility restrictions are not applied. const rooms = await LivechatRooms.findByVisitorIdAndAgentId(visitor, agent, { projection: { _id: 1 }, }).toArray(); // keep rids an array — `$in: undefined` throws "$in needs an array" when no rooms match const rids = rooms.map(({ _id }) => _id); 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(); await AuditLog.insertOne( { ts: new Date(), results: messages.length, u: userFields, fields: { msg, users: usernames, rids, room: name, startDate, endDate, type, visitor, agent }, }, { ignoreUndefined: true }, ); return messages; }; export const auditGetAuditionsMethod = async (userId: string | null, startDate: Date, endDate: Date): Promise => { if (!userId || !(await hasPermissionAsync(userId, 'can-audit-log'))) { throw new Meteor.Error('Not allowed'); } return AuditLog.find( { ts: { $gt: startDate, $lt: endDate, }, }, { projection: { 'u.services': 0, 'u.roles': 0, 'u.lastLogin': 0, 'u.statusConnection': 0, 'u.emails': 0, }, }, ).toArray(); };