import moment from 'moment';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { ILivechatDepartment, ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models';

import type { IBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import { AbstractBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import { filterBusinessHoursThatMustBeOpened } from '../../../../../app/livechat/server/business-hour/Helper';
import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper';
import { bhLogger } from '../lib/logger';

interface IBusinessHoursExtraProperties extends ILivechatBusinessHour {
	timezoneName: string;
	departmentsToApplyBusinessHour: string;
}

export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior {
	constructor() {
		super();
		this.onAddAgentToDepartment = this.onAddAgentToDepartment.bind(this);
		this.onRemoveAgentFromDepartment = this.onRemoveAgentFromDepartment.bind(this);
		this.onRemoveDepartment = this.onRemoveDepartment.bind(this);
	}

	async onStartBusinessHours(): Promise<void> {
		await this.UsersRepository.removeBusinessHoursFromAllUsers();
		await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
		const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
		const day = currentTime.format('dddd');
		const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {
			fields: {
				workHours: 1,
				timezone: 1,
				type: 1,
				active: 1,
			},
		});
		const businessHoursToOpen = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
		for (const businessHour of businessHoursToOpen) {
			this.openBusinessHour(businessHour);
		}
	}

	async openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
		const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToOpen(day, hour, undefined, {
			fields: {
				_id: 1,
				type: 1,
			},
		});
		for (const businessHour of businessHours) {
			this.openBusinessHour(businessHour);
		}
	}

	async closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
		const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, undefined, {
			fields: {
				_id: 1,
				type: 1,
			},
		});
		for (const businessHour of businessHours) {
			this.closeBusinessHour(businessHour);
		}
	}

	async afterSaveBusinessHours(businessHourData: IBusinessHoursExtraProperties): Promise<void> {
		const departments = businessHourData.departmentsToApplyBusinessHour?.split(',').filter(Boolean);
		const currentDepartments = businessHourData.departments?.map((dept: any) => dept._id);
		const toRemove = [...(currentDepartments || []).filter((dept: Record<string, any>) => !departments.includes(dept._id))];
		await this.removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(businessHourData._id, toRemove);
		const businessHour = await this.BusinessHourRepository.findOneById(businessHourData._id);
		if (!businessHour) {
			return;
		}
		const businessHourIdToOpen = (await filterBusinessHoursThatMustBeOpened([businessHour])).map((businessHour) => businessHour._id);
		if (!businessHourIdToOpen.length) {
			return closeBusinessHour(businessHour);
		}
		return openBusinessHour(businessHour);
	}

	async onAddAgentToDepartment(options: Record<string, any> = {}): Promise<any> {
		const { departmentId, agentsId } = options;
		const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(departmentId, {
			projection: { businessHourId: 1 },
		});
		if (!department || !agentsId.length) {
			return options;
		}
		const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
		if (!defaultBusinessHour) {
			return options;
		}
		await removeBusinessHourByAgentIds(agentsId, defaultBusinessHour._id);
		if (!department.businessHourId) {
			return options;
		}
		const businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
		if (!businessHour) {
			return options;
		}
		const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([businessHour]);
		if (!businessHourToOpen.length) {
			return options;
		}
		await this.UsersRepository.addBusinessHourByAgentIds(agentsId, businessHour._id);
		return options;
	}

	async onRemoveAgentFromDepartment(options: Record<string, any> = {}): Promise<any> {
		const { departmentId, agentsId } = options;
		const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(departmentId, {
			projection: { businessHourId: 1 },
		});
		if (!department || !agentsId.length) {
			return options;
		}
		return this.handleRemoveAgentsFromDepartments(department, agentsId, options);
	}

	async onRemoveDepartment(options: Record<string, any> = {}): Promise<any> {
		const { department, agentsIds } = options;
		if (!department || !agentsIds?.length) {
			return options;
		}
		const deletedDepartment = LivechatDepartment.trashFindOneById(department._id);
		return this.handleRemoveAgentsFromDepartments(deletedDepartment, agentsIds, options);
	}

	async allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
		const isWithinBushinessHours = await this.UsersRepository.isAgentWithinBusinessHours(agentId);
		if (isWithinBushinessHours) {
			return true;
		}

		bhLogger.debug(`No active business hour found for agent with id: ${agentId} based on user's cache. Attempting to recheck the status`);

		// double check to see if user is actually within business hours
		// this is required since the cache of businessHour Ids we maintain within user's collection might be stale
		// in many scenario's like, if the agent is created when a business is active,
		// or if a normal user is converted to agent when a business hour is active
		const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
		const day = currentTime.format('dddd');
		const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {
			fields: {
				workHours: 1,
				timezone: 1,
				type: 1,
				active: 1,
			},
		});
		const openedBusinessHours = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
		if (!openedBusinessHours.length) {
			bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. No opened business hour found`);
			return false;
		}

		const agentDepartments = await LivechatDepartmentAgents.find(
			{ departmentEnabled: true, agentId },
			{ projection: { agentId: 1, departmentId: 1 } },
		).toArray();

		if (agentDepartments.length) {
			// check if any one these departments have a opened business hour linked to it
			const departments = await LivechatDepartment.findInIds(
				agentDepartments.map(({ departmentId }) => departmentId),
				{ projection: { _id: 1, businessHourId: 1 } },
			).toArray();

			const departmentsWithActiveBH = departments.filter(
				({ businessHourId }) => businessHourId && openedBusinessHours.findIndex(({ _id }) => _id === businessHourId) !== -1,
			);

			if (!departmentsWithActiveBH.length) {
				bhLogger.debug(
					`No opened business hour found for any of the departments connected to the agent with id: ${agentId}. Now, checking if the default business hour can be used`,
				);

				// check if this agent has any departments that is connected to any non-default business hour
				// if no such departments found then check default BH and if it is active, then allow the agent to change service status
				const hasAtLeastOneDepartmentWithNonDefaultBH = departments.some(({ businessHourId }) => !!businessHourId);
				if (!hasAtLeastOneDepartmentWithNonDefaultBH) {
					const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT);
					if (isDefaultBHActive?._id) {
						await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId);

						bhLogger.debug(`Business hour status recheck passed for agentId: ${agentId}. Found default business hour to be active`);
						return true;
					}
					bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. Found default business hour to be inactive`);
				}
				return false;
			}

			const activeBusinessHoursForAgent = departmentsWithActiveBH.map(({ businessHourId }) => businessHourId);
			await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId(activeBusinessHoursForAgent, agentId);

			bhLogger.debug(
				`Business hour status recheck passed for agentId: ${agentId}. Found following business hours to be active:`,
				activeBusinessHoursForAgent,
			);
			return true;
		}

		// check if default businessHour is active
		const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT);
		if (isDefaultBHActive?._id) {
			await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId);

			bhLogger.debug(`Business hour status recheck passed for agentId: ${agentId}. Found default business hour to be active`);
			return true;
		}

		bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. No opened business hour found`);

		return false;
	}

	private async handleRemoveAgentsFromDepartments(department: Record<string, any>, agentsIds: string[], options: any): Promise<any> {
		const agentIdsWithoutDepartment = [];
		const agentIdsToRemoveCurrentBusinessHour = [];
		for await (const agentId of agentsIds) {
			if ((await LivechatDepartmentAgents.findByAgentId(agentId).count()) === 0) {
				agentIdsWithoutDepartment.push(agentId);
			}
			if (!(await LivechatDepartmentAgents.findAgentsByAgentIdAndBusinessHourId(agentId, department.businessHourId)).length) {
				// eslint-disable-line no-await-in-loop
				agentIdsToRemoveCurrentBusinessHour.push(agentId);
			}
		}
		if (department.businessHourId) {
			await removeBusinessHourByAgentIds(agentIdsToRemoveCurrentBusinessHour, department.businessHourId);
		}
		if (!agentIdsWithoutDepartment.length) {
			return options;
		}
		const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
		if (!defaultBusinessHour) {
			return options;
		}
		const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]);
		if (!businessHourToOpen.length) {
			return options;
		}
		await this.UsersRepository.addBusinessHourByAgentIds(agentIdsWithoutDepartment, defaultBusinessHour._id);
		return options;
	}

	private async openBusinessHour(businessHour: Record<string, any>): Promise<void> {
		return openBusinessHour(businessHour);
	}

	private async removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(
		businessHourId: string,
		departmentsToRemove: string[],
	): Promise<void> {
		if (!departmentsToRemove.length) {
			return;
		}
		const agentIds = (await LivechatDepartmentAgents.findByDepartmentIds(departmentsToRemove).toArray()).map((dept: any) => dept.agentId);
		await removeBusinessHourByAgentIds(agentIds, businessHourId);
	}

	private async closeBusinessHour(businessHour: Record<string, any>): Promise<void> {
		closeBusinessHour(businessHour);
	}
}


import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Messages } from '@rocket.chat/models';

import { canAccessRoomId } from '../../app/authorization/server';
import { Subscriptions } from '../../app/models/server';
import { settings } from '../../app/settings/server';
import { readSecondaryPreferred } from '../database/readSecondaryPreferred';

Meteor.methods({
	messageSearch(text, rid, limit, offset) {
		check(text, String);
		check(rid, Match.Maybe(String));
		check(limit, Match.Optional(Number));
		check(offset, Match.Optional(Number));

		// TODO: Evaluate why we are returning `users` and `channels`, as the only thing that gets set is the `messages`.
		const result = {
			message: {
				docs: [],
			},
		};

		const currentUserId = Meteor.userId();
		if (!currentUserId) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', {
				method: 'messageSearch',
			});
		}

		// Don't process anything else if the user can't access the room
		if (rid) {
			if (!canAccessRoomId(rid, currentUserId)) {
				return false;
			}
		} else if (settings.get('Search.defaultProvider.GlobalSearchEnabled') !== true) {
			return result;
		}

		const user = Meteor.user();
		const currentUserName = user.username;
		const currentUserTimezoneOffset = user.utcOffset;

		const query = {};
		const options = {
			projection: {},
			sort: {
				ts: -1,
			},
			skip: offset || 0,
			limit: limit || 20,
		};

		// I would place these methods at the bottom of the file for clarity but travis doesn't appreciate that.
		// (no-use-before-define)

		function filterStarred() {
			query['starred._id'] = currentUserId;
			return '';
		}

		function filterUrl() {
			query['urls.0'] = {
				$exists: true,
			};
			return '';
		}

		function filterPinned() {
			query.pinned = true;
			return '';
		}

		function filterLocation() {
			query.location = {
				$exist: true,
			};
			return '';
		}

		function filterBeforeDate(_, day, month, year) {
			month--;
			const beforeDate = new Date(year, month, day);
			beforeDate.setHours(beforeDate.getUTCHours() + beforeDate.getTimezoneOffset() / 60 + currentUserTimezoneOffset);
			query.ts = {
				$lte: beforeDate,
			};
			return '';
		}

		function filterAfterDate(_, day, month, year) {
			month--;
			day++;
			const afterDate = new Date(year, month, day);
			afterDate.setUTCHours(afterDate.getUTCHours() + afterDate.getTimezoneOffset() / 60 + currentUserTimezoneOffset);
			if (query.ts) {
				query.ts.$gte = afterDate;
			} else {
				query.ts = {
					$gte: afterDate,
				};
			}
			return '';
		}

		function filterOnDate(_, day, month, year) {
			month--;
			const date = new Date(year, month, day);
			date.setUTCHours(date.getUTCHours() + date.getTimezoneOffset() / 60 + currentUserTimezoneOffset);
			const dayAfter = new Date(date);
			dayAfter.setDate(dayAfter.getDate() + 1);
			delete query.ts;
			query.ts = {
				$gte: date,
				$lt: dayAfter,
			};
			return '';
		}

		function filterLabel(_, tag) {
			query['attachments.0.labels'] = new RegExp(escapeRegExp(tag), 'i');
			return '';
		}

		function filterTitle(_, tag) {
			query['attachments.title'] = new RegExp(escapeRegExp(tag), 'i');
			return '';
		}

		function filterDescription(_, tag) {
			query['attachments.description'] = new RegExp(escapeRegExp(tag), 'i');
			return '';
		}

		function sortByTimestamp(_, direction) {
			if (direction.startsWith('asc')) {
				options.sort.ts = 1;
			} else if (direction.startsWith('desc')) {
				options.sort.ts = -1;
			}
			return '';
		}

		/*
		 text = 'from:rodrigo mention:gabriel chat'
		 */

		// Query for senders
		const from = [];
		text = text.replace(/from:([a-z0-9.-_]+)/gi, function (match, username) {
			if (username === 'me' && !from.includes(currentUserName)) {
				username = currentUserName;
			}
			from.push(username);
			return '';
		});

		if (from.length > 0) {
			query['u.username'] = {
				$regex: from.join('|'),
				$options: 'i',
			};
		}

		// Query for senders
		const mention = [];
		text = text.replace(/mention:([a-z0-9.-_]+)/gi, function (match, username) {
			mention.push(username);
			return '';
		});

		if (mention.length > 0) {
			query['mentions.username'] = {
				$regex: mention.join('|'),
				$options: 'i',
			};
		}

		// Filter on messages that are starred by the current user.
		text = text.replace(/has:star/g, filterStarred);
		// Filter on messages that have an url.
		text = text.replace(/has:url|has:link/g, filterUrl);
		// Filter on pinned messages.
		text = text.replace(/is:pinned|has:pin/g, filterPinned);
		// Filter on messages which have a location attached.
		text = text.replace(/has:location|has:map/g, filterLocation);
		// Filter image tags
		text = text.replace(/label:(\w+)/g, filterLabel);
		// Filter on description of messages.
		text = text.replace(/file-desc:(\w+)/g, filterDescription);
		// Filter on title of messages.
		text = text.replace(/file-title:(\w+)/g, filterTitle);
		// Filtering before/after/on a date
		// matches dd-MM-yyyy, dd/MM/yyyy, dd-MM-yyyy, prefixed by before:, after: and on: respectively.
		// Example: before:15/09/2016 after: 10-08-2016
		// if "on:" is set, "before:" and "after:" are ignored.
		text = text.replace(/before:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterBeforeDate);
		text = text.replace(/after:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterAfterDate);
		text = text.replace(/on:(\d{1,2})[\/\.-](\d{1,2})[\/\.-](\d{4})/g, filterOnDate);
		// Sort order
		text = text.replace(/(?:order|sort):(asc|ascend|ascending|desc|descend|descending)/g, sortByTimestamp);

		// Query in message text
		text = text.trim().replace(/\s\s/g, ' ');
		if (text !== '') {
			if (/^\/.+\/[imxs]*$/.test(text)) {
				const r = text.split('/');
				query.msg = {
					$regex: r[1],
					$options: r[2],
				};
			} else if (settings.get('Message_AlwaysSearchRegExp')) {
				query.msg = {
					$regex: text,
					$options: 'i',
				};
			} else {
				query.$text = {
					$search: text,
				};
				options.projection = {
					score: {
						$meta: 'textScore',
					},
				};
			}
		}

		if (Object.keys(query).length > 0) {
			query.t = {
				$ne: 'rm', // hide removed messages (useful when searching for user messages)
			};
			query._hidden = {
				$ne: true, // don't return _hidden messages
			};

			if (rid) {
				query.rid = rid;
			} else {
				query.rid = {
					$in: Subscriptions.findByUserId(user._id)
						.fetch()
						.map((subscription) => subscription.rid),
				};
			}

			if (!settings.get('Message_ShowEditedStatus')) {
				options.projection = {
					editedAt: 0,
				};
			}

			result.message.docs = Promise.await(
				Messages.find(query, {
					readPreference: readSecondaryPreferred(Messages.col.s.db),
					...options,
				}).toArray(),
			);
		}

		return result;
	},
});


import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { useRef, useEffect } from 'react';

import { drawDoughnutChart } from '../../../../../app/livechat/client/lib/chartHandler';
import { AsyncStatePhase } from '../../../../hooks/useAsyncState';
import { useEndpointData } from '../../../../hooks/useEndpointData';
import Chart from './Chart';
import { useUpdateChartData } from './useUpdateChartData';

const labels = ['Available', 'Away', 'Busy', 'Offline'];

const initialData = {
	available: 0,
	away: 0,
	busy: 0,
	offline: 0,
};

const init = (canvas, context, t) =>
	drawDoughnutChart(
		canvas,
		t('Agents'),
		context,
		labels.map((l) => t(l)),
		Object.values(initialData),
	);

const AgentStatusChart = ({ params, reloadRef, ...props }) => {
	const t = useTranslation();

	const canvas = useRef();
	const context = useRef();

	const updateChartData = useUpdateChartData({
		context,
		canvas,
		t,
		init,
	});

	const { value: data, phase: state, reload } = useEndpointData('/v1/livechat/analytics/dashboards/charts/agents-status', params);

	reloadRef.current.agentStatusChart = reload;

	const { offline = 0, available = 0, away = 0, busy = 0 } = data ?? initialData;

	useEffect(() => {
		const initChart = async () => {
			context.current = await init(canvas.current, context.current, t);
		};
		initChart();
	}, [t]);

	useEffect(() => {
		if (state === AsyncStatePhase.RESOLVED && context.current) {
			updateChartData(t('Offline'), [offline]);
			updateChartData(t('Available'), [available]);
			updateChartData(t('Away'), [away]);
			updateChartData(t('Busy'), [busy]);
		}
	}, [available, away, busy, offline, state, t, updateChartData]);

	return <Chart ref={canvas} {...props} />;
};

export default AgentStatusChart;


import { Field } from '@rocket.chat/fuselage';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';

import RelativeUrlSettingInput from './RelativeUrlSettingInput';

export default {
	title: 'Admin/Settings/Inputs/RelativeUrlSettingInput',
	component: RelativeUrlSettingInput,
	parameters: {
		actions: {
			argTypesRegex: '^on.*',
		},
	},
	decorators: [(fn) => <Field>{fn()}</Field>],
} as ComponentMeta<typeof RelativeUrlSettingInput>;

const Template: ComponentStory<typeof RelativeUrlSettingInput> = (args) => <RelativeUrlSettingInput {...args} />;

export const Default = Template.bind({});
Default.args = {
	_id: 'setting_id',
	label: 'Label',
	placeholder: 'Placeholder',
};

export const Disabled = Template.bind({});
Disabled.args = {
	_id: 'setting_id',
	label: 'Label',
	placeholder: 'Placeholder',
	disabled: true,
};

export const WithValue = Template.bind({});
WithValue.args = {
	_id: 'setting_id',
	label: 'Label',
	value: 'Value',
	placeholder: 'Placeholder',
};

export const WithResetButton = Template.bind({});
WithResetButton.args = {
	_id: 'setting_id',
	label: 'Label',
	placeholder: 'Placeholder',
	hasResetButton: true,
};


const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');

const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {


import chai from 'chai';
import chaiSpies from 'chai-spies';
import chaiDatetime from 'chai-datetime';
import chaiDom from 'chai-dom';

chai.use(chaiSpies);
chai.use(chaiDatetime);
chai.use(chaiDom);


.tabs {
	margin-bottom: 0.5rem;

	border-bottom: 2px solid var(--input-border-color);
}

.tabs-wrapper {
	display: flex;

	width: 100%;

	margin: 0 -1rem -2px;
}

.tab {
	display: flex;

	margin: 0 1rem;

	cursor: pointer;

	transition: all 0.3s;

	color: var(--rc-color-primary-light);

	border-bottom: 2px solid transparent;

	font-family: inherit;
	font-size: 1rem;
	font-weight: 500;
	line-height: 1.25rem;
	align-items: stretch;
	flex-flow: row nowrap;

	&.active {
		color: var(--rc-color-button-primary);
		border-bottom-color: var(--rc-color-button-primary);
	}

	&:focus {
		text-decoration: underline;
	}

	& > span {
		flex: 1;

		padding: 1rem 0;
	}
}


import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { LivechatTrigger } from '@rocket.chat/models';

import { hasPermission } from '../../../authorization/server';

Meteor.methods({
	async 'livechat:removeTrigger'(triggerId) {
		if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', {
				method: 'livechat:removeTrigger',
			});
		}

		check(triggerId, String);

		await LivechatTrigger.removeById(triggerId);

		return true;
	},
});


async openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {

const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToOpen(day, hour, undefined, {

keep keep keep keep keep replace keep keep keep keep


@keyframes loading {
	0% {
		transform: scale(0.7);

		opacity: 0;
	}

	50% {
		transform: scale(1);

		opacity: 1;
	}

	100% {
		transform: scale(0.7);

		opacity: 0;
	}
}

.rc-popout {
	min-width: 380px;
	max-width: 800px;

	transition: all 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);

	color: var(--rc-color-primary-lightest);

	border: solid 4px var(--rc-color-primary-darkest);
	background: var(--rc-color-primary-darkest);

	&-wrapper {
		position: absolute;
		z-index: 10;

		top: calc(var(--header-min-height) + 60px);
		left: calc(100% - 395px);

		display: flex;

		animation: dropdown-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);

		box-shadow: 3px 3px 5px 1px rgba(47, 52, 61, 0.08), 0 0 12px 0 rgba(47, 52, 61, 0.12);

		align-items: center;
		justify-content: center;

		-webkit-user-drag: element;
		-khtml-user-drag: element;
	}

	&__title {
		flex: 1 1 auto;

		font-size: 16px;
	}

	&--open {
		& .rc-popout__header {
			padding: 5px 10px 8px;

			& .rc-popout__controls {
				display: none;
			}
		}
	}

	&__close {
		cursor: pointer;
		transform: rotate(45deg);

		font-size: 20px;
	}

	&__minimize {
		cursor: pointer;
		transition: transform linear 0.2s;

		font-size: 20px;
	}

	&__header {
		display: flex;
		flex-direction: row;

		padding: 5px 10px;

		font-size: 21px;
		justify-content: flex-end;

		& .rc-popout__controls {
			padding: 0;
		}
	}

	&__content {
		display: flex;

		flex-direction: column;

		animation: dropdown-show 0.1s cubic-bezier(0.45, 0.05, 0.55, 0.95);
		align-items: stretch;

		& .youtube-player {
			width: inherit;
			height: inherit;
		}
	}

	&__controls {
		display: flex;
		flex: 10 1 auto;

		padding: 8px 10px 5px;

		& button {
			margin: 0 5px;

			font-size: 16px;

			&.preparing {
				animation: loading 2s infinite;
				pointer-events: none;
			}

			&.broadcasting {
				color: red;
			}
		}

		& span {
			text-transform: capitalize;

			font-size: 15px;
			line-height: 18px;
		}
	}

	&__content-icon {
		margin: 1.2rem auto;

		font-size: 6rem;

		&--popout-warning {
			color: var(--rc-color-alert);
		}

		&--popout-success {
			color: var(--rc-color-success-light);
		}

		&--popout-info {
			color: var(--rc-color-alert);
		}

		&--popout-error {
			color: var(--rc-color-error-light);
		}
	}

	&__content-text {
		text-align: center;
		word-break: break-word;

		font-size: 1rem;
		line-height: 1.5;
	}

	&__content-error {
		display: none;

		width: 100%;
		margin: 0.5rem 0;

		text-align: center;
		word-break: break-word;

		color: var(--rc-color-primary-lightest);
		background: var(--rc-color-error);

		line-height: 2;
	}

	&--closed {
		& .rc-popout__minimize {
			transform: rotate(180deg);
		}

		&.rc-popout__content {
			overflow: hidden;

			height: 0;

			& .streaming-object {
				visibility: 'hidden';

				height: 0;
			}
		}
	}

	& svg {
		fill: var(--rc-color-primary-lightest);
	}
}

@media (width <= 400px) {
	.rc-popout {
		top: initial !important;
		bottom: 0;
		left: 0 !important;

		width: 100%;
		min-width: 100%;
		max-width: 100%;
		margin: 0 16px;

		animation: dropup-show 0.3s cubic-bezier(0.45, 0.05, 0.55, 0.95);
	}
}


import { Badge, Box, Button, ButtonGroup, Icon, Margins, Throbber, Tabs } from '@rocket.chat/fuselage';
import { useDebouncedValue, useSafely } from '@rocket.chat/fuselage-hooks';
import { useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import React, { useEffect, useState, useMemo } from 'react';
import s from 'underscore.string';

import {
	ProgressStep,
	ImportWaitingStates,
	ImportFileReadyStates,
	ImportPreparingStartedStates,
	ImportingStartedStates,
	ImportingErrorStates,
} from '../../../../app/importer/lib/ImporterProgressStep';
import Page from '../../../components/Page';
import PrepareChannels from './PrepareChannels';
import PrepareUsers from './PrepareUsers';
import { useErrorHandler } from './useErrorHandler';

const waitFor = (fn, predicate) =>
	new Promise((resolve, reject) => {
		const callPromise = () => {
			fn().then((result) => {
				if (predicate(result)) {
					resolve(result);
					return;
				}

				setTimeout(callPromise, 1000);
			}, reject);
		};

		callPromise();
	});

function PrepareImportPage() {
	const t = useTranslation();
	const handleError = useErrorHandler();

	const [isPreparing, setPreparing] = useSafely(useState(true));
	const [progressRate, setProgressRate] = useSafely(useState(null));
	const [status, setStatus] = useSafely(useState(null));
	const [messageCount, setMessageCount] = useSafely(useState(0));
	const [users, setUsers] = useState([]);
	const [channels, setChannels] = useState([]);
	const [isImporting, setImporting] = useSafely(useState(false));

	const usersCount = useMemo(() => users.filter(({ do_import }) => do_import).length, [users]);
	const channelsCount = useMemo(() => channels.filter(({ do_import }) => do_import).length, [channels]);

	const importHistoryRoute = useRoute('admin-import');
	const newImportRoute = useRoute('admin-import-new');
	const importProgressRoute = useRoute('admin-import-progress');

	const getImportFileData = useEndpoint('GET', '/v1/getImportFileData');
	const getCurrentImportOperation = useEndpoint('GET', '/v1/getCurrentImportOperation');
	const startImport = useEndpoint('POST', '/v1/startImport');

	useEffect(() => {
		const streamer = new Meteor.Streamer('importers');

		const handleProgressUpdated = ({ rate }) => {
			setProgressRate(rate);
		};

		streamer.on('progress', handleProgressUpdated);

		return () => {
			streamer.removeListener('progress', handleProgressUpdated);
		};
	}, [setProgressRate]);

	useEffect(() => {
		const loadImportFileData = async () => {
			try {
				const data = await waitFor(getImportFileData, (data) => data && !data.waiting);

				if (!data) {
					handleError(t('Importer_not_setup'));
					importHistoryRoute.push();
					return;
				}

				if (data.step) {
					handleError(t('Failed_To_Load_Import_Data'));
					importHistoryRoute.push();
					return;
				}

				setMessageCount(data.message_count);
				setUsers(data.users.map((user) => ({ ...user, do_import: true })));
				setChannels(data.channels.map((channel) => ({ ...channel, do_import: true })));
				setPreparing(false);
				setProgressRate(null);
			} catch (error) {
				handleError(error, t('Failed_To_Load_Import_Data'));
				importHistoryRoute.push();
			}
		};

		const loadCurrentOperation = async () => {
			try {
				const { operation } = await waitFor(
					getCurrentImportOperation,
					({ operation }) => operation.valid && !ImportWaitingStates.includes(operation.status),
				);

				if (!operation.valid) {
					newImportRoute.push();
					return;
				}

				if (ImportingStartedStates.includes(operation.status)) {
					importProgressRoute.push();
					return;
				}

				if (
					operation.status === ProgressStep.USER_SELECTION ||
					ImportPreparingStartedStates.includes(operation.status) ||
					ImportFileReadyStates.includes(operation.status)
				) {
					setStatus(operation.status);
					loadImportFileData();
					return;
				}

				if (ImportingErrorStates.includes(operation.status)) {
					handleError(t('Import_Operation_Failed'));
					importHistoryRoute.push();
					return;
				}

				if (operation.status === ProgressStep.DONE) {
					importHistoryRoute.push();
					return;
				}

				handleError(t('Unknown_Import_State'));
				importHistoryRoute.push();
			} catch (error) {
				handleError(t('Failed_To_Load_Import_Data'));
				importHistoryRoute.push();
			}
		};

		loadCurrentOperation();
	}, [
		getCurrentImportOperation,
		getImportFileData,
		handleError,
		importHistoryRoute,
		importProgressRoute,
		newImportRoute,
		setMessageCount,
		setPreparing,
		setProgressRate,
		setStatus,
		t,
	]);

	const handleBackToImportsButtonClick = () => {
		importHistoryRoute.push();
	};

	const handleStartButtonClick = async () => {
		setImporting(true);

		try {
			await startImport({ input: { users, channels } });
			importProgressRoute.push();
		} catch (error) {
			handleError(error, t('Failed_To_Start_Import'));
			importHistoryRoute.push();
		}
	};

	const [tab, setTab] = useState('users');
	const handleTabClick = useMemo(() => (tab) => () => setTab(tab), []);

	const statusDebounced = useDebouncedValue(status, 100);

	const handleMinimumImportData = () =>
		!!((!usersCount && !channelsCount && !messageCount) || (!usersCount && !channelsCount && messageCount !== 0));

	return (
		<Page>
			<Page.Header title={t('Importing_Data')}>
				<ButtonGroup>
					<Button secondary onClick={handleBackToImportsButtonClick}>
						<Icon name='back' /> {t('Back_to_imports')}
					</Button>
					<Button primary disabled={isImporting || handleMinimumImportData()} onClick={handleStartButtonClick}>
						{t('Importer_Prepare_Start_Import')}
					</Button>
				</ButtonGroup>
			</Page.Header>

			<Page.ScrollableContentWithShadow>
				<Box marginInline='auto' marginBlock='x24' width='full' maxWidth='590px'>
					<Box is='h2' fontScale='p2m'>
						{statusDebounced && t(statusDebounced.replace('importer_', 'importer_status_'))}
					</Box>
					{!isPreparing && (
						<Tabs flexShrink={0}>
							<Tabs.Item selected={tab === 'users'} onClick={handleTabClick('users')}>
								{t('Users')} <Badge>{usersCount}</Badge>
							</Tabs.Item>
							<Tabs.Item selected={tab === 'channels'} onClick={handleTabClick('channels')}>
								{t('Channels')} <Badge>{channelsCount}</Badge>
							</Tabs.Item>
							<Tabs.Item disabled>
								{t('Messages')}
								<Badge>{messageCount}</Badge>
							</Tabs.Item>
						</Tabs>
					)}
					<Margins block='x24'>
						{isPreparing && (
							<>
								{progressRate ? (
									<Box display='flex' justifyContent='center' fontScale='p2'>
										<Box is='progress' value={(progressRate * 10).toFixed(0)} max='1000' marginInlineEnd='x24' />
										<Box is='span'>{s.numberFormat(progressRate, 0)}%</Box>
									</Box>
								) : (
									<Throbber justifyContent='center' />
								)}
							</>
						)}
						{!isPreparing && tab === 'users' && <PrepareUsers usersCount={usersCount} users={users} setUsers={setUsers} />}
						{!isPreparing && tab === 'channels' && (
							<PrepareChannels channels={channels} channelsCount={channelsCount} setChannels={setChannels} />
						)}
					</Margins>
				</Box>
			</Page.ScrollableContentWithShadow>
		</Page>
	);
}

export default PrepareImportPage;


import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import {
	useSetModal,
	useToastMessageDispatch,
	useUserId,
	useSetting,
	usePermission,
	useRole,
	useEndpoint,
	useMethod,
	useTranslation,
} from '@rocket.chat/ui-contexts';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import React, { useCallback, useState, useEffect } from 'react';

import { RoomManager } from '../../../../../../../app/ui-utils/client';
import PlaceChatOnHoldModal from '../../../../../../../ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal';
import CloseChatModal from '../../../../../../components/Omnichannel/modals/CloseChatModal';
import CloseChatModalData from '../../../../../../components/Omnichannel/modals/CloseChatModalData';
import ForwardChatModal from '../../../../../../components/Omnichannel/modals/ForwardChatModal';
import ReturnChatQueueModal from '../../../../../../components/Omnichannel/modals/ReturnChatQueueModal';
import TranscriptModal from '../../../../../../components/Omnichannel/modals/TranscriptModal';
import { useOmnichannelRouteConfig } from '../../../../../../hooks/omnichannel/useOmnichannelRouteConfig';
import { QuickActionsActionConfig, QuickActionsEnum } from '../../../../lib/QuickActions';
import { useQuickActionsContext } from '../../../../lib/QuickActions/QuickActionsContext';

export const useQuickActions = (
	room: IOmnichannelRoom,
): {
	visibleActions: QuickActionsActionConfig[];
	actionDefault: (e: unknown) => void;
	getAction: (id: string) => void;
} => {
	const setModal = useSetModal();

	const t = useTranslation();
	const dispatchToastMessage = useToastMessageDispatch();
	const context = useQuickActionsContext();
	const actions = (Array.from(context.actions.values()) as QuickActionsActionConfig[]).sort((a, b) => (a.order || 0) - (b.order || 0));

	const [onHoldModalActive, setOnHoldModalActive] = useState(false);

	const visitorRoomId = room.v._id;
	const rid = room._id;
	const uid = useUserId();
	const roomLastMessage = room.lastMessage;

	const getVisitorInfo = useEndpoint('GET', '/v1/livechat/visitors.info');

	const getVisitorEmail = useMutableCallback(async () => {
		if (!visitorRoomId) {
			return;
		}

		const {
			visitor: { visitorEmails },
		} = await getVisitorInfo({ visitorId: visitorRoomId });

		if (visitorEmails?.length && visitorEmails[0].address) {
			return visitorEmails[0].address;
		}
	});

	useEffect(() => {
		if (onHoldModalActive && roomLastMessage?.token) {
			setModal(null);
		}
	}, [roomLastMessage, onHoldModalActive, setModal]);

	const closeModal = useCallback(() => setModal(null), [setModal]);

	const closeOnHoldModal = useCallback(() => {
		closeModal();
		setOnHoldModalActive(false);
	}, [closeModal]);

	const methodReturn = useMethod('livechat:returnAsInquiry');

	const handleMoveChat = useCallback(async () => {
		try {
			await methodReturn(rid);
			closeModal();
			Session.set('openedRoom', null);
			FlowRouter.go('/home');
		} catch (error) {
			dispatchToastMessage({ type: 'error', message: error });
		}
	}, [closeModal, dispatchToastMessage, methodReturn, rid]);

	const requestTranscript = useMethod('livechat:requestTranscript');

	const handleRequestTranscript = useCallback(
		async (email: string, subject: string) => {
			try {
				await requestTranscript(rid, email, subject);
				closeModal();
				RoomManager.close(`l${rid}`);
				dispatchToastMessage({
					type: 'success',
					message: t('Livechat_transcript_has_been_requested'),
				});
			} catch (error) {
				dispatchToastMessage({ type: 'error', message: error });
			}
		},
		[closeModal, dispatchToastMessage, requestTranscript, rid, t],
	);

	const sendTranscript = useMethod('livechat:sendTranscript');

	const handleSendTranscript = useCallback(
		async (email: string, subject: string, token: string) => {
			try {
				await sendTranscript(token, rid, email, subject);
				closeModal();
			} catch (error) {
				dispatchToastMessage({ type: 'error', message: error });
			}
		},
		[closeModal, dispatchToastMessage, rid, sendTranscript],
	);

	const discardTranscript = useMethod('livechat:discardTranscript');

	const handleDiscardTranscript = useCallback(async () => {
		try {
			await discardTranscript(rid);
			dispatchToastMessage({
				type: 'success',
				message: t('Livechat_transcript_request_has_been_canceled'),
			});
			closeModal();
		} catch (error) {
			dispatchToastMessage({ type: 'error', message: error });
		}
	}, [closeModal, discardTranscript, dispatchToastMessage, rid, t]);

	const forwardChat = useEndpoint('POST', '/v1/livechat/room.forward');

	const handleForwardChat = useCallback(
		async (departmentId?: string, userId?: string, comment?: string) => {
			if (departmentId && userId) {
				return;
			}
			const transferData: {
				roomId: string;
				clientAction: boolean;
				comment?: string;
				departmentId?: string;
				userId?: string;
			} = {
				roomId: rid,
				comment,
				clientAction: true,
			};

			if (departmentId) {
				transferData.departmentId = departmentId;
			}
			if (userId) {
				transferData.userId = userId;
			}

			try {
				const result = await forwardChat(transferData);
				if (!result) {
					throw new Error(departmentId ? t('error-no-agents-online-in-department') : t('error-forwarding-chat'));
				}
				dispatchToastMessage({ type: 'success', message: t('Transferred') });
				FlowRouter.go('/');
				closeModal();
			} catch (error) {
				dispatchToastMessage({ type: 'error', message: error });
			}
		},
		[closeModal, dispatchToastMessage, forwardChat, rid, t],
	);

	const closeChat = useMethod('livechat:closeRoom');

	const handleClose = useCallback(
		async (comment?: string, tags?: string[]) => {
			try {
				await closeChat(rid, comment, { clientAction: true, tags });
				closeModal();
				dispatchToastMessage({ type: 'success', message: t('Chat_closed_successfully') });
			} catch (error) {
				dispatchToastMessage({ type: 'error', message: error });
			}
		},
		[closeChat, closeModal, dispatchToastMessage, rid, t],
	);

	const onHoldChat = useEndpoint('POST', '/v1/livechat/room.onHold');

	const handleOnHoldChat = useCallback(async () => {
		try {
			await onHoldChat({ roomId: rid });
			closeModal();
			dispatchToastMessage({ type: 'success', message: t('Chat_On_Hold_Successfully') });
		} catch (error) {
			dispatchToastMessage({ type: 'error', message: error });
		}
	}, [onHoldChat, rid, closeModal, dispatchToastMessage, t]);

	const openModal = useMutableCallback(async (id: string) => {
		switch (id) {
			case QuickActionsEnum.MoveQueue:
				setModal(<ReturnChatQueueModal onMoveChat={handleMoveChat} onCancel={closeModal} />);
				break;
			case QuickActionsEnum.Transcript:
				const visitorEmail = await getVisitorEmail();

				if (!visitorEmail) {
					dispatchToastMessage({ type: 'error', message: t('Customer_without_registered_email') });
					break;
				}

				setModal(
					<TranscriptModal
						room={room}
						email={visitorEmail}
						onRequest={handleRequestTranscript}
						onSend={handleSendTranscript}
						onDiscard={handleDiscardTranscript}
						onCancel={closeModal}
					/>,
				);
				break;
			case QuickActionsEnum.ChatForward:
				setModal(<ForwardChatModal room={room} onForward={handleForwardChat} onCancel={closeModal} />);
				break;
			case QuickActionsEnum.CloseChat:
				setModal(
					room.departmentId ? (
						<CloseChatModalData departmentId={room.departmentId} onConfirm={handleClose} onCancel={closeModal} />
					) : (
						<CloseChatModal onConfirm={handleClose} onCancel={closeModal} />
					),
				);
				break;
			case QuickActionsEnum.OnHoldChat:
				setModal(<PlaceChatOnHoldModal onOnHoldChat={handleOnHoldChat} onCancel={closeOnHoldModal} />);
				setOnHoldModalActive(true);
				break;
			default:
				break;
		}
	});

	const omnichannelRouteConfig = useOmnichannelRouteConfig();

	const manualOnHoldAllowed = useSetting('Livechat_allow_manual_on_hold');

	const hasManagerRole = useRole('livechat-manager');

	const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole) && room?.lastMessage?.t !== 'livechat-close';
	const canMoveQueue = !!omnichannelRouteConfig?.returnQueue && room?.u !== undefined;
	const canForwardGuest = usePermission('transfer-livechat-guest');
	const canSendTranscript = usePermission('send-omnichannel-chat-transcript');
	const canCloseRoom = usePermission('close-livechat-room');
	const canCloseOthersRoom = usePermission('close-others-livechat-room');
	const canPlaceChatOnHold = Boolean(!room.onHold && room.u && !(room as any).lastMessage?.token && manualOnHoldAllowed);

	const hasPermissionButtons = (id: string): boolean => {
		switch (id) {
			case QuickActionsEnum.MoveQueue:
				return !!roomOpen && canMoveQueue;
			case QuickActionsEnum.ChatForward:
				return !!roomOpen && canForwardGuest;
			case QuickActionsEnum.Transcript:
				return canSendTranscript;
			case QuickActionsEnum.CloseChat:
				return !!roomOpen && (canCloseRoom || canCloseOthersRoom);
			case QuickActionsEnum.OnHoldChat:
				return !!roomOpen && canPlaceChatOnHold;
			default:
				break;
		}
		return false;
	};

	const visibleActions = actions.filter(({ id }) => hasPermissionButtons(id));

	const actionDefault = useMutableCallback((e) => {
		const index = e.currentTarget.getAttribute('data-quick-actions');
		const { id } = visibleActions[index];
		openModal(id);
	});

	const getAction = useMutableCallback((id) => {
		openModal(id);
	});

	return { visibleActions, actionDefault, getAction };
};


async closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {

const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, undefined, {

keep keep keep keep replace keep keep keep


import { Template } from 'meteor/templating';

import './messagePopupChannel.html';
import { roomCoordinator } from '../../../../client/lib/rooms/roomCoordinator';

Template.messagePopupChannel.helpers({
	channelIcon() {
		return roomCoordinator.getIcon(this);
	},
});


import './style.css';

export const createHexColorPreviewMessageRenderer = () => (message) => {
	if (!message.html?.trim()) {
		return message;
	}

	const regex = /(?:^|\s|\n)(#[A-Fa-f0-9]{3}([A-Fa-f0-9]{3})?)\b/g;

	message.html = message.html.replace(regex, (match, completeColor) =>
		match.replace(
			completeColor,
			`<div class="message-color"><div class="message-color-sample" style="background-color:${completeColor}"></div>${completeColor.toUpperCase()}</div>`,
		),
	);
	return message;
};


// in many scenario's like, if the agent is created when a business is active,

// or if a normal user is converted to agent when a business hour is active

keep keep keep keep replace replace keep keep keep keep keep

const allActiveBusinessHoursForEntireWeek = await this.BusinessHourRepository.findActiveBusinessHours({


import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';

import { LivechatRooms } from '../../../../models/server';
import { API } from '../../../../api/server';
import { findLivechatTransferHistory } from '../lib/transfer';

API.v1.addRoute(
	'livechat/transfer.history/:rid',
	{ authRequired: true },
	{
		get() {
			check(this.urlParams, {
				rid: String,
			});

			const { rid } = this.urlParams;

			const room = LivechatRooms.findOneById(rid, { _id: 1 });
			if (!room) {
				throw new Meteor.Error('invalid-room');
			}

			const { offset, count } = this.getPaginationItems();
			const { sort } = this.parseJsonQuery();

			const history = Promise.await(
				findLivechatTransferHistory({
					userId: this.userId,
					rid,
					pagination: {
						offset,
						count,
						sort,
					},
				}),
			);

			return API.v1.success(history);
		},
	},
);


import { HttpBridge } from '@rocket.chat/apps-engine/server/bridges/HttpBridge';
import type { IHttpResponse } from '@rocket.chat/apps-engine/definition/accessors';
import type { IHttpBridgeRequestInfo } from '@rocket.chat/apps-engine/server/bridges';

import type { AppServerOrchestrator } from '../orchestrator';
import { fetch } from '../../../../server/lib/http/fetch';

const isGetOrHead = (method: string): boolean => ['GET', 'HEAD'].includes(method.toUpperCase());

export class AppHttpBridge extends HttpBridge {
	// eslint-disable-next-line no-empty-function
	constructor(private readonly orch: AppServerOrchestrator) {
		super();
	}

	protected async call(info: IHttpBridgeRequestInfo): Promise<IHttpResponse> {
		// begin comptability with old HTTP.call API
		const url = new URL(info.url);

		const { request, method } = info;

		const { headers = {} } = request;

		let { content } = request;

		if (!content && typeof request.data === 'object') {
			content = JSON.stringify(request.data);
			headers['Content-Type'] = 'application/json';
		}

		if (request.auth) {
			if (request.auth.indexOf(':') < 0) {
				throw new Error('auth option should be of the form "username:password"');
			}

			const base64 = Buffer.from(request.auth, 'ascii').toString('base64');
			headers.Authorization = `Basic ${base64}`;
		}

		let paramsForBody;

		if (content || isGetOrHead(method)) {
			if (request.params) {
				Object.keys(request.params).forEach((key) => {
					if (request.params?.[key]) {
						url.searchParams.append(key, request.params?.[key]);
					}
				});
			}
		} else {
			paramsForBody = request.params;
		}

		if (paramsForBody) {
			const data = new URLSearchParams();
			Object.entries(paramsForBody).forEach(([key, value]) => {
				data.append(key, value);
			});
			content = data.toString();
			headers['Content-Type'] = 'application/x-www-form-urlencoded';
		}

		if (isGetOrHead(method)) {
			content = undefined;
		}

		// end comptability with old HTTP.call API

		this.orch.debugLog(`The App ${info.appId} is requesting from the outter webs:`, info);

		try {
			const response = await fetch(
				url.href,
				{
					method,
					body: content,
					headers,
				},
				(request.hasOwnProperty('strictSSL') && !request.strictSSL) ||
					(request.hasOwnProperty('rejectUnauthorized') && request.rejectUnauthorized),
			);

			const result: IHttpResponse = {
				url: info.url,
				method: info.method,
				statusCode: response.status,
				headers: Object.fromEntries(response.headers as unknown as any),
			};

			const body = Buffer.from(await response.arrayBuffer());

			if (request.encoding === null) {
				/**
				 * The property `content` is not appropriately typed in the
				 * Apps-engine definition, and we can't simply change it there
				 * as it would be a breaking change. Thus, we're left with this
				 * type assertion.
				 */
				result.content = body as any;
			} else {
				result.content = body.toString(request.encoding as BufferEncoding);
				result.data = ((): any => {
					const contentType = (response.headers.get('content-type') || '').split(';')[0];
					if (!['application/json', 'text/javascript', 'application/javascript', 'application/x-javascript'].includes(contentType)) {
						return null;
					}

					try {
						return JSON.parse(result.content);
					} catch {
						return null;
					}
				})();
			}

			return result;
		} catch (e: any) {
			return e.response;
		}
	}
}


const openedBusinessHours = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);

bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. No opened business hour found`);

keep keep keep keep replace keep keep keep keep

const openedBusinessHours = await filterBusinessHoursThatMustBeOpenedByDay(allActiveBusinessHoursForEntireWeek, day);


/**
 * @todo rename it as isAbsoluteURL
 */
export const isURL = (str: string): boolean => /^(https?:\/\/|data:)/.test(str);


import { Meteor } from 'meteor/meteor';

import { getStatusText } from '../../../lib';

Meteor.methods({
	getUserStatusText(userId) {
		const currentUserId = Meteor.userId();
		if (!currentUserId) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUserStatusText' });
		}

		return getStatusText(userId);
	},
});


import { Settings } from '@rocket.chat/models';

import { retrieveRegistrationStatus } from './retrieveRegistrationStatus';

export async function unregisterWorkspace() {
	const { workspaceRegistered } = retrieveRegistrationStatus();
	if (!workspaceRegistered) {
		return true;
	}

	await Promise.all([
		Settings.updateValueById('Cloud_Workspace_Id', null),
		Settings.updateValueById('Cloud_Workspace_Name', null),
		Settings.updateValueById('Cloud_Workspace_Client_Id', null),
		Settings.updateValueById('Cloud_Workspace_Client_Secret', null),
		Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', null),
		Settings.updateValueById('Cloud_Workspace_PublicKey', null),
		Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', null),
	]);

	return true;
}


// check if this agent has any departments that is connected to any non-default business hour

// if no such departments found then check default BH and if it is active, then allow the agent to change service status

const hasAtLeastOneDepartmentWithNonDefaultBH = departments.some(({ businessHourId }) => !!businessHourId);

const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT);

const hasAtLeastOneDepartmentWithNonDefaultBH = departments.some(({ businessHourId }) => {

return businessHourId && allActiveBusinessHoursForEntireWeek.findIndex(({ _id }) => _id === businessHourId) !== -1;

import type { FindOptions } from 'mongodb';
import type { ILivechatBusinessHour, LivechatBusinessHourTypes } from '@rocket.chat/core-typings';

import type { IBaseModel } from './IBaseModel';

export interface IWorkHoursCronJobsItem {
	day: string;
	times: string[];
}

export interface IWorkHoursCronJobsWrapper {
	start: IWorkHoursCronJobsItem[];
	finish: IWorkHoursCronJobsItem[];
}

export interface ILivechatBusinessHoursModel extends IBaseModel<ILivechatBusinessHour> {
	findOneDefaultBusinessHour(options?: undefined): Promise<ILivechatBusinessHour | null>;
	findOneDefaultBusinessHour(options: FindOptions<ILivechatBusinessHour>): Promise<ILivechatBusinessHour | null>;
	findOneDefaultBusinessHour<P>(options: FindOptions<P extends ILivechatBusinessHour ? ILivechatBusinessHour : P>): Promise<P | null>;
	findOneDefaultBusinessHour<P>(options?: any): Promise<ILivechatBusinessHour | P | null>;
	findActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]>;
	findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]>;
	insertOne(data: Omit<ILivechatBusinessHour, '_id'>): Promise<any>;
	findHoursToScheduleJobs(): Promise<IWorkHoursCronJobsWrapper[]>;

	findActiveBusinessHoursToOpen(
		day: string,
		start: string,
		type?: LivechatBusinessHourTypes,
		options?: any,
	): Promise<ILivechatBusinessHour[]>;

	findActiveBusinessHoursToClose(
		day: string,
		finish: string,
		type?: LivechatBusinessHourTypes,
		options?: any,
	): Promise<ILivechatBusinessHour[]>;
}


import { useUserPreference, useSetting } from '@rocket.chat/ui-contexts';
import moment from 'moment';
import { useCallback } from 'react';

const dayFormat = ['h:mm A', 'H:mm'] as const;

export const useFormatTime = (): ((input: moment.MomentInput) => string) => {
	const clockMode = useUserPreference<1 | 2>('clockMode');
	const format = useSetting('Message_TimeFormat') as string;
	const sameDay = clockMode !== undefined ? dayFormat[clockMode - 1] : format;

	return useCallback(
		(time) => {
			switch (clockMode) {
				case 1:
				case 2:
					return moment(time).format(sameDay);

				default:
					return moment(time).format(format);
			}
		},
		[clockMode, format, sameDay],
	);
};


@use 'sass:math';

@import '../../../../styles/colors';
@import '../../../../styles/helpers';
@import '../../../../styles/variables';

.uikit-button {
	position: relative;

	display: flex;
	flex-direction: row;

	box-sizing: border-box;
	padding: 2px 6px;

	cursor: pointer;
	user-select: none;
	transition:
		color $default-time-animation,
		background-color $default-time-animation,
		border-color $default-time-animation,
		transform math.div($default-time-animation, 2);
	white-space: nowrap;
	text-decoration: none;

	color: $color-text-grey;

	border: $default-border solid;

	border-color: $bg-color-grey;
	border-radius: (2 * $default-border-radius);
	outline: none;
	background-color: $bg-color-white;

	font-family: $font-family;
	font-size: 0.875rem;
	font-weight: normal;
	line-height: 1.25rem;
	align-items: center;
	justify-content: center;

	@include pressable-button(2px, $default-border);

	&:focus {
		box-shadow: 0 0 5px rgba(#000000, 0.5);
	}

	&::after {
		position: relative;
		left: 4px;

		visibility: hidden;

		box-sizing: border-box;

		width: 0;
		height: 0;
		margin-right: 2px;

		content: "";

		transition:
			opacity math.div($default-time-animation, 2),
			border-width math.div($default-time-animation, 2),
			width math.div($default-time-animation, 2),
			height math.div($default-time-animation, 2),
			visibility math.div($default-time-animation, 2);
		animation: button-loading-rotation 1s linear infinite;

		opacity: 0;
		border-width: 0;

		border-style: solid;

		border-color:
			currentColor
			currentColor
			currentColor
			transparent;
		border-radius: 50%;
	}

	&:hover:not(:disabled) {
		color: $color-text-dark;
		background-color: $bg-color-grey;
	}

	&:disabled {
		cursor: default;

		opacity: $disabled-opacity;

		&::before {
			cursor: default;
		}
	}

	&--style-primary {
		color: var(--color, $color-blue);

		&:hover:not(:disabled) {
			color: $color-text-lighter;
			border-color: var(--color, $color-blue);
			background-color: var(--color, $color-blue);
		}
	}

	&--style-danger {
		color: $color-dark-red;

		&:hover:not(:disabled) {
			color: $color-text-lighter;
			border-color: $color-dark-red;
			background-color: $color-dark-red;
		}
	}
}


import React, { FC } from 'react';

import Section from './InfoPanelSection';

const InfoPanelAvatar: FC = ({ children }) => (
	<Section display='flex' justifyContent='center'>
		{children}
	</Section>
);

export default InfoPanelAvatar;


keep keep keep keep keep replace replace keep keep keep keep keep

const agentIdsWithoutDepartment: string[] = [];

const agentIdsToRemoveCurrentBusinessHour: string[] = [];

import moment from 'moment';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { ILivechatDepartment, ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatDepartmentAgents } from '@rocket.chat/models';

import type { IBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import { AbstractBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour';
import { filterBusinessHoursThatMustBeOpened } from '../../../../../app/livechat/server/business-hour/Helper';
import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper';
import { bhLogger } from '../lib/logger';

interface IBusinessHoursExtraProperties extends ILivechatBusinessHour {
	timezoneName: string;
	departmentsToApplyBusinessHour: string;
}

export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior {
	constructor() {
		super();
		this.onAddAgentToDepartment = this.onAddAgentToDepartment.bind(this);
		this.onRemoveAgentFromDepartment = this.onRemoveAgentFromDepartment.bind(this);
		this.onRemoveDepartment = this.onRemoveDepartment.bind(this);
	}

	async onStartBusinessHours(): Promise<void> {
		await this.UsersRepository.removeBusinessHoursFromAllUsers();
		await this.UsersRepository.updateLivechatStatusBasedOnBusinessHours();
		const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
		const day = currentTime.format('dddd');
		const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {
			fields: {
				workHours: 1,
				timezone: 1,
				type: 1,
				active: 1,
			},
		});
		const businessHoursToOpen = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
		for (const businessHour of businessHoursToOpen) {
			this.openBusinessHour(businessHour);
		}
	}

	async openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
		const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToOpen(day, hour, undefined, {
			fields: {
				_id: 1,
				type: 1,
			},
		});
		for (const businessHour of businessHours) {
			this.openBusinessHour(businessHour);
		}
	}

	async closeBusinessHoursByDayAndHour(day: string, hour: string): Promise<void> {
		const businessHours = await this.BusinessHourRepository.findActiveBusinessHoursToClose(day, hour, undefined, {
			fields: {
				_id: 1,
				type: 1,
			},
		});
		for (const businessHour of businessHours) {
			this.closeBusinessHour(businessHour);
		}
	}

	async afterSaveBusinessHours(businessHourData: IBusinessHoursExtraProperties): Promise<void> {
		const departments = businessHourData.departmentsToApplyBusinessHour?.split(',').filter(Boolean);
		const currentDepartments = businessHourData.departments?.map((dept: any) => dept._id);
		const toRemove = [...(currentDepartments || []).filter((dept: Record<string, any>) => !departments.includes(dept._id))];
		await this.removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(businessHourData._id, toRemove);
		const businessHour = await this.BusinessHourRepository.findOneById(businessHourData._id);
		if (!businessHour) {
			return;
		}
		const businessHourIdToOpen = (await filterBusinessHoursThatMustBeOpened([businessHour])).map((businessHour) => businessHour._id);
		if (!businessHourIdToOpen.length) {
			return closeBusinessHour(businessHour);
		}
		return openBusinessHour(businessHour);
	}

	async onAddAgentToDepartment(options: Record<string, any> = {}): Promise<any> {
		const { departmentId, agentsId } = options;
		const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(departmentId, {
			projection: { businessHourId: 1 },
		});
		if (!department || !agentsId.length) {
			return options;
		}
		const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
		if (!defaultBusinessHour) {
			return options;
		}
		await removeBusinessHourByAgentIds(agentsId, defaultBusinessHour._id);
		if (!department.businessHourId) {
			return options;
		}
		const businessHour = await this.BusinessHourRepository.findOneById(department.businessHourId);
		if (!businessHour) {
			return options;
		}
		const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([businessHour]);
		if (!businessHourToOpen.length) {
			return options;
		}
		await this.UsersRepository.addBusinessHourByAgentIds(agentsId, businessHour._id);
		return options;
	}

	async onRemoveAgentFromDepartment(options: Record<string, any> = {}): Promise<any> {
		const { departmentId, agentsId } = options;
		const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, 'businessHourId'>>(departmentId, {
			projection: { businessHourId: 1 },
		});
		if (!department || !agentsId.length) {
			return options;
		}
		return this.handleRemoveAgentsFromDepartments(department, agentsId, options);
	}

	async onRemoveDepartment(options: Record<string, any> = {}): Promise<any> {
		const { department, agentsIds } = options;
		if (!department || !agentsIds?.length) {
			return options;
		}
		const deletedDepartment = LivechatDepartment.trashFindOneById(department._id);
		return this.handleRemoveAgentsFromDepartments(deletedDepartment, agentsIds, options);
	}

	async allowAgentChangeServiceStatus(agentId: string): Promise<boolean> {
		const isWithinBushinessHours = await this.UsersRepository.isAgentWithinBusinessHours(agentId);
		if (isWithinBushinessHours) {
			return true;
		}

		bhLogger.debug(`No active business hour found for agent with id: ${agentId} based on user's cache. Attempting to recheck the status`);

		// double check to see if user is actually within business hours
		// this is required since the cache of businessHour Ids we maintain within user's collection might be stale
		// in many scenario's like, if the agent is created when a business is active,
		// or if a normal user is converted to agent when a business hour is active
		const currentTime = moment.utc(moment().utc().format('dddd:HH:mm'), 'dddd:HH:mm');
		const day = currentTime.format('dddd');
		const activeBusinessHours = await this.BusinessHourRepository.findActiveAndOpenBusinessHoursByDay(day, {
			fields: {
				workHours: 1,
				timezone: 1,
				type: 1,
				active: 1,
			},
		});
		const openedBusinessHours = await filterBusinessHoursThatMustBeOpened(activeBusinessHours);
		if (!openedBusinessHours.length) {
			bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. No opened business hour found`);
			return false;
		}

		const agentDepartments = await LivechatDepartmentAgents.find(
			{ departmentEnabled: true, agentId },
			{ projection: { agentId: 1, departmentId: 1 } },
		).toArray();

		if (agentDepartments.length) {
			// check if any one these departments have a opened business hour linked to it
			const departments = await LivechatDepartment.findInIds(
				agentDepartments.map(({ departmentId }) => departmentId),
				{ projection: { _id: 1, businessHourId: 1 } },
			).toArray();

			const departmentsWithActiveBH = departments.filter(
				({ businessHourId }) => businessHourId && openedBusinessHours.findIndex(({ _id }) => _id === businessHourId) !== -1,
			);

			if (!departmentsWithActiveBH.length) {
				bhLogger.debug(
					`No opened business hour found for any of the departments connected to the agent with id: ${agentId}. Now, checking if the default business hour can be used`,
				);

				// check if this agent has any departments that is connected to any non-default business hour
				// if no such departments found then check default BH and if it is active, then allow the agent to change service status
				const hasAtLeastOneDepartmentWithNonDefaultBH = departments.some(({ businessHourId }) => !!businessHourId);
				if (!hasAtLeastOneDepartmentWithNonDefaultBH) {
					const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT);
					if (isDefaultBHActive?._id) {
						await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId);

						bhLogger.debug(`Business hour status recheck passed for agentId: ${agentId}. Found default business hour to be active`);
						return true;
					}
					bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. Found default business hour to be inactive`);
				}
				return false;
			}

			const activeBusinessHoursForAgent = departmentsWithActiveBH.map(({ businessHourId }) => businessHourId);
			await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId(activeBusinessHoursForAgent, agentId);

			bhLogger.debug(
				`Business hour status recheck passed for agentId: ${agentId}. Found following business hours to be active:`,
				activeBusinessHoursForAgent,
			);
			return true;
		}

		// check if default businessHour is active
		const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT);
		if (isDefaultBHActive?._id) {
			await this.UsersRepository.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId);

			bhLogger.debug(`Business hour status recheck passed for agentId: ${agentId}. Found default business hour to be active`);
			return true;
		}

		bhLogger.debug(`Business hour status recheck failed for agentId: ${agentId}. No opened business hour found`);

		return false;
	}

	private async handleRemoveAgentsFromDepartments(department: Record<string, any>, agentsIds: string[], options: any): Promise<any> {
		const agentIdsWithoutDepartment = [];
		const agentIdsToRemoveCurrentBusinessHour = [];
		for await (const agentId of agentsIds) {
			if ((await LivechatDepartmentAgents.findByAgentId(agentId).count()) === 0) {
				agentIdsWithoutDepartment.push(agentId);
			}
			if (!(await LivechatDepartmentAgents.findAgentsByAgentIdAndBusinessHourId(agentId, department.businessHourId)).length) {
				// eslint-disable-line no-await-in-loop
				agentIdsToRemoveCurrentBusinessHour.push(agentId);
			}
		}
		if (department.businessHourId) {
			await removeBusinessHourByAgentIds(agentIdsToRemoveCurrentBusinessHour, department.businessHourId);
		}
		if (!agentIdsWithoutDepartment.length) {
			return options;
		}
		const defaultBusinessHour = await this.BusinessHourRepository.findOneDefaultBusinessHour();
		if (!defaultBusinessHour) {
			return options;
		}
		const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]);
		if (!businessHourToOpen.length) {
			return options;
		}
		await this.UsersRepository.addBusinessHourByAgentIds(agentIdsWithoutDepartment, defaultBusinessHour._id);
		return options;
	}

	private async openBusinessHour(businessHour: Record<string, any>): Promise<void> {
		return openBusinessHour(businessHour);
	}

	private async removeBusinessHourFromRemovedDepartmentsUsersIfNeeded(
		businessHourId: string,
		departmentsToRemove: string[],
	): Promise<void> {
		if (!departmentsToRemove.length) {
			return;
		}
		const agentIds = (await LivechatDepartmentAgents.findByDepartmentIds(departmentsToRemove).toArray()).map((dept: any) => dept.agentId);
		await removeBusinessHourByAgentIds(agentIds, businessHourId);
	}

	private async closeBusinessHour(businessHour: Record<string, any>): Promise<void> {
		closeBusinessHour(businessHour);
	}
}


import { States, StatesIcon, StatesTitle, Pagination } from '@rocket.chat/fuselage';
import { useMediaQuery, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, useMemo, MutableRefObject, useState, useEffect } from 'react';

import FilterByText from '../../../../components/FilterByText';
import {
	GenericTable,
	GenericTableHeader,
	GenericTableHeaderCell,
	GenericTableBody,
	GenericTableLoadingTable,
} from '../../../../components/GenericTable';
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination';
import { useSort } from '../../../../components/GenericTable/hooks/useSort';
import { useEndpointData } from '../../../../hooks/useEndpointData';
import { AsyncStatePhase } from '../../../../lib/asyncState';
import UsersTableRow from './UsersTableRow';

type UsersTableProps = {
	reload: MutableRefObject<() => void>;
};

const UsersTable = ({ reload }: UsersTableProps): ReactElement | null => {
	const t = useTranslation();
	const usersRoute = useRoute('admin-users');
	const mediaQuery = useMediaQuery('(min-width: 1024px)');
	const [text, setText] = useState('');
	const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination();
	const { sortBy, sortDirection, setSort } = useSort<'name' | 'username' | 'emails.address' | 'status'>('name');

	const query = useDebouncedValue(
		useMemo(
			() => ({
				fields: JSON.stringify({
					name: 1,
					username: 1,
					emails: 1,
					roles: 1,
					status: 1,
					avatarETag: 1,
					active: 1,
				}),
				query: JSON.stringify({
					$or: [
						{ 'emails.address': { $regex: text || '', $options: 'i' } },
						{ username: { $regex: text || '', $options: 'i' } },
						{ name: { $regex: text || '', $options: 'i' } },
					],
				}),
				sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`,
				count: itemsPerPage,
				offset: current,
			}),
			[text, itemsPerPage, current, sortBy, sortDirection],
		),
		500,
	);

	const { value, phase, reload: reloadList } = useEndpointData('/v1/users.list', query);

	useEffect(() => {
		reload.current = reloadList;
	}, [reload, reloadList]);

	const handleClick = useMutableCallback((id): void =>
		usersRoute.push({
			context: 'info',
			id,
		}),
	);

	const headers = useMemo(
		() => [
			<GenericTableHeaderCell w='x200' key='name' direction={sortDirection} active={sortBy === 'name'} onClick={setSort} sort='name'>
				{t('Name')}
			</GenericTableHeaderCell>,
			mediaQuery && (
				<GenericTableHeaderCell
					w='x140'
					key='username'
					direction={sortDirection}
					active={sortBy === 'username'}
					onClick={setSort}
					sort='username'
				>
					{t('Username')}
				</GenericTableHeaderCell>
			),
			<GenericTableHeaderCell
				w='x120'
				key='email'
				direction={sortDirection}
				active={sortBy === 'emails.address'}
				onClick={setSort}
				sort='emails.address'
			>
				{t('Email')}
			</GenericTableHeaderCell>,
			mediaQuery && (
				<GenericTableHeaderCell w='x120' key='roles' onClick={setSort}>
					{t('Roles')}
				</GenericTableHeaderCell>
			),
			<GenericTableHeaderCell w='x100' key='status' direction={sortDirection} active={sortBy === 'status'} onClick={setSort} sort='status'>
				{t('Status')}
			</GenericTableHeaderCell>,
		],
		[mediaQuery, setSort, sortBy, sortDirection, t],
	);

	if (phase === AsyncStatePhase.REJECTED) {
		return null;
	}

	return (
		<>
			<FilterByText placeholder={t('Search_Users')} onChange={({ text }): void => setText(text)} />
			{phase === AsyncStatePhase.LOADING && (
				<GenericTable>
					<GenericTableHeader>{headers}</GenericTableHeader>
					<GenericTableBody>{phase === AsyncStatePhase.LOADING && <GenericTableLoadingTable headerCells={5} />}</GenericTableBody>
				</GenericTable>
			)}
			{value?.users && value.users.length > 0 && phase === AsyncStatePhase.RESOLVED && (
				<>
					<GenericTable>
						<GenericTableHeader>{headers}</GenericTableHeader>
						<GenericTableBody>
							{value?.users.map((user) => (
								<UsersTableRow key={user._id} onClick={handleClick} mediaQuery={mediaQuery} user={user} />
							))}
						</GenericTableBody>
					</GenericTable>
					<Pagination
						divider
						current={current}
						itemsPerPage={itemsPerPage}
						count={value?.total || 0}
						onSetItemsPerPage={onSetItemsPerPage}
						onSetCurrent={onSetCurrent}
						{...paginationProps}
					/>
				</>
			)}
			{phase === AsyncStatePhase.RESOLVED && value?.users.length === 0 && (
				<States>
					<StatesIcon name='magnifier' />
					<StatesTitle>{t('No_results_found')}</StatesTitle>
				</States>
			)}
		</>
	);
};

export default UsersTable;


import { Session } from '../Session';

export class ClientSession extends Session {
	async init(): Promise<string> {
		const sodium = await this.sodium();

		const clientKeypair = await sodium.crypto_box_keypair();
		this.secretKey = await sodium.crypto_box_secretkey(clientKeypair);
		this.publicKey = await sodium.crypto_box_publickey(clientKeypair);

		return this.publicKey.toString(this.stringFormatKey);
	}

	async setServerKey(serverPublic: string): Promise<void> {
		const sodium = await this.sodium();

		const [decryptKey, encryptKey] = await sodium.crypto_kx_client_session_keys(
			this.publicKey,
			this.secretKey,
			this.publicKeyFromString(serverPublic),
		);

		this.decryptKey = decryptKey;
		this.encryptKey = encryptKey;
	}
}


import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Accounts } from 'meteor/accounts-base';
import { ReactiveVar } from 'meteor/reactive-var';
import { Tracker } from 'meteor/tracker';
import { HTTP } from 'meteor/http';

import { settings } from '../../../settings';

export class IframeLogin {
	constructor() {
		this.enabled = false;
		this.reactiveIframeUrl = new ReactiveVar();
		this.reactiveEnabled = new ReactiveVar();
		this.iframeUrl = undefined;
		this.apiUrl = undefined;
		this.apiMethod = undefined;

		Tracker.autorun((c) => {
			this.enabled = settings.get('Accounts_iframe_enabled');
			this.reactiveEnabled.set(this.enabled);

			this.iframeUrl = settings.get('Accounts_iframe_url');
			this.apiUrl = settings.get('Accounts_Iframe_api_url');
			this.apiMethod = settings.get('Accounts_Iframe_api_method');

			if (this.enabled === false) {
				return c.stop();
			}

			if (this.enabled === true && this.iframeUrl && this.apiUrl && this.apiMethod) {
				c.stop();
				if (!Accounts._storedLoginToken()) {
					this.tryLogin(() => {});
				}
			}
		});
	}

	tryLogin(callback) {
		if (!this.enabled) {
			return;
		}

		if (!this.iframeUrl || !this.apiUrl || !this.apiMethod) {
			return;
		}

		console.log('tryLogin');
		const options = {
			beforeSend: (xhr) => {
				xhr.withCredentials = true;
			},
		};

		let { iframeUrl } = this;
		let separator = '?';
		if (iframeUrl.indexOf('?') > -1) {
			separator = '&';
		}

		if (navigator.userAgent.indexOf('Electron') > -1) {
			iframeUrl += `${separator}client=electron`;
		}

		HTTP.call(this.apiMethod, this.apiUrl, options, (error, result) => {
			console.log(error, result);
			if (result && result.data && (result.data.token || result.data.loginToken)) {
				this.loginWithToken(result.data, (error, result) => {
					if (error) {
						this.reactiveIframeUrl.set(iframeUrl);
					} else {
						this.reactiveIframeUrl.set();
					}
					callback(error, result);
				});
			} else {
				this.reactiveIframeUrl.set(iframeUrl);
				callback(error, result);
			}
		});
	}

	loginWithToken(tokenData, callback) {
		if (!this.enabled) {
			return;
		}

		if (Match.test(tokenData, String)) {
			tokenData = {
				token: tokenData,
			};
		}

		console.log('loginWithToken');

		if (tokenData.loginToken) {
			return Meteor.loginWithToken(tokenData.loginToken, callback);
		}

		Accounts.callLoginMethod({
			methodArguments: [
				{
					iframe: true,
					token: tokenData.token,
				},
			],
			userCallback: callback,
		});
	}
}

export const iframeLogin = new IframeLogin();


findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]> {

findActiveBusinessHours(options: FindOptions<ILivechatBusinessHour> = {}): Promise<ILivechatBusinessHour[]> {


import { clientLogger } from '../lib/logger';
import { FederationRoomEvents, Subscriptions, Users } from '../../../models/server';
import { normalizers } from '../normalizers';
import { deleteRoom } from '../../../lib/server/functions';
import { getFederationDomain } from '../lib/getFederationDomain';
import { dispatchEvents } from '../handler';
import { checkRoomType, checkRoomDomainsLength } from '../functions/helpers';

export async function doAfterCreateRoom(room, users, subscriptions) {
	const normalizedUsers = [];

	//
	// Add user events
	//
	const addUserEvents = [];

	for (const user of users) {
		/* eslint-disable no-await-in-loop */

		const subscription = subscriptions[user._id];

		const normalizedSourceUser = normalizers.normalizeUser(user);
		const normalizedSourceSubscription = normalizers.normalizeSubscription(subscription);

		normalizedUsers.push(normalizedSourceUser);

		const addUserEvent = await FederationRoomEvents.createAddUserEvent(
			getFederationDomain(),
			room._id,
			normalizedSourceUser,
			normalizedSourceSubscription,
		);

		addUserEvents.push(addUserEvent);

		/* eslint-enable no-await-in-loop */
	}

	//
	// Genesis
	//

	// Normalize room
	const normalizedRoom = normalizers.normalizeRoom(room, normalizedUsers);

	// Check if the number of domains is allowed
	if (!checkRoomDomainsLength(normalizedRoom.federation.domains)) {
		throw new Error(`Cannot federate rooms with more than ${process.env.FEDERATED_DOMAINS_LENGTH || 10} domains`);
	}

	// Ensure a genesis event for this room
	const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom);

	// Dispatch the events
	await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]);
}

async function afterCreateRoom(roomOwner, room) {
	// If the room is federated, ignore
	if (room.federation) {
		return roomOwner;
	}

	// Find all subscriptions of this room
	let subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch();
	subscriptions = subscriptions.reduce((acc, s) => {
		acc[s.u._id] = s;

		return acc;
	}, {});

	// Get all user ids
	const userIds = Object.keys(subscriptions);

	// Load all the users
	const users = Users.findUsersWithUsernameByIds(userIds).fetch();

	// Check if there is a federated user on this room
	const hasFederatedUser = users.find((u) => u.username.indexOf('@') !== -1);

	// If there are not federated users on this room, ignore it
	if (!hasFederatedUser) {
		return roomOwner;
	}

	try {
		// If the room is not on the allowed types, ignore
		if (!checkRoomType(room)) {
			throw new Error('Channels cannot be federated');
		}

		clientLogger.debug({ msg: 'afterCreateRoom', roomOwner, room });

		await doAfterCreateRoom(room, users, subscriptions);
	} catch (err) {
		deleteRoom(room._id);

		clientLogger.error({ msg: 'afterCreateRoom => Could not create federated room:', err });
	}

	return room;
}

export const definition = {
	hook: 'afterCreateRoom',
	callback: (roomOwner, room) => Promise.await(afterCreateRoom(roomOwner, room)),
	id: 'federation-after-create-room',
};


import { Box } from '@rocket.chat/fuselage';
import colors from '@rocket.chat/fuselage-tokens/colors.json';
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import React, { ReactElement } from 'react';

export const SidebarFooterWatermark = (): ReactElement | null => {
	const t = useTranslation();
	const isEnterpriseEdition = useEndpoint('GET', '/v1/licenses.isEnterprise');
	const result = useQuery(['licenses.isEnterprise'], () => isEnterpriseEdition(), {
		refetchOnWindowFocus: false,
	});

	if (!result.isSuccess || result.isLoading || result.data.isEnterprise) {
		return null;
	}

	return (
		<Box pi='x16' pbe='x8'>
			<Box is='a' href='https://rocket.chat/' target='_blank' rel='noopener noreferrer'>
				<Box fontScale='micro' color='hint' pbe='x4'>
					{t('Powered_by_RocketChat')}
				</Box>
				<Box fontScale='micro' color={colors.n100} pbe='x4'>
					{t('Free_Edition')}
				</Box>
			</Box>
		</Box>
	);
};


import { IRoom, isRoomFederated, IUser } from '@rocket.chat/core-typings';
import { Box, Icon } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { escapeHTML } from '@rocket.chat/string-helpers';
import { usePermission, useSetModal, useTranslation, useUser, useUserRoom } from '@rocket.chat/ui-contexts';
import React, { useMemo } from 'react';

import * as Federation from '../../../../../../app/federation-v2/client/Federation';
import GenericModal from '../../../../../components/GenericModal';
import { useEndpointActionExperimental } from '../../../../../hooks/useEndpointActionExperimental';
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';
import { Action } from '../../../../hooks/useActionSpread';
import RemoveUsersModal from '../../../../teams/contextualBar/members/RemoveUsersModal';
import { getRoomDirectives } from '../../../lib/getRoomDirectives';

// TODO: Remove endpoint concatenation
export const useRemoveUserAction = (user: Pick<IUser, '_id' | 'username'>, rid: IRoom['_id'], reload?: () => void): Action | undefined => {
	const t = useTranslation();
	const room = useUserRoom(rid);
	const currentUser = useUser();
	const { _id: uid } = user;

	if (!room) {
		throw Error('Room not provided');
	}

	const hasPermissionToRemove = usePermission('remove-user', rid);
	const userCanRemove = isRoomFederated(room)
		? Federation.isEditableByTheUser(currentUser || undefined, room) && hasPermissionToRemove
		: hasPermissionToRemove;
	const setModal = useSetModal();
	const closeModal = useMutableCallback(() => setModal(null));
	const roomName = room?.t && escapeHTML(roomCoordinator.getRoomName(room.t, room));

	const endpointPrefix = room.t === 'p' ? '/v1/groups' : '/v1/channels';
	const { roomCanRemove } = getRoomDirectives(room);

	const removeFromTeam = useEndpointActionExperimental('POST', '/v1/teams.removeMember', t('User_has_been_removed_from_team'));
	const removeFromRoom = useEndpointActionExperimental('POST', `${endpointPrefix}.kick`, t('User_has_been_removed_from_s', roomName));

	const removeUserOptionAction = useMutableCallback(() => {
		const handleRemoveFromTeam = async (rooms: IRoom[]): Promise<void> => {
			if (room.teamId) {
				const roomKeys = Object.keys(rooms);
				await removeFromTeam({
					teamId: room.teamId,
					userId: uid,
					...(roomKeys.length && { rooms: roomKeys }),
				});
				closeModal();
				reload?.();
			}
		};

		const handleRemoveFromRoom = async (rid: IRoom['_id'], uid: IUser['_id']): Promise<void> => {
			await removeFromRoom({ roomId: rid, userId: uid });
			closeModal();
			reload?.();
		};

		if (room.teamMain && room.teamId) {
			return setModal(
				<RemoveUsersModal teamId={room?.teamId} userId={uid} onClose={closeModal} onCancel={closeModal} onConfirm={handleRemoveFromTeam} />,
			);
		}

		setModal(
			<GenericModal
				variant='danger'
				confirmText={t('Yes_remove_user')}
				onClose={closeModal}
				onCancel={closeModal}
				onConfirm={(): Promise<void> => handleRemoveFromRoom(rid, uid)}
			>
				{t('The_user_will_be_removed_from_s', roomName)}
			</GenericModal>,
		);
	});

	const removeUserOption = useMemo(
		() =>
			roomCanRemove && userCanRemove
				? {
						label: (
							<Box color='danger'>
								<Icon mie='x4' name='cross' size='x20' />
								{room?.teamMain ? t('Remove_from_team') : t('Remove_from_room')}
							</Box>
						),
						action: removeUserOptionAction,
				  }
				: undefined,
		[room, roomCanRemove, userCanRemove, removeUserOptionAction, t],
	);

	return removeUserOption;
};


export interface ILivechatBusinessHoursModel extends IBaseModel<ILivechatBusinessHour> {

findOneDefaultBusinessHour(options?: undefined): Promise<ILivechatBusinessHour | null>;

findOneDefaultBusinessHour(options: FindOptions<ILivechatBusinessHour>): Promise<ILivechatBusinessHour | null>;

findOneDefaultBusinessHour<P>(options: FindOptions<P extends ILivechatBusinessHour ? ILivechatBusinessHour : P>): Promise<P | null>;

import moment from 'moment';
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import { LivechatBusinessHours, Users } from '@rocket.chat/models';

import { createDefaultBusinessHourRow } from '../../../models/server/models/LivechatBusinessHours';

export const filterBusinessHoursThatMustBeOpened = async (
	businessHours: ILivechatBusinessHour[],
): Promise<Pick<ILivechatBusinessHour, '_id' | 'type'>[]> => {
	const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm');

	return businessHours
		.filter(
			(businessHour) =>
				businessHour.active &&
				businessHour.workHours
					.filter((hour) => hour.open)
					.some((hour) => {
						const localTimeStart = moment(`${hour.start.cron.dayOfWeek}:${hour.start.cron.time}`, 'dddd:HH:mm');
						const localTimeFinish = moment(`${hour.finish.cron.dayOfWeek}:${hour.finish.cron.time}`, 'dddd:HH:mm');
						return currentTime.isSameOrAfter(localTimeStart) && currentTime.isSameOrBefore(localTimeFinish);
					}),
		)
		.map((businessHour) => ({
			_id: businessHour._id,
			type: businessHour.type,
		}));
};

export const openBusinessHourDefault = async (): Promise<void> => {
	await Users.removeBusinessHoursFromAllUsers();
	const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm');
	const day = currentTime.format('dddd');
	const activeBusinessHours = await LivechatBusinessHours.findDefaultActiveAndOpenBusinessHoursByDay(day, {
		fields: {
			workHours: 1,
			timezone: 1,
			type: 1,
			active: 1,
		},
	});
	const businessHoursToOpenIds = (await filterBusinessHoursThatMustBeOpened(activeBusinessHours)).map((businessHour) => businessHour._id);
	await Users.openAgentsBusinessHoursByBusinessHourId(businessHoursToOpenIds);
	await Users.updateLivechatStatusBasedOnBusinessHours();
};

export const createDefaultBusinessHourIfNotExists = async (): Promise<void> => {
	if ((await LivechatBusinessHours.find({ type: LivechatBusinessHourTypes.DEFAULT }).count()) === 0) {
		await LivechatBusinessHours.insertOne(createDefaultBusinessHourRow());
	}
};


import type { IMessage } from '@rocket.chat/core-typings';

export type FollowMessageMethod = (options: { mid: IMessage['_id'] }) => false | undefined;


import { Settings } from '@rocket.chat/models';

import { addMigration } from '../../lib/migrations';

addMigration({
	version: 275,
	async up() {
		await Settings.deleteMany({
			_id: {
				$in: [
					'bigbluebutton_Enabled',
					'bigbluebutton_server',
					'bigbluebutton_sharedSecret',
					'bigbluebutton_Open_New_Window',
					'bigbluebutton_enable_d',
					'bigbluebutton_enable_p',
					'bigbluebutton_enable_c',
					'bigbluebutton_enable_teams',
					'Jitsi_Enabled',
					'Jitsi_Domain',
					'Jitsi_URL_Room_Prefix',
					'Jitsi_URL_Room_Suffix',
					'Jitsi_URL_Room_Hash',
					'Jitsi_SSL',
					'Jitsi_Open_New_Window',
					'Jitsi_Enable_Channels',
					'Jitsi_Enable_Teams',
					'Jitsi_Chrome_Extension',
					'Jitsi_Enabled_TokenAuth',
					'Jitsi_Application_ID',
					'Jitsi_Application_Secret',
					'Jitsi_Limit_Token_To_Room',
					'Video Conference',
					'Jitsi',
					'BigBlueButton',
				],
			},
		});
	},
});


import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Box, Button, Icon, Tag } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { FC, memo, MouseEvent, useState } from 'react';

import { useScopeDict } from '../../../hooks/useScopeDict';

const Item: FC<{
	data: IOmnichannelCannedResponse & { departmentName: ILivechatDepartment['name'] };
	onClickItem: (e: MouseEvent<HTMLOrSVGElement>) => void;
	onClickUse: (e: MouseEvent<HTMLOrSVGElement>, text: string) => void;
}> = ({ data, onClickItem, onClickUse }) => {
	const t = useTranslation();

	const scope = useScopeDict(data.scope, data.departmentName);

	const clickable = css`
		cursor: pointer;
	`;

	const [visibility, setVisibility] = useState(false);

	return (
		<Box
			pbs={16}
			pbe={12}
			pi={24}
			borderBlockEndWidth='2px'
			borderBlockEndColor='neutral-200'
			borderBlockEndStyle='solid'
			onClick={onClickItem}
			className={clickable}
			onMouseEnter={(): void => setVisibility(true)}
			onMouseLeave={(): void => setVisibility(false)}
		>
			<Box display='flex' flexDirection='row' justifyContent='space-between' alignItems='center'>
				<Box w='full' minWidth={0}>
					<Box fontScale='p2m' withTruncatedText>
						!{data.shortcut}
					</Box>
					<Box fontScale='c1' color='info' withTruncatedText>
						{scope}
					</Box>
				</Box>
				<Box display='flex' flexDirection='row' alignItems='center'>
					<Button
						display={visibility ? 'block' : 'none'}
						small
						onClick={(e): void => {
							onClickUse(e, data.text);
						}}
					>
						{t('Use')}
					</Button>
					<Icon name='chevron-left' size={24} color='neutral-700' />
				</Box>
			</Box>
			<Box fontScale='p2' mbs='8px' color='info' withTruncatedText>
				"{data.text}"
			</Box>
			{data.tags && data.tags.length > 0 && (
				<Box display='flex' w='full' flexDirection='row' mbs='8px' flexWrap='wrap'>
					{data.tags.map((tag: string, idx: number) => (
						<Box key={idx} mie='4px' mbe='4px'>
							<Tag>{tag}</Tag>
						</Box>
					))}
				</Box>
			)}
		</Box>
	);
};

export default memo(Item);


findOneDefaultBusinessHour<P>(options?: any): Promise<ILivechatBusinessHour | P | null>;

findActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]>;

findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]>;

insertOne(data: Omit<ILivechatBusinessHour, '_id'>): Promise<any>;

findActiveAndOpenBusinessHoursByDay(day: string, options?: FindOptions<ILivechatBusinessHour>): Promise<ILivechatBusinessHour[]>;

import type { ILivechatBusinessHoursModel } from '@rocket.chat/model-typings';
import type { Collection, Db, FindOptions } from 'mongodb';
import type { ILivechatBusinessHour, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';

import { BaseRaw } from './BaseRaw';

export interface IWorkHoursCronJobsItem {
	day: string;
	times: string[];
}

export interface IWorkHoursCronJobsWrapper {
	start: IWorkHoursCronJobsItem[];
	finish: IWorkHoursCronJobsItem[];
}

export class LivechatBusinessHoursRaw extends BaseRaw<ILivechatBusinessHour> implements ILivechatBusinessHoursModel {
	constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<ILivechatBusinessHour>>) {
		super(db, 'livechat_business_hours', trash);
	}

	async findOneDefaultBusinessHour(options?: undefined): Promise<ILivechatBusinessHour | null>;

	async findOneDefaultBusinessHour(options: FindOptions<ILivechatBusinessHour>): Promise<ILivechatBusinessHour | null>;

	async findOneDefaultBusinessHour<P>(options: FindOptions<P extends ILivechatBusinessHour ? ILivechatBusinessHour : P>): Promise<P | null>;

	findOneDefaultBusinessHour<P>(options?: any): Promise<ILivechatBusinessHour | P | null> {
		return this.findOne({ type: LivechatBusinessHourTypes.DEFAULT }, options);
	}

	findActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]> {
		return this.find(
			{
				active: true,
				workHours: {
					$elemMatch: {
						$or: [{ 'start.cron.dayOfWeek': day }, { 'finish.cron.dayOfWeek': day }],
						open: true,
					},
				},
			},
			options,
		).toArray();
	}

	findDefaultActiveAndOpenBusinessHoursByDay(day: string, options?: any): Promise<ILivechatBusinessHour[]> {
		return this.find(
			{
				type: LivechatBusinessHourTypes.DEFAULT,
				active: true,
				workHours: {
					$elemMatch: {
						$or: [{ 'start.cron.dayOfWeek': day, 'finish.cron.dayOfWeek': day }],
						open: true,
					},
				},
			},
			options,
		).toArray();
	}

	async insertOne(data: Omit<ILivechatBusinessHour, '_id'>): Promise<any> {
		return super.insertOne({
			...data,
			ts: new Date(),
		});
	}

	findHoursToScheduleJobs(): Promise<IWorkHoursCronJobsWrapper[]> {
		return this.col
			.aggregate([
				{
					$facet: {
						start: [
							{ $match: { active: true } },
							{ $project: { _id: 0, workHours: 1 } },
							{ $unwind: { path: '$workHours' } },
							{ $match: { 'workHours.open': true } },
							{
								$group: {
									_id: { day: '$workHours.start.cron.dayOfWeek' },
									times: { $addToSet: '$workHours.start.cron.time' },
								},
							},
							{
								$project: {
									_id: 0,
									day: '$_id.day',
									times: 1,
								},
							},
						],
						finish: [
							{ $match: { active: true } },
							{ $project: { _id: 0, workHours: 1 } },
							{ $unwind: { path: '$workHours' } },
							{ $match: { 'workHours.open': true } },
							{
								$group: {
									_id: { day: '$workHours.finish.cron.dayOfWeek' },
									times: { $addToSet: '$workHours.finish.cron.time' },
								},
							},
							{
								$project: {
									_id: 0,
									day: '$_id.day',
									times: 1,
								},
							},
						],
					},
				},
			])
			.toArray() as any;
	}

	async findActiveBusinessHoursToOpen(
		day: string,
		start: string,
		type?: LivechatBusinessHourTypes,
		options?: any,
	): Promise<ILivechatBusinessHour[]> {
		const query: Record<string, any> = {
			active: true,
			workHours: {
				$elemMatch: {
					'start.cron.dayOfWeek': day,
					'start.cron.time': start,
					'open': true,
				},
			},
		};
		if (type) {
			query.type = type;
		}
		return this.col.find(query, options).toArray();
	}

	async findActiveBusinessHoursToClose(
		day: string,
		finish: string,
		type?: LivechatBusinessHourTypes,
		options?: any,
	): Promise<ILivechatBusinessHour[]> {
		const query: Record<string, any> = {
			active: true,
			workHours: {
				$elemMatch: {
					'finish.cron.dayOfWeek': day,
					'finish.cron.time': finish,
					'open': true,
				},
			},
		};
		if (type) {
			query.type = type;
		}
		return this.col.find(query, options).toArray();
	}
}


import { assert } from 'chai';
import { isTeamsRemoveMemberProps } from '@rocket.chat/rest-typings';

describe('Teams (definition/rest/v1)', () => {
	describe('isTeamsRemoveMemberProps', () => {
		it('should be a function', () => {
			assert.isFunction(isTeamsRemoveMemberProps);
		});
		it('should return false if parameter is empty', () => {
			assert.isFalse(isTeamsRemoveMemberProps({}));
		});
		it('should return false if teamId is is informed but missing userId', () => {
			assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId' }));
		});
		it('should return false if teamName is is informed but missing userId', () => {
			assert.isFalse(isTeamsRemoveMemberProps({ teamName: 'teamName' }));
		});

		it('should return true if teamId and userId are informed', () => {
			assert.isTrue(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId' }));
		});
		it('should return true if teamName and userId are informed', () => {
			assert.isTrue(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId' }));
		});

		it('should return false if teamName and userId are informed but rooms are empty', () => {
			assert.isFalse(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId', rooms: [] }));
		});

		it('should return true if teamName and userId are informed but rooms are undefined', () => {
			assert.isTrue(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId', rooms: undefined }));
		});

		it('should return false if teamId and userId are informed and rooms are empty', () => {
			assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [] }));
		});

		it('should return true if teamId and userId are informed but rooms are undefined', () => {
			assert.isTrue(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: undefined }));
		});

		it('should return false if teamId and userId are informed but rooms are empty', () => {
			assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [] }));
		});

		it('should return true if teamId and userId are informed and rooms are informed', () => {
			assert.isTrue(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: ['room'] }));
		});

		it('should return false if teamId and userId are informed and rooms are informed but rooms is not an array of strings', () => {
			assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [123] }));
		});

		it('should return false if teamName and userId are informed and rooms are informed but there is an extra property', () => {
			assert.isFalse(
				isTeamsRemoveMemberProps({
					teamName: 'teamName',
					userId: 'userId',
					rooms: ['room'],
					extra: 'extra',
				}),
			);
		});

		it('should return false if teamId and userId are informed and rooms are informed but there is an extra property', () => {
			assert.isFalse(
				isTeamsRemoveMemberProps({
					teamId: 'teamId',
					userId: 'userId',
					rooms: ['room'],
					extra: 'extra',
				}),
			);
		});

		it('should return false if teamName and userId are informed and rooms are informed but there is an extra property', () => {
			assert.isFalse(
				isTeamsRemoveMemberProps({
					teamName: 'teamName',
					userId: 'userId',
					rooms: ['room'],
					extra: 'extra',
				}),
			);
		});
	});
});