Skip to content

chore: Remove some omnichannel tech debt -2 #35854

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions apps/meteor/app/livechat/imports/server/rest/departments.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { ILivechatDepartment } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models';
import { isGETLivechatDepartmentProps, isPOSTLivechatDepartmentProps } from '@rocket.chat/rest-typings';
import {
isLivechatDepartmentDepartmentIdAgentsGETProps,
isLivechatDepartmentDepartmentIdAgentsPOSTProps,
} from '@rocket.chat/rest-typings/src/v1/omnichannel';
import { Match, check } from 'meteor/check';

import { API } from '../../../../api/server';
Expand Down Expand Up @@ -270,12 +274,10 @@ API.v1.addRoute(
GET: { permissions: ['view-livechat-departments', 'view-l-room'], operation: 'hasAny' },
POST: { permissions: ['manage-livechat-departments', 'add-livechat-department-agents'], operation: 'hasAny' },
},
validateParams: { GET: isLivechatDepartmentDepartmentIdAgentsGETProps, POST: isLivechatDepartmentDepartmentIdAgentsPOSTProps },
},
{
async get() {
check(this.urlParams, {
_id: String,
});
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();

Expand All @@ -292,13 +294,6 @@ API.v1.addRoute(
return API.v1.success(agents);
},
async post() {
check(
this.bodyParams,
Match.ObjectIncluding({
upsert: Array,
remove: Array,
}),
);
await saveDepartmentAgents(this.urlParams._id, this.bodyParams);

return API.v1.success();
Expand Down
33 changes: 17 additions & 16 deletions apps/meteor/app/livechat/server/api/lib/livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,15 @@ async function findDepartments(
businessUnit?: string,
): Promise<Pick<ILivechatDepartment, '_id' | 'name' | 'showOnRegistration' | 'showOnOfflineForm' | 'departmentsAllowedToForward'>[]> {
// TODO: check this function usage
return (
await LivechatDepartment.findEnabledWithAgentsAndBusinessUnit<
Pick<ILivechatDepartment, '_id' | 'name' | 'showOnRegistration' | 'showOnOfflineForm' | 'departmentsAllowedToForward'>
>(businessUnit, {
_id: 1,
name: 1,
showOnRegistration: 1,
showOnOfflineForm: 1,
departmentsAllowedToForward: 1,
})
).toArray();
return LivechatDepartment.findEnabledWithAgentsAndBusinessUnit<
Pick<ILivechatDepartment, '_id' | 'name' | 'showOnRegistration' | 'showOnOfflineForm' | 'departmentsAllowedToForward'>
>(businessUnit, {
_id: 1,
name: 1,
showOnRegistration: 1,
showOnOfflineForm: 1,
departmentsAllowedToForward: 1,
}).toArray();
}

export function findGuest(token: string): Promise<ILivechatVisitor | null> {
Expand Down Expand Up @@ -96,12 +94,13 @@ export function normalizeHttpHeaderData(headers: Headers = new Headers()): {
}

export async function settings({ businessUnit = '' }: { businessUnit?: string } = {}): Promise<Record<string, string | number | any>> {
// Putting this ugly conversion while we type the livechat service
const initSettings = await getInitSettings();
const triggers = await findTriggers();
const departments = await findDepartments(businessUnit);
const [initSettings, triggers, departments, emojis] = await Promise.all([
getInitSettings(),
findTriggers(),
findDepartments(businessUnit),
EmojiCustom.find().toArray(),
]);
const sound = `${Meteor.absoluteUrl()}sounds/chime.mp3`;
const emojis = await EmojiCustom.find().toArray();
return {
enabled: initSettings.Livechat_enabled,
settings: {
Expand Down Expand Up @@ -178,11 +177,13 @@ export async function settings({ businessUnit = '' }: { businessUnit?: string }
};
}

// TODO: try to use a patchfunction instead of a callback
export async function getExtraConfigInfo(room?: IOmnichannelRoom): Promise<any> {
return callbacks.run('livechat.onLoadConfigApi', { room });
}

// TODO: please forgive me for this. Still finding the good types for these callbacks
// TODO2: try to use a patchfunction instead of a callback
export function onCheckRoomParams(params: any): Promise<unknown> {
return callbacks.run('livechat.onCheckRoomApiParams', params);
}
15 changes: 7 additions & 8 deletions apps/meteor/app/livechat/server/api/v1/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@ API.v1.addRoute(
}

const { token, department, businessUnit } = this.queryParams;

const config = await cachedSettings({ businessUnit });

const status = await online(department);
const guest = token ? await findGuestWithoutActivity(token) : null;
const [config, status, guest] = await Promise.all([
cachedSettings({ businessUnit }),
online(department),
token ? findGuestWithoutActivity(token) : null,
]);

const room = guest ? await findOpenRoom(guest.token) : undefined;
const agent = guest && room && room.servedBy && (await findAgent(room.servedBy._id));
const [agent, extraInfo] = await Promise.all([room?.servedBy ? findAgent(room.servedBy._id) : null, getExtraConfigInfo(room)]);

const extra = await getExtraConfigInfo(room);
return API.v1.success({
config: { ...config, online: status, ...extra, ...(guest && { guest }), ...(room && { room }), ...(agent && { agent }) },
config: { ...config, online: status, ...extraInfo, ...(guest && { guest }), ...(room && { room }), ...(agent && { agent }) },
});
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/livechat/server/lib/Helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ const parseFromIntOrStr = (value: string | number) => {
export const updateDepartmentAgents = async (
departmentId: string,
agents: {
upsert?: Pick<ILivechatDepartmentAgents, 'agentId' | 'count' | 'order'>[];
upsert?: (Pick<ILivechatDepartmentAgents, 'agentId'> & { count?: number; order?: number })[];
remove?: Pick<ILivechatDepartmentAgents, 'agentId'>[];
},
departmentEnabled: boolean,
Expand Down
30 changes: 7 additions & 23 deletions apps/meteor/app/livechat/server/lib/departmentsLib.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AppEvents, Apps } from '@rocket.chat/apps';
import type { LivechatDepartmentDTO, ILivechatDepartment, ILivechatDepartmentAgents, ILivechatAgent } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents, LivechatVisitors, LivechatRooms, Users } from '@rocket.chat/models';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { updateDepartmentAgents } from './Helper';
Expand All @@ -25,7 +26,7 @@ export async function saveDepartment(
departmentData: LivechatDepartmentDTO,
departmentAgents?: {
upsert?: { agentId: string; count?: number; order?: number }[];
remove?: { agentId: string; count?: number; order?: number };
remove?: { agentId: string; count?: number; order?: number }[];
},
departmentUnit?: { _id?: string },
) {
Expand Down Expand Up @@ -182,30 +183,13 @@ export async function unarchiveDepartment(_id: string) {
export async function saveDepartmentAgents(
_id: string,
departmentAgents: {
upsert?: Pick<ILivechatDepartmentAgents, 'agentId' | 'count' | 'order' | 'username'>[];
remove?: Pick<ILivechatDepartmentAgents, 'agentId'>[];
upsert?: (Pick<ILivechatDepartmentAgents, 'agentId' | 'username'> & {
count?: number;
sort?: number;
})[];
remove?: Pick<ILivechatDepartmentAgents, 'agentId' | 'username'>[];
},
) {
check(_id, String);
check(departmentAgents, {
upsert: Match.Maybe([
Match.ObjectIncluding({
agentId: String,
username: String,
count: Match.Maybe(Match.Integer),
order: Match.Maybe(Match.Integer),
}),
]),
remove: Match.Maybe([
Match.ObjectIncluding({
agentId: String,
username: Match.Maybe(String),
count: Match.Maybe(Match.Integer),
order: Match.Maybe(Match.Integer),
}),
]),
});

const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'enabled'>>(_id, { projection: { enabled: 1 } });
if (!department) {
throw new Meteor.Error('error-department-not-found', 'Department not found');
Expand Down
11 changes: 0 additions & 11 deletions apps/meteor/app/livechat/server/lib/omni-users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { afterAgentAdded, afterRemoveAgent } from './hooks';
import { callbacks } from '../../../../lib/callbacks';
import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles';
import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles';
import { hasRoleAsync } from '../../../authorization/server/functions/hasRole';
import { settings } from '../../../settings/server';

export async function notifyAgentStatusChanged(userId: string, status?: UserStatus) {
Expand Down Expand Up @@ -83,16 +82,6 @@ export async function removeManager(id: IUser['_id']) {
}

export async function saveAgentInfo(_id: string, agentData: any, agentDepartments: string[]) {
// TODO: check if these 'check' functions are necessary
check(_id, String);
check(agentData, Object);
check(agentDepartments, [String]);

const user = await Users.findOneById(_id);
if (!user || !(await hasRoleAsync(_id, 'livechat-agent'))) {
throw new Error('error-user-is-not-agent');
}

await Users.setLivechatData(_id, removeEmpty(agentData));

const currentDepartmentsForAgent = await LivechatDepartmentAgents.findByAgentId(_id).toArray();
Expand Down
12 changes: 7 additions & 5 deletions apps/meteor/app/livechat/server/methods/saveAgentInfo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
Expand All @@ -15,15 +14,18 @@ declare module '@rocket.chat/ddp-client' {

Meteor.methods<ServerMethods>({
async 'livechat:saveAgentInfo'(_id, agentData, agentDepartments) {
const uid = Meteor.userId();
if (!uid || !(await hasPermissionAsync(uid, 'manage-livechat-agents'))) {
check(_id, String);
check(agentData, Object);
check(agentDepartments, [String]);

const user = await Meteor.userAsync();
if (!user || !(await hasPermissionAsync(user._id, 'manage-livechat-agents'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'livechat:saveAgentInfo',
});
}

const user = await Users.findOneById(_id);
if (!user || !(await hasRoleAsync(_id, 'livechat-agent'))) {
if (!(await hasRoleAsync(_id, 'livechat-agent'))) {
throw new Meteor.Error('error-user-is-not-agent', 'User is not a livechat agent', {
method: 'livechat:saveAgentInfo',
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IOmnichannelBusinessUnit, IOmnichannelServiceLevelAgreements } from '@rocket.chat/core-typings';
import type { IOmnichannelBusinessUnit, IOmnichannelServiceLevelAgreements, IUser, ILivechatTag } from '@rocket.chat/core-typings';
import { Users, OmnichannelServiceLevelAgreements, LivechatTag, LivechatUnitMonitors, LivechatUnit } from '@rocket.chat/models';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand All @@ -11,9 +11,9 @@ import { removeUserFromRolesAsync } from '../../../../../server/lib/roles/remove

export const LivechatEnterprise = {
async addMonitor(username: string) {
check(username, String);

const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } });
const user = await Users.findOneByUsername<Pick<IUser, '_id' | 'username' | 'roles'>>(username, {
projection: { _id: 1, username: 1 },
});

if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
Expand All @@ -29,18 +29,17 @@ export const LivechatEnterprise = {
},

async removeMonitor(username: string) {
check(username, String);

const user = await Users.findOneByUsername(username, { projection: { _id: 1 } });
const user = await Users.findOneByUsername<Pick<IUser, '_id' | 'username'>>(username, {
projection: { _id: 1, username: 1 },
});

if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
method: 'livechat:removeMonitor',
});
}

const removeRoleResult = await removeUserFromRolesAsync(user._id, ['livechat-monitor']);
if (!removeRoleResult) {
if (!(await removeUserFromRolesAsync(user._id, ['livechat-monitor']))) {
return false;
}

Expand All @@ -51,9 +50,7 @@ export const LivechatEnterprise = {
},

async removeUnit(_id: string) {
check(_id, String);

const unit = await LivechatUnit.findOneById(_id, { projection: { _id: 1 } });
const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id'>>(_id, { projection: { _id: 1 } });

if (!unit) {
throw new Meteor.Error('unit-not-found', 'Unit not found', { method: 'livechat:removeUnit' });
Expand Down Expand Up @@ -94,7 +91,9 @@ export const LivechatEnterprise = {

let ancestors: string[] = [];
if (_id) {
const unit = await LivechatUnit.findOneById(_id);
const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id' | 'ancestors'>>(_id, {
projection: { _id: 1, ancestors: 1 },
});
if (!unit) {
throw new Meteor.Error('error-unit-not-found', 'Unit not found', {
method: 'livechat:saveUnit',
Expand All @@ -112,16 +111,14 @@ export const LivechatEnterprise = {

const monitors = validUserMonitors.map(({ _id: monitorId, username }) => ({
monitorId,
username,
})) as { monitorId: string; username: string }[];
username: username!,
}));

return LivechatUnit.createOrUpdateUnit(_id, unitData, ancestors, monitors, unitDepartments);
},

async removeTag(_id: string) {
check(_id, String);

const tag = await LivechatTag.findOneById(_id, { projection: { _id: 1, name: 1 } });
const tag = await LivechatTag.findOneById<Pick<ILivechatTag, '_id' | 'name'>>(_id, { projection: { _id: 1, name: 1 } });

if (!tag) {
throw new Meteor.Error('tag-not-found', 'Tag not found', { method: 'livechat:removeTag' });
Expand All @@ -132,20 +129,15 @@ export const LivechatEnterprise = {
},

async saveTag(_id: string | undefined, tagData: { name: string; description?: string }, tagDepartments: string[]) {
check(_id, Match.Maybe(String));

check(tagData, {
name: String,
description: Match.Optional(String),
});

check(tagDepartments, [String]);

return LivechatTag.createOrUpdateTag(_id, tagData, tagDepartments);
},

async saveSLA(_id: string | null, slaData: Pick<IOmnichannelServiceLevelAgreements, 'name' | 'description' | 'dueTimeInMinutes'>) {
const oldSLA = _id && (await OmnichannelServiceLevelAgreements.findOneById(_id, { projection: { dueTimeInMinutes: 1 } }));
const oldSLA =
_id &&
(await OmnichannelServiceLevelAgreements.findOneById<Pick<IOmnichannelServiceLevelAgreements, 'dueTimeInMinutes'>>(_id, {
projection: { dueTimeInMinutes: 1 },
}));
const exists = await OmnichannelServiceLevelAgreements.findDuplicate(_id, slaData.name, slaData.dueTimeInMinutes);
if (exists) {
throw new Error('error-duplicated-sla');
Expand All @@ -167,7 +159,9 @@ export const LivechatEnterprise = {
},

async removeSLA(_id: string) {
const sla = await OmnichannelServiceLevelAgreements.findOneById(_id, { projection: { _id: 1 } });
const sla = await OmnichannelServiceLevelAgreements.findOneById<Pick<IOmnichannelServiceLevelAgreements, '_id'>>(_id, {
projection: { _id: 1 },
});
if (!sla) {
throw new Error(`SLA with id ${_id} not found`);
}
Expand Down

This file was deleted.

Loading
Loading