Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
236 lines
6.2 KiB
TypeScript
236 lines
6.2 KiB
TypeScript
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<IUser['username']>[];
|
|
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<IUser> => {
|
|
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<IUser['username']>[];
|
|
msg: IMessage['msg'];
|
|
type: string;
|
|
visitor?: ILivechatVisitor['_id'];
|
|
agent?: ILivechatAgent['_id'];
|
|
};
|
|
|
|
export const auditGetMessagesMethod = async (userId: string | null, params: AuditMessagesParams): Promise<IMessage[]> => {
|
|
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<IMessage> = {
|
|
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<IUser['username']>[];
|
|
msg: IMessage['msg'];
|
|
type: string;
|
|
visitor?: ILivechatVisitor['_id'];
|
|
agent?: ILivechatAgent['_id'];
|
|
};
|
|
|
|
export const auditGetOmnichannelMessagesMethod = async (
|
|
userId: string | null,
|
|
params: AuditOmnichannelMessagesParams,
|
|
): Promise<IMessage[]> => {
|
|
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<IMessage> = {
|
|
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<IAuditLog[]> => {
|
|
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();
|
|
};
|