import {
	INodeProperties,
} from 'n8n-workflow';

export const categoryOperations = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		description: 'Choose an operation',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'category',
				],
			},
		},
		options: [
			{
				name: 'Create',
				value: 'create',
				description: 'Create a category',
			},
			{
				name: 'Get All',
				value: 'getAll',
				description: 'Get all categories',
			},
			{
				name: 'Update',
				value: 'update',
				description: 'Update a category',
			},
		],
		default: 'create',
	},
] as INodeProperties[];

export const categoryFields: INodeProperties[] = [
	/* -------------------------------------------------------------------------- */
	/*                                category:create                             */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Name',
		name: 'name',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'create',
				],
			},
		},
		default: '',
		description: 'Name of the category.',
	},
	{
		displayName: 'Color',
		name: 'color',
		type: 'color',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'create',
				],
			},
		},
		default: '0000FF',
		description: 'Color of the category.',
	},
	{
		displayName: 'Text Color',
		name: 'textColor',
		type: 'color',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'create',
				],
			},
		},
		default: '0000FF',
		description: 'Text color of the category.',
	},

	/* -------------------------------------------------------------------------- */
	/*                                category:getAll                             */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'getAll',
				],
			},
		},
		default: false,
		description: 'If all results should be returned or only up to a given limit.',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'getAll',
				],
				returnAll: [
					false,
				],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 100,
		},
		default: 50,
		description: 'How many results to return.',
	},

	/* -------------------------------------------------------------------------- */
	/*                                category:update                             */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Category ID',
		name: 'categoryId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'update',
				],
			},
		},
		default: '',
		description: 'ID of the category.',
	},
	{
		displayName: 'Name',
		name: 'name',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'update',
				],
			},
		},
		default: '',
		description: 'New name of the category.',
	},
	{
		displayName: 'Update Fields',
		name: 'updateFields',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: [
					'category',
				],
				operation: [
					'update',
				],
			},
		},
		options: [
			{
				displayName: 'Color',
				name: 'color',
				type: 'color',
				default: '0000FF',
				description: 'Color of the category',
			},
			{
				displayName: 'Text Color',
				name: 'textColor',
				type: 'color',
				default: '0000FF',
				description: 'Text color of the category',
			},
		],
	},
];


body.company = { id: additionalFields.companyId as number };

body.addresses = keysToSnakeCase(addresses) as IAddress[];

body.opt_in_reason = additionalFields.optInReason as string;


import { INodeProperties } from 'n8n-workflow';

export const invoiceOperations = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		displayOptions: {
			show: {
				resource: [
					'invoice',
				],
			},
		},
		options: [
			{
				name: 'Create',
				value: 'create',
				description: 'Create a new invoice',
			},
			{
				name: 'Delete',
				value: 'delete',
				description: 'Delete a invoice',
			},
			{
				name: 'Email',
				value: 'email',
				description: 'Email an invoice',
			},
			{
				name: 'Get',
				value: 'get',
				description: 'Get data of a invoice',
			},
			{
				name: 'Get All',
				value: 'getAll',
				description: 'Get data of all invoices',
			},
		],
		default: 'create',
		description: 'The operation to perform.',
	},
] as INodeProperties[];

export const invoiceFields = [
/* -------------------------------------------------------------------------- */
/*                                 invoice:create                             */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				operation: [
					'create',
				],
				resource: [
					'invoice',
				],
			},
		},
		options: [
			{
				displayName: 'Client',
				name: 'client',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getClients',
				},
				default: '',
			},
			{
				displayName: 'Auto Bill',
				name: 'autoBill',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Custom Value 1',
				name: 'customValue1',
				type: 'number',
				typeOptions: {
					minValue: 0,
				},
				default: 0,
			},
			{
				displayName: 'Custom Value 2',
				name: 'customValue2',
				type: 'number',
				typeOptions: {
					minValue: 0,
				},
				default: 0,
			},
			{
				displayName: 'Discount',
				name: 'discount',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Due Date',
				name: 'dueDate',
				type: 'dateTime',
				default: '',
			},
			{
				displayName: 'Email',
				name: 'email',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Email Invoice',
				name: 'emailInvoice',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Invoice Date',
				name: 'invoiceDate',
				type: 'dateTime',
				default: '',
			},
			{
				displayName: 'Invoice Number',
				name: 'invoiceNumber',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Invoice Status',
				name: 'invoiceStatus',
				type: 'options',
				options: [
					{
						name: 'Draft',
						value: 1,
					},
					{
						name: 'Sent',
						value: 2,
					},
				],
				default: 1,
			},
			{
				displayName: 'Is Amount Discount',
				name: 'isAmountDiscount',
				type: 'boolean',
				default: false,
			},
			{
				displayName: 'Paid',
				name: 'paid',
				type: 'number',
				default: 0,
			},
			{
				displayName: 'Partial',
				name: 'partial',
				type: 'number',
				default: 0,
			},
			{
				displayName: 'Partial Due Date',
				name: 'partialDueDate',
				type: 'dateTime',
				default: '',
			},
			{
				displayName: 'PO Number',
				name: 'poNumber',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Private Notes',
				name: 'privateNotes',
				type: 'string',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				default: '',
			},
			{
				displayName: 'Public Notes',
				name: 'publicNotes',
				type: 'string',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				default: '',
			},
			{
				displayName: 'Tax Name 1',
				name: 'taxName1',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Tax Name 2',
				name: 'taxName2',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Tax Rate 1',
				name: 'taxRate1',
				type: 'number',
				default: 0,
			},
			{
				displayName: 'Tax Rate 2',
				name: 'taxRate2',
				type: 'number',
				default: 0,
			},
		],
	},
	{
		displayName: 'Invoice Items',
		name: 'invoiceItemsUi',
		placeholder: 'Add Invoice Item',
		type: 'fixedCollection',
		typeOptions: {
			multipleValues: true,
		},
		displayOptions: {
			show: {
				resource: [
					'invoice',
				],
				operation: [
					'create',
				],
			},
		},
		default: {},
		options: [
			{
				name: 'invoiceItemsValues',
				displayName: 'Invoice Item',
				values: [
					{
						displayName: 'Cost',
						name: 'cost',
						type: 'number',
						default: 0,
					},
					{
						displayName: 'Description',
						name: 'description',
						type: 'string',
						typeOptions: {
							alwaysOpenEditWindow: true,
						},
						default: '',
					},
					{
						displayName: 'Service',
						name: 'service',
						typeOptions: {
							alwaysOpenEditWindow: true,
						},
						type: 'string',
						default: '',
					},
					{
						displayName: 'Hours',
						name: 'hours',
						type: 'number',
						typeOptions: {
							minValue: 0,
						},
						default: 0,
					},
					{
						displayName: 'Tax Name 1',
						name: 'taxName1',
						type: 'string',
						default: '',
					},
					{
						displayName: 'Tax Name 2',
						name: 'taxName2',
						type: 'string',
						default: '',
					},
					{
						displayName: 'Tax Rate 1',
						name: 'taxRate1',
						type: 'number',
						default: 0,
					},
					{
						displayName: 'Tax Rate 2',
						name: 'taxRate2',
						type: 'number',
						default: 0,
					},
				],
			},
		],
	},
/* -------------------------------------------------------------------------- */
/*                                 invoice:delete                             */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Invoice ID',
		name: 'invoiceId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'invoice',
				],
				operation: [
					'delete',
				],
			},
		},
	},
/* -------------------------------------------------------------------------- */
/*                                  invoice:email                             */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Invoice ID',
		name: 'invoiceId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'invoice',
				],
				operation: [
					'email',
				],
			},
		},
	},
/* -------------------------------------------------------------------------- */
/*                                  invoice:get                               */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Invoice ID',
		name: 'invoiceId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				resource: [
					'invoice',
				],
				operation: [
					'get',
				],
			},
		},
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				operation: [
					'get',
				],
				resource: [
					'invoice',
				],
			},
		},
		options: [
			{
				displayName: 'Include',
				name: 'include',
				type: 'options',
				options: [
					{
						name: 'Client',
						value: 'client',
					},
				],
				default: 'client',
			},
		],
	},
/* -------------------------------------------------------------------------- */
/*                                  invoice:getAll                            */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				resource: [
					'invoice',
				],
				operation: [
					'getAll',
				],
			},
		},
		default: false,
		description: 'If all results should be returned or only up to a given limit.',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				resource: [
					'invoice',
				],
				operation: [
					'getAll',
				],
				returnAll: [
					false,
				],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 60,
		},
		default: 50,
		description: 'How many results to return.',
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				operation: [
					'getAll',
				],
				resource: [
					'invoice',
				],
			},
		},
		options: [
			{
				displayName: 'Invoice Number',
				name: 'invoiceNumber',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Include',
				name: 'include',
				type: 'options',
				options: [
					{
						name: 'Client',
						value: 'client',
					},
				],
				default: 'client',
			},
		],
	},
] as INodeProperties[];


body.email_addresses = emails as IEmailContact[];

keep keep replace replace replace replace replace keep keep keep

import {
	OptionsWithUri,
 } from 'request';

import {
	IExecuteFunctions,
	IHookFunctions,
	ILoadOptionsFunctions,
	IWebhookFunctions,
} from 'n8n-core';

import {
	IDataObject, NodeApiError
} from 'n8n-workflow';

import {
	snakeCase,
 } from 'change-case';

export async function keapApiRequest(this: IWebhookFunctions | IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
	let options: OptionsWithUri = {
		headers: {
			'Content-Type': 'application/json',
		},
		method,
		body,
		qs,
		uri: uri || `https://api.infusionsoft.com/crm/rest/v1${resource}`,
		json: true,
	};
	try {
		options = Object.assign({}, options, option);
		if (Object.keys(headers).length !== 0) {
			options.headers = Object.assign({}, options.headers, headers);
		}
		if (Object.keys(body).length === 0) {
			delete options.body;
		}
		//@ts-ignore
		return await this.helpers.requestOAuth2.call(this, 'keapOAuth2Api', options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

export async function keapApiRequestAllItems(this: IHookFunctions| IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any

	const returnData: IDataObject[] = [];

	let responseData;
	let uri: string | undefined;
	query.limit = 50;

	do {
		responseData = await keapApiRequest.call(this, method, endpoint, body, query, uri);
		uri = responseData.next;
		returnData.push.apply(returnData, responseData[propertyName]);
	} while (
		returnData.length < responseData.count
	);

	return returnData;
}

export function keysToSnakeCase(elements: IDataObject[] | IDataObject) : IDataObject[] {
	if (!Array.isArray(elements)) {
		elements = [elements];
	}
	for (const element of elements) {
		for (const key of Object.keys(element)) {
			if (key !== snakeCase(key)) {
				element[snakeCase(key)] = element[key];
				delete element[key];
			}
		}
	}
	return elements;
}


import {
	URL,
} from 'url';

import {
	sign,
} from 'aws4';

import {
	get,
} from 'lodash';

import {
	OptionsWithUri,
} from 'request';

import {
	parseString,
} from 'xml2js';

import {
	IExecuteFunctions,
	IHookFunctions,
	ILoadOptionsFunctions,
	IWebhookFunctions,
} from 'n8n-core';

import {
	IDataObject,
	NodeApiError,
	NodeOperationError,
 } from 'n8n-workflow';

import {
	pascalCase,
} from 'change-case';

export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer | IDataObject, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
	const credentials = this.getCredentials('aws');
	if (credentials === undefined) {
		throw new NodeOperationError(this.getNode(), 'No credentials got returned!');
	}

	const endpoint = new URL(((credentials.rekognitionEndpoint as string || '').replace('{region}', credentials.region as string) || `https://${service}.${credentials.region}.amazonaws.com`) + path);

	// Sign AWS API request with the user credentials
	const signOpts = {headers: headers || {}, host: endpoint.host, method, path, body};

	sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim()});

	const options: OptionsWithUri = {
		headers: signOpts.headers,
		method,
		uri: endpoint.href,
		body: signOpts.body,
	};

	if (Object.keys(option).length !== 0) {
		Object.assign(options, option);
	}
	try {
		return await this.helpers.request!(options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

export async function awsApiRequestREST(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, service: string, method: string, path: string, body?: string, query: IDataObject = {}, headers?: object, options: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
	const response = await awsApiRequest.call(this, service, method, path, body, query, headers, options, region);
	try {
		return JSON.parse(response);
	} catch (error) {
		return response;
	}
}

export async function awsApiRequestSOAP(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer | IDataObject, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
	const response = await awsApiRequest.call(this, service, method, path, body, query, headers, option, region);
	try {
		return await new Promise((resolve, reject) => {
			parseString(response, { explicitArray: false }, (err, data) => {
				if (err) {
					return reject(err);
				}
				resolve(data);
			});
		});
	} catch (error) {
		return error;
	}
}

export async function awsApiRequestSOAPAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, propertyName: string, service: string, method: string, path: string, body?: string, query: IDataObject = {}, headers: IDataObject = {},  option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any

	const returnData: IDataObject[] = [];

	let responseData;

	do {
		responseData = await awsApiRequestSOAP.call(this, service, method, path, body, query, headers, option, region);

		//https://forums.aws.amazon.com/thread.jspa?threadID=55746
		if (get(responseData, `${propertyName.split('.')[0]}.NextContinuationToken`)) {
			query['continuation-token'] = get(responseData, `${propertyName.split('.')[0]}.NextContinuationToken`);
		}
		if (get(responseData, propertyName)) {
			if (Array.isArray(get(responseData, propertyName))) {
				returnData.push.apply(returnData, get(responseData, propertyName));
			} else {
				returnData.push(get(responseData, propertyName));
			}
		}
		if (query.limit && query.limit <= returnData.length) {
			return returnData;
		}
	} while (
		get(responseData, `${propertyName.split('.')[0]}.IsTruncated`) !== undefined &&
		get(responseData, `${propertyName.split('.')[0]}.IsTruncated`) !== 'false'
	);

	return returnData;
}

function queryToString(params: IDataObject) {
	return Object.keys(params).map(key => key + '=' + params[key]).join('&');
}

export function keysTPascalCase(object: IDataObject) {
	const data: IDataObject = {};
	for (const key of Object.keys(object)) {
		data[pascalCase(key as string)] = object[key];
	}
	return data;
}


<template>

		<NodeIcon class="node-icon" :nodeType="nodeType" :style="nodeIconStyle" />

			{{nodeType.displayName}}

			{{nodeType.description}}

</template>


import {
	OptionsWithUri,
} from 'request';

import {
	IExecuteFunctions,
	IExecuteSingleFunctions,
	ILoadOptionsFunctions,
} from 'n8n-core';

import {
	IDataObject, NodeApiError,
} from 'n8n-workflow';

export async function mindeeApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, path: string, body: any = {}, qs: IDataObject = {}, option = {}): Promise<any> { // tslint:disable-line:no-any

	const resource = this.getNodeParameter('resource', 0) as string;

	let credentials;

	if (resource === 'receipt') {
		credentials = this.getCredentials('mindeeReceiptApi') as IDataObject;
	} else {
		credentials = this.getCredentials('mindeeInvoiceApi') as IDataObject;
	}

	const options: OptionsWithUri = {
		headers: {
			'X-Inferuser-Token': credentials.apiKey,
		},
		method,
		body,
		qs,
		uri: `https://api.mindee.net/products${path}`,
		json: true,
	};
	try {
		if (Object.keys(body).length === 0) {
			delete options.body;
		}
		if (Object.keys(qs).length === 0) {
			delete options.qs;
		}
		if (Object.keys(option).length !== 0) {
			Object.assign(options, option);
		}
		//@ts-ignore
		return await this.helpers.request.call(this, options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

export function cleanData(predictions: IDataObject[]) {

	const newData: IDataObject = {};

	for (const key of Object.keys(predictions[0])) {

		const data = predictions[0][key] as IDataObject | IDataObject[];

		if (key === 'taxes' && data.length) {
			newData[key] = {
				amount: (data as IDataObject[])[0].amount,
				rate: (data as IDataObject[])[0].rate,
			};
		} else if (key === 'locale') {
				//@ts-ignore
				newData['currency'] = data.currency;
				//@ts-ignore
				newData['locale'] = data.value;
		} else {
			//@ts-ignore
			newData[key] = data.value || data.name || data.raw || data.degrees || data.amount || data.iban;
		}
	}

	return newData;
}


attachments = attachmentsUi.attachmentsValues as IAttachment[];

&& (attachmentsUi.attachmentsBinary as IDataObject).length) {

throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');


import { INodeProperties } from 'n8n-workflow';

export const tableOperations = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
			},
		},
		options: [
			{
				name: 'Create Row',
				value: 'createRow',
				description: 'Create/Insert a row',
			},
			{
				name: 'Delete Row',
				value: 'deleteRow',
				description: 'Delete one or multiple rows',
			},
			{
				name: 'Get All Columns',
				value: 'getAllColumns',
				description: 'Get all columns',
			},
			{
				name: 'Get All Rows',
				value: 'getAllRows',
				description: 'Get all the rows',
			},
			{
				name: 'Get Column',
				value: 'getColumn',
				description: 'Get a column',
			},
			{
				name: 'Get Row',
				value: 'getRow',
				description: 'Get a row',
			},
			{
				name: 'Push Button',
				value: 'pushButton',
				description: 'Pushes a button',
			},
		],
		default: 'createRow',
		description: 'The operation to perform.',
	},
] as INodeProperties[];

export const tableFields = [

/* -------------------------------------------------------------------------- */
/*                                table:createRow                             */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Doc',
		name: 'docId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getDocs',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'createRow',
				],
			},
		},
		description: 'ID of the doc.',
	},
	{
		displayName: 'Table',
		name: 'tableId',
		type: 'options',
		typeOptions: {
			loadOptionsDependsOn: [
				'docId',
			],
			loadOptionsMethod: 'getTables',
		},
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'createRow',
				],
			},
		},
		description: 'The table to create the row in.',
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'createRow',
				],
			},
		},
		options: [
			{
				displayName: 'Disable Parsing',
				name: 'disableParsing',
				type: 'boolean',
				default: false,
				description: `If true, the API will not attempt to parse the data in any way.`,
			},
			{
				displayName: 'Key Columns',
				name: 'keyColumns',
				type: 'string',
				default: '',
				description: `Optional column IDs, URLs, or names (fragile and discouraged)
,
				specifying columns to be used as upsert keys. If more than one separate by ,`,
			},
		],
	},
/* -------------------------------------------------------------------------- */
/*                                   table:get                                */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Doc',
		name: 'docId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getDocs',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getRow',
				],
			},
		},
		description: 'ID of the doc.',
	},
	{
		displayName: 'Table',
		name: 'tableId',
		type: 'options',
		typeOptions: {
			loadOptionsDependsOn: [
				'docId',
			],
			loadOptionsMethod: 'getTables',
		},
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getRow',
				],
			},
		},
		description: 'The table to get the row from.',
	},
	{
		displayName: 'Row ID',
		name: 'rowId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getRow',
				],
			},
		},
		description: `ID or name of the row. Names are discouraged because

		they're easily prone to being changed by users. If you're

		using a name, be sure to URI-encode it. If there are

		multiple rows with the same value in the identifying column,

		an arbitrary one will be selected`,
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getRow',
				],
			},
		},
		options: [
			{
				displayName: 'RAW Data',
				name: 'rawData',
				type: 'boolean',
				default: false,
				description: `Returns the data exactly in the way it got received from the API.`,
			},
			{
				displayName: 'Use Column Names',
				name: 'useColumnNames',
				type: 'boolean',
				default: false,
				description: `Use column names instead of column IDs in the returned output.

				This is generally discouraged as it is fragile. If columns are renamed,

				code using original names may throw errors.`,
			},
			{
				displayName: 'ValueFormat',
				name: 'valueFormat',
				type: 'options',
				default: '',
				options: [
					{
						name: 'Simple',
						value: 'simple',
					},
					{
						name: 'Simple With Arrays',
						value: 'simpleWithArrays',
					},
					{
						name: 'Rich',
						value: 'rich',
					},
				],
				description: `The format that cell values are returned as.`,
			},
		],
	},
/* -------------------------------------------------------------------------- */
/*                                   table:getAll                             */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Doc',
		name: 'docId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getDocs',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllRows',
				],
			},
		},
		description: 'ID of the doc.',
	},
	{
		displayName: 'Table',
		name: 'tableId',
		type: 'options',
		typeOptions: {
			loadOptionsDependsOn: [
				'docId',
			],
			loadOptionsMethod: 'getTables',
		},
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllRows',
				],
			},
		},
		description: 'The table to get the rows from.',
	},
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllRows',
				],
			},
		},
		default: false,
		description: 'If all results should be returned or only up to a given limit.',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllRows',
				],
				returnAll: [
					false,
				],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 100,
		},
		default: 50,
		description: 'How many results to return.',
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllRows',
				],
			},
		},
		options: [
			{
				displayName: 'Query',
				name: 'query',
				type: 'string',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				default: '',
				description: `Query used to filter returned rows, specified as <column_id_or_name>:<value>.

				If you'd like to use a column name instead of an ID, you must quote it (e.g., "My Column":123).

				Also note that value is a JSON value; if you'd like to use a string, you must surround it in quotes (e.g., "groceries").`,
			},
			{
				displayName: 'RAW Data',
				name: 'rawData',
				type: 'boolean',
				default: false,
				description: `Returns the data exactly in the way it got received from the API.`,
			},
			{
				displayName: 'Sort By',
				name: 'sortBy',
				type: 'options',
				default: '',
				options: [
					{
						name: 'Created At',
						value: 'createdAt',
					},
					{
						name: 'Natural',
						value: 'natural',
					},
				],
				description: `Specifies the sort order of the rows returned.

				If left unspecified, rows are returned by creation time ascending.`,
			},
			{
				displayName: 'Use Column Names',
				name: 'useColumnNames',
				type: 'boolean',
				default: false,
				description: `Use column names instead of column IDs in the returned output.

				This is generally discouraged as it is fragile. If columns

				are renamed, code using original names may throw errors.`,
			},
			{
				displayName: 'ValueFormat',
				name: 'valueFormat',
				type: 'options',
				default: '',
				options: [
					{
						name: 'Simple',
						value: 'simple',
					},
					{
						name: 'Simple With Arrays',
						value: 'simpleWithArrays',
					},
					{
						name: 'Rich',
						value: 'rich',
					},
				],
				description: `The format that cell values are returned as.`,
			},
			{
				displayName: 'Visible Only',
				name: 'visibleOnly',
				type: 'boolean',
				default: false,
				description: `If true, returns only visible rows and columns for the table.`,
			},
		],
	},
/* -------------------------------------------------------------------------- */
/*                                 row:delete                                 */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Doc',
		name: 'docId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getDocs',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'deleteRow',
				],
			},
		},
		description: 'ID of the doc.',
	},
	{
		displayName: 'Table',
		name: 'tableId',
		type: 'options',
		typeOptions: {
			loadOptionsDependsOn: [
				'docId',
			],
			loadOptionsMethod: 'getTables',
		},
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'deleteRow',
				],
			},
		},
		description: 'The table to delete the row in.',
	},
	{
		displayName: 'Row ID',
		name: 'rowId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'deleteRow',
				],
			},
		},
		description: 'Row IDs to delete.',
	},
/* -------------------------------------------------------------------------- */
/*                                   table:pushButton                         */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Doc',
		name: 'docId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getDocs',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'pushButton',
				],
			},
		},
		description: 'ID of the doc.',
	},
	{
		displayName: 'Table',
		name: 'tableId',
		type: 'options',
		typeOptions: {
			loadOptionsDependsOn: [
				'docId',
			],
			loadOptionsMethod: 'getTables',
		},
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'pushButton',
				],
			},
		},
		description: 'The table to get the row from.',
	},
	{
		displayName: 'Row ID',
		name: 'rowId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'pushButton',
				],
			},
		},
		description: `ID or name of the row. Names are discouraged because

		they're easily prone to being changed by users. If you're

		using a name, be sure to URI-encode it. If there are multiple

		rows with the same value in the identifying column, an arbitrary

		one will be selected`,
	},
	{
		displayName: 'Column',
		name: 'columnId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getColumns',
			loadOptionsDependsOn: [
				'docId',
				'tableId',
			],
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'pushButton',
				],
			},
		},
	},
/* -------------------------------------------------------------------------- */
/*                                   table:getColumn                          */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Doc',
		name: 'docId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getDocs',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getColumn',
				],
			},
		},
		description: 'ID of the doc.',
	},
	{
		displayName: 'Table',
		name: 'tableId',
		type: 'options',
		typeOptions: {
			loadOptionsDependsOn: [
				'docId',
			],
			loadOptionsMethod: 'getTables',
		},
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getColumn',
				],
			},
		},
		description: 'The table to get the row from.',
	},
	{
		displayName: 'Column ID',
		name: 'columnId',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getColumn',
				],
			},
		},
		description: 'The table to get the row from.',
	},
/* -------------------------------------------------------------------------- */
/*                                   table:getAllColumns                      */
/* -------------------------------------------------------------------------- */
	{
		displayName: 'Doc',
		name: 'docId',
		type: 'options',
		required: true,
		typeOptions: {
			loadOptionsMethod: 'getDocs',
		},
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllColumns',
				],
			},
		},
		description: 'ID of the doc.',
	},
	{
		displayName: 'Table',
		name: 'tableId',
		type: 'options',
		typeOptions: {
			loadOptionsDependsOn: [
				'docId',
			],
			loadOptionsMethod: 'getTables',
		},
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllColumns',
				],
			},
		},
		description: 'The table to get the row from.',
	},
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllColumns',
				],
			},
		},
		default: false,
		description: 'If all results should be returned or only up to a given limit.',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				resource: [
					'table',
				],
				operation: [
					'getAllColumns',
				],
				returnAll: [
					false,
				],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 100,
		},
		default: 50,
		description: 'How many results to return.',
	},
] as INodeProperties[];


import {
	OptionsWithUri,
} from 'request';

import {
	IExecuteFunctions,
	IExecuteSingleFunctions,
	ILoadOptionsFunctions,
} from 'n8n-core';

import {
	IDataObject, NodeApiError,
} from 'n8n-workflow';

export async function microsoftApiRequest(this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, headers: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
	const options: OptionsWithUri = {
		headers: {
			'Content-Type': 'application/json',
		},
		method,
		body,
		qs,
		uri: uri || `https://graph.microsoft.com${resource}`,
		json: true,
	};
	try {
		if (Object.keys(headers).length !== 0) {
			options.headers = Object.assign({}, options.headers, headers);
		}
		//@ts-ignore
		return await this.helpers.requestOAuth2.call(this, 'microsoftTeamsOAuth2Api', options);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error);
	}
}

export async function microsoftApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any

	const returnData: IDataObject[] = [];

	let responseData;
	let uri: string | undefined;

	do {
		responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri);
		uri = responseData['@odata.nextLink'];
		returnData.push.apply(returnData, responseData[propertyName]);
		if (query.limit && query.limit <= returnData.length) {
			return returnData;
		}
	} while (
		responseData['@odata.nextLink'] !== undefined
	);

	return returnData;
}

export async function microsoftApiRequestAllItemsSkip(this: IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any

	const returnData: IDataObject[] = [];

	let responseData;
	query['$top'] = 100;
	query['$skip'] = 0;

	do {
		responseData = await microsoftApiRequest.call(this, method, endpoint, body, query);
		query['$skip'] += query['$top'];
		returnData.push.apply(returnData, responseData[propertyName]);
	} while (
		responseData['value'].length !== 0
	);

	return returnData;
}


import {
	ICredentialType,
	NodePropertyTypes,
} from 'n8n-workflow';

export class AsanaApi implements ICredentialType {
	name = 'asanaApi';
	displayName = 'Asana API';
	documentationUrl = 'asana';
	properties = [
		{
			displayName: 'Access Token',
			name: 'accessToken',
			type: 'string' as NodePropertyTypes,
			default: '',
		},
	];
}


const selector = { level: 'settings' as const, ...(settingId && { settingId }) };

await Permissions.find(selector).forEach((permission: IPermission) => {

keep keep keep keep replace replace replace keep keep keep keep

await Permissions.findByLevel('settings', settingId).forEach((permission: IPermission) => {

import type { IPermission, IRole, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { IPermissionsModel } from '@rocket.chat/model-typings';
import type { Collection, Db } from 'mongodb';

import { BaseRaw } from './BaseRaw';

export class PermissionsRaw extends BaseRaw<IPermission> implements IPermissionsModel {
	constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IPermission>>) {
		super(db, 'permissions', trash);
	}

	async createOrUpdate(name: string, roles: IRole['_id'][]): Promise<IPermission['_id']> {
		const exists = await this.findOne<Pick<IPermission, '_id'>>(
			{
				_id: name,
				roles,
			},
			{ projection: { _id: 1 } },
		);

		if (exists) {
			return exists._id;
		}

		await this.updateOne({ _id: name }, { $set: { roles } }, { upsert: true });

		return name;
	}

	async create(id: string, roles: IRole['_id'][]): Promise<IPermission['_id']> {
		const exists = await this.findOneById<Pick<IPermission, '_id'>>(id, { projection: { _id: 1 } });
		if (exists) {
			return exists._id;
		}

		await this.updateOne({ _id: id }, { $set: { roles } }, { upsert: true });

		return id;
	}

	async addRole(permission: string, role: IRole['_id']): Promise<void> {
		await this.updateOne({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } });
	}

	async setRoles(permission: string, roles: IRole['_id'][]): Promise<void> {
		await this.updateOne({ _id: permission }, { $set: { roles } });
	}

	async removeRole(permission: string, role: IRole['_id']): Promise<void> {
		await this.updateOne({ _id: permission, roles: role }, { $pull: { roles: role } });
	}
}


import { performance } from 'perf_hooks';

import { format } from '@rocket.chat/log-format';
import { getQueuedLogs, logEntries } from '@rocket.chat/logger';
import EJSON from 'ejson';

import notifications from '../../app/notifications/server/lib/Notifications';

const processString = (string: string, date: Date): string => {
	try {
		const obj = EJSON.parse(string);
		if (!obj || typeof obj !== 'object') {
			throw new TypeError('Invalid JSON');
		}

		if ('toJSONValue' in obj) {
			return format(obj.toJSONValue(), { color: true });
		}

		if (!Array.isArray(obj) && !(obj instanceof Date) && !(obj instanceof Uint8Array)) {
			return format(obj, { color: true });
		}
		return format(
			{
				message: string,
				time: date,
				level: 'info',
			},
			{ color: true },
		);
	} catch (e) {
		return string;
	}
};

const rawTransformLog = (item: any): { id: string; string: string; ts: Date; time?: number } => {
	return {
		id: item.id,
		string: processString(item.data, item.ts),
		ts: item.ts,
	};
};

const timedTransformLog = (log: any): { id: string; string: string; ts: Date; time?: number } => {
	const timeStart = performance.now();
	const item = rawTransformLog(log);
	const timeEnd = performance.now();

	item.time = timeEnd - timeStart;

	return item;
};

const transformLog = process.env.STDOUT_METRICS === 'true' ? timedTransformLog : rawTransformLog;

logEntries.on('log', (item) => {
	// TODO having this as 'emitWithoutBroadcast' will not sent this data to ddp-streamer, so this data
	// won't be available when using micro services.
	notifications.streamStdout.emitWithoutBroadcast('stdout', transformLog(item));
});

export function getLogs(): {
	id: string;
	string: string;
	ts: Date;
}[] {
	return getQueuedLogs().map(transformLog);
}


import { ContextualbarHeader as ContextualbarHeaderComponent } from '@rocket.chat/fuselage';
import type { FC, ReactNode, ComponentProps } from 'react';
import React, { memo } from 'react';

type ContextualbarHeaderProps = {
	expanded?: boolean;
	children: ReactNode;
} & ComponentProps<typeof ContextualbarHeaderComponent>;

const ContextualbarHeader: FC<ContextualbarHeaderProps> = ({ children, expanded, ...props }) => (
	<ContextualbarHeaderComponent height={expanded ? '64px' : '56px'} {...props}>
		{children}
	</ContextualbarHeaderComponent>
);

export default memo(ContextualbarHeader);


declare module 'asterisk-manager' {
	export = any; // TODO
}


import type { IPermission, IRole, RocketChatRecordDeleted } from '@rocket.chat/core-typings';

import type { IPermissionsModel } from '@rocket.chat/model-typings';

import type { Collection, Db } from 'mongodb';

export class PermissionsRaw extends BaseRaw<IPermission> implements IPermissionsModel {

import type { Collection, Db, FindCursor, IndexDescription } from 'mongodb';


import { Box, MessageGenericPreviewImage } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import React from 'react';

import { useOembedLayout } from '../../hooks/useOembedLayout';
import type { UrlPreviewMetadata } from './UrlPreviewMetadata';

const UrlImagePreview = ({ url }: Pick<UrlPreviewMetadata, 'url'>): ReactElement => {
	const { maxHeight: oembedMaxHeight } = useOembedLayout();

	return (
		<Box maxHeight={oembedMaxHeight} maxWidth='100%'>
			<MessageGenericPreviewImage className='gallery-item' url={url || ''} />
		</Box>
	);
};

export default UrlImagePreview;


import type { ComponentChildren } from 'preact';
import { Component } from 'preact';
import type { CSSProperties } from 'preact/compat';

import { createClassName } from '../../helpers/createClassName';
import { parse } from '../../helpers/parse';
import styles from './styles.scss';

const findLastTextNode = (node: Node): Text | null => {
	if (node.nodeType === Node.TEXT_NODE) {
		return node as Text;
	}
	const children = node.childNodes;
	for (let i = children.length - 1; i >= 0; i--) {
		const textNode = findLastTextNode(children[i]);
		if (textNode !== null) {
			return textNode;
		}
	}

	return null;
};

const replaceCaret = (el: Element) => {
	// Place the caret at the end of the element
	const target = findLastTextNode(el);
	// do not move caret if element was not focused
	const isTargetFocused = document.activeElement === el;
	if (target !== null && target.nodeValue !== null && isTargetFocused) {
		const range = document.createRange();
		const sel = window.getSelection();
		range.setStart(target, target.nodeValue.length);
		range.collapse(true);
		sel?.removeAllRanges();
		sel?.addRange(range);
		if (el instanceof HTMLElement) {
			el.focus();
		}
	}
};

type ComposerProps = {
	className?: string;
	style?: CSSProperties;
	value?: string;
	onChange?: (value: string) => void;
	onSubmit?: (value: string) => void;
	onUpload?: (files: (File | null)[]) => void;
	handleEmojiClick?: () => void;
	placeholder?: string;
	pre?: ComponentChildren;
	post?: ComponentChildren;
	notifyEmojiSelect?: (cb: (emoji: string) => void) => void;
	limitTextLength?: number;
};

type ComposerState = {
	inputLock: boolean;
};

export class Composer extends Component<ComposerProps, ComposerState> {
	private el: HTMLElement | null = null;

	handleRef = (el: HTMLDivElement | null) => {
		this.el = el;
	};

	handleInput = (onChange?: (value: string) => void) => () => {
		if (this.state.inputLock) {
			return;
		}
		onChange?.(this.el?.innerText ?? '');
	};

	handleKeypress = (onSubmit?: (value: string) => void) => (event: KeyboardEvent) => {
		if (event.which === 13 && !event.shiftKey) {
			event.preventDefault();
			if (this.el) {
				onSubmit?.(this.el.innerText);
				this.el.innerText = '';
			}
		}
	};

	handlePaste = (onUpload?: (files: (File | null)[]) => void) => async (event: ClipboardEvent) => {
		if (!event.clipboardData?.items) {
			return;
		}

		event.preventDefault();

		const items = Array.from(event.clipboardData.items);

		const files = items.filter((item) => item.kind === 'file' && /^image\//.test(item.type)).map((item) => item.getAsFile());
		if (files.length) {
			onUpload?.(files);
			return;
		}

		const texts = await Promise.all(
			items
				.filter((item) => item.kind === 'string' && /^text\/plain/.test(item.type))
				.map((item) => new Promise<string>((resolve) => item.getAsString(resolve))),
		);

		texts.forEach((text) => this.pasteText(parse(text)));
	};

	handleDrop = (onUpload?: (files: (File | null)[]) => void) => async (event: DragEvent) => {
		if (!event.dataTransfer?.items) {
			return;
		}

		event.preventDefault();

		const items = Array.from(event.dataTransfer.items);

		const files = items.filter((item) => item.kind === 'file' && /^image\//.test(item.type)).map((item) => item.getAsFile());
		if (files.length) {
			onUpload?.(files);
			return;
		}

		const texts = await Promise.all(
			items
				.filter((item) => item.kind === 'string' && /^text\/plain/.test(item.type))
				.map((item) => new Promise<string>((resolve) => item.getAsString(resolve))),
		);
		texts.forEach((text) => this.pasteText(parse(text)));
	};

	handleClick = () => {
		const { handleEmojiClick } = this.props;
		handleEmojiClick?.();
	};

	pasteText = (plainText: string) => {
		this.el?.focus();

		if (document.queryCommandSupported('insertText')) {
			document.execCommand('insertText', false, plainText);
			return;
		}

		const range = document.getSelection()?.getRangeAt(0);
		if (!range) {
			return;
		}

		range.deleteContents();
		const textNode = document.createTextNode(plainText);
		range.insertNode(textNode);
		range.selectNodeContents(textNode);
		range.collapse(false);

		const selection = window.getSelection();
		selection?.removeAllRanges();
		selection?.addRange(range);
	};

	private value: string | undefined;

	constructor(props: ComposerProps) {
		super(props);
		this.state = {
			inputLock: false,
		};
		this.value = this.props.value;
		this.handleNotifyEmojiSelect = this.handleNotifyEmojiSelect.bind(this);

		if (typeof this.props.notifyEmojiSelect === 'function') {
			this.props.notifyEmojiSelect(this.handleNotifyEmojiSelect);
		}
	}

	// we only update composer if value length changed from 0 to 1 or 1 to 0
	// everything else is managed by this.el
	shouldComponentUpdate({ value: nextValue = '' }: ComposerProps) {
		const { value = '', limitTextLength } = this.props;

		const nextValueEmpty = !nextValue || nextValue.length === 0;
		const valueEmpty = !value || value.length === 0;

		if (nextValueEmpty !== valueEmpty) {
			return true;
		}

		if (nextValue.length === limitTextLength || value.length === limitTextLength) {
			return true;
		}

		return false;
	}

	componentDidUpdate() {
		const { el } = this;
		if (!el) {
			return;
		}

		if (this.props.value !== el.innerHTML) {
			this.value = this.props.value ?? '';
			el.innerHTML = this.value;
		}
		replaceCaret(el);
	}

	handleNotifyEmojiSelect(emoji: string) {
		if (!this.el) {
			throw new Error('Composer: handleNotifyEmojiSelect called but el is not defined');
		}

		const { onChange } = this.props;
		const caretPosition = this.getCaretPosition(this.el);
		const oldText = this.el?.innerText ?? '';
		const newText = `${oldText.slice(0, caretPosition)}${emoji} ${oldText.slice(caretPosition)}`;
		this.el.innerHTML = newText;
		this.moveCursorToEndAndFocus(caretPosition + emoji.length + 1);
		onChange?.(this.el.innerText);
	}

	moveCursorToEndAndFocus(endIndex: number) {
		if (!this.el) {
			throw new Error('Composer: moveCursorToEndAndFocus called but el is not defined');
		}

		const setPos = document.createRange();
		const set = window.getSelection();
		setPos.setStart(this.el.childNodes[0], endIndex);
		setPos.collapse(true);
		set?.removeAllRanges();
		set?.addRange(setPos);
	}

	getCaretPosition(element: HTMLElement) {
		const doc = element.ownerDocument || element.document;
		const win = doc.defaultView || doc.parentWindow;

		if (!win) {
			throw new Error('Composer: getCaretPosition called but win is not defined');
		}

		if (typeof win.getSelection !== 'undefined' && (win.getSelection()?.rangeCount ?? 0) > 0) {
			const range = win.getSelection()!.getRangeAt(0);
			const preCaretRange = range.cloneRange();
			preCaretRange.selectNodeContents(element);
			preCaretRange.setEnd(range.endContainer, range.endOffset);
			return preCaretRange.toString().length;
		}

		if (doc.selection && doc.selection.type !== 'Control') {
			const textRange = doc.selection.createRange!();
			const preCaretTextRange = doc.body.createTextRange?.();
			preCaretTextRange?.moveToElementText?.(element);
			preCaretTextRange?.setEndPoint?.('EndToEnd', textRange);
			return preCaretTextRange?.text?.length ?? 0;
		}

		return 0;
	}

	handleInputLock(locked: boolean) {
		this.setState({ inputLock: locked });
		return 0;
	}

	render = ({ pre, post, value, placeholder, onChange, onSubmit, onUpload, className, style }: ComposerProps) => (
		<div className={createClassName(styles, 'composer', {}, [className])} style={style}>
			{pre}
			<div
				ref={this.handleRef}
				contentEditable
				data-placeholder={placeholder}
				onInput={this.handleInput(onChange)}
				onKeyPress={this.handleKeypress(onSubmit)}
				onPaste={this.handlePaste(onUpload)}
				onDrop={this.handleDrop(onUpload)}
				onClick={this.handleClick}
				onCompositionStart={() => {
					this.handleInputLock(true);
				}}
				onCompositionEnd={() => {
					this.handleInputLock(false);
					onChange?.(this.el?.innerText ?? '');
				}}
				className={createClassName(styles, 'composer__input')}
			>
				{value}
			</div>
			{post}
		</div>
	);
}

export { ComposerAction } from './ComposerAction';
export { ComposerActions } from './ComposerActions';


import { BSONType } from './types';

export const getBSONType = <T>(v: T): BSONType => {
	if (typeof v === 'number') {
		return BSONType.Double;
	}

	if (typeof v === 'string') {
		return BSONType.String;
	}

	if (typeof v === 'boolean') {
		return BSONType.Boolean;
	}

	if (Array.isArray(v)) {
		return BSONType.Array;
	}

	if (v === null) {
		return BSONType.Null;
	}

	if (v instanceof RegExp) {
		return BSONType.Regex;
	}

	if (typeof v === 'function') {
		return BSONType.JavaScript;
	}

	if (v instanceof Date) {
		return BSONType.Date;
	}

	if (v instanceof Uint8Array) {
		return BSONType.BinData;
	}

	return BSONType.Object;
};

const getBSONTypeOrder = (type: BSONType): number => {
	switch (type) {
		case BSONType.Null:
			return 0;

		case BSONType.Double:
		case BSONType.Int:
		case BSONType.Long:
			return 1;

		case BSONType.String:
		case BSONType.Symbol:
			return 2;

		case BSONType.Object:
			return 3;

		case BSONType.Array:
			return 4;

		case BSONType.BinData:
			return 5;

		case BSONType.ObjectId:
			return 6;

		case BSONType.Boolean:
			return 7;

		case BSONType.Date:
		case BSONType.Timestamp:
			return 8;

		case BSONType.Regex:
			return 9;

		case BSONType.JavaScript:
		case BSONType.JavaScriptWithScope:
			return 100;

		default:
			return -1;
	}
};

type ObjectID = {
	toHexString(): string;
	equals(otherID: ObjectID): boolean;
};

export const compareBSONValues = (a: unknown, b: unknown): number => {
	if (a === undefined) {
		return b === undefined ? 0 : -1;
	}

	if (b === undefined) {
		return 1;
	}

	const ta = getBSONType(a);
	const oa = getBSONTypeOrder(ta);

	const tb = getBSONType(b);
	const ob = getBSONTypeOrder(tb);

	if (oa !== ob) {
		return oa < ob ? -1 : 1;
	}

	if (ta !== tb) {
		throw Error('Missing type coercion logic in compareBSONValues');
	}

	switch (ta) {
		case BSONType.Double:
			return (a as number) - (b as number);

		case BSONType.String:
			return (a as string).localeCompare(b as string);

		case BSONType.Object:
			return compareBSONValues(
				Array.prototype.concat.call([], ...Object.entries(a as Record<string, unknown>)),
				Array.prototype.concat.call([], ...Object.entries(b as Record<string, unknown>)),
			);

		case BSONType.Array: {
			for (let i = 0; ; i++) {
				if (i === (a as unknown[]).length) {
					return i === (b as unknown[]).length ? 0 : -1;
				}

				if (i === (b as unknown[]).length) {
					return 1;
				}

				const s = compareBSONValues((a as unknown[])[i], (b as unknown[])[i]);
				if (s !== 0) {
					return s;
				}
			}
		}

		case BSONType.BinData: {
			if ((a as Uint8Array).length !== (b as Uint8Array).length) {
				return (a as Uint8Array).length - (b as Uint8Array).length;
			}

			for (let i = 0; i < (a as Uint8Array).length; i++) {
				if ((a as Uint8Array)[i] === (b as Uint8Array)[i]) {
					continue;
				}

				return (a as Uint8Array)[i] < (b as Uint8Array)[i] ? -1 : 1;
			}

			return 0;
		}

		case BSONType.Null:
		case BSONType.Undefined:
			return 0;

		case BSONType.ObjectId:
			return (a as ObjectID).toHexString().localeCompare((b as ObjectID).toHexString());

		case BSONType.Boolean:
			return Number(a) - Number(b);

		case BSONType.Date:
			return (a as Date).getTime() - (b as Date).getTime();

		case BSONType.Regex:
			throw Error('Sorting not supported on regular expression');

		case BSONType.JavaScript:
		case BSONType.JavaScriptWithScope:
			throw Error('Sorting not supported on Javascript code');
	}

	throw Error('Unknown type to sort');
};


constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IPermission>>) {

async createOrUpdate(name: string, roles: IRole['_id'][]): Promise<IPermission['_id']> {

const exists = await this.findOne<Pick<IPermission, '_id'>>(

keep keep keep keep add keep keep keep keep

protected modelIndexes(): IndexDescription[] {


import type { IBaseEventContent } from '../AbstractMatrixEvent';
import { AbstractMatrixEvent } from '../AbstractMatrixEvent';
import { MatrixEventType } from '../MatrixEventType';

interface IMatrixEventContentSetRoomTopic extends IBaseEventContent {
	topic: string;
}

export class MatrixEventRoomTopicChanged extends AbstractMatrixEvent {
	public content: IMatrixEventContentSetRoomTopic;

	public type = MatrixEventType.ROOM_TOPIC_CHANGED;
}


import type { IOmnichannelRoom } from '@rocket.chat/core-typings';
import { Field, FieldGroup, Button, TextAreaInput, Modal, Box, PaginatedSelectFiltered, Divider } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
import { useEndpoint, useSetting, useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';

import { useRecordList } from '../../../hooks/lists/useRecordList';
import { AsyncStatePhase } from '../../../hooks/useAsyncState';
import UserAutoComplete from '../../UserAutoComplete';
import { useDepartmentsList } from '../hooks/useDepartmentsList';

const ForwardChatModal = ({
	onForward,
	onCancel,
	room,
	...props
}: {
	onForward: (departmentId?: string, userId?: string, comment?: string) => Promise<void>;
	onCancel: () => void;
	room: IOmnichannelRoom;
}): ReactElement => {
	const t = useTranslation();
	const getUserData = useEndpoint('GET', '/v1/users.info');
	const idleAgentsAllowedForForwarding = useSetting('Livechat_enabled_when_agent_idle') as boolean;

	const { getValues, handleSubmit, register, setFocus, setValue, watch } = useForm();

	useEffect(() => {
		setFocus('comment');
	}, [setFocus]);

	const department = watch('department');
	const username = watch('username');

	const [departmentsFilter, setDepartmentsFilter] = useState<string | number | undefined>('');
	const debouncedDepartmentsFilter = useDebouncedValue(departmentsFilter, 500);

	const { itemsList: departmentsList, loadMoreItems: loadMoreDepartments } = useDepartmentsList(
		useMemo(() => ({ filter: debouncedDepartmentsFilter as string, enabled: true }), [debouncedDepartmentsFilter]),
	);
	const { phase: departmentsPhase, items: departments, itemCount: departmentsTotal } = useRecordList(departmentsList);

	const _id = { $ne: room.servedBy?._id };
	const conditions = {
		_id,
		...(!idleAgentsAllowedForForwarding && {
			$or: [
				{
					status: {
						$exists: true,
						$ne: 'offline',
					},
					roles: {
						$ne: 'bot',
					},
				},
				{
					roles: 'bot',
				},
			],
		}),
		statusLivechat: 'available',
	};

	const endReached = useCallback(
		(start) => {
			if (departmentsPhase === AsyncStatePhase.LOADING) {
				loadMoreDepartments(start, Math.min(50, departmentsTotal));
			}
		},
		[departmentsPhase, departmentsTotal, loadMoreDepartments],
	);

	const onSubmit = useCallback(
		async ({ department: departmentId, username, comment }) => {
			let uid;

			if (username) {
				const { user } = await getUserData({ username });
				uid = user?._id;
			}

			onForward(departmentId, uid, comment);
		},
		[getUserData, onForward],
	);

	useEffect(() => {
		register('department');
		register('username');
	}, [register]);

	return (
		<Modal wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit(onSubmit)} {...props} />} {...props}>
			<Modal.Header>
				<Modal.Icon name='baloon-arrow-top-right' />
				<Modal.Title>{t('Forward_chat')}</Modal.Title>
				<Modal.Close onClick={onCancel} />
			</Modal.Header>
			<Modal.Content fontScale='p2'>
				<FieldGroup>
					<Field>
						<Field.Label>{t('Forward_to_department')}</Field.Label>
						<Field.Row>
							<PaginatedSelectFiltered
								withTitle
								filter={departmentsFilter as string}
								setFilter={setDepartmentsFilter}
								options={departments}
								maxWidth='100%'
								placeholder={t('Select_an_option')}
								onChange={(value: string): void => {
									setValue('department', value);
								}}
								flexGrow={1}
								endReached={endReached}
							/>
						</Field.Row>
					</Field>
					<Divider p={0} children={t('or')} />
					<Field>
						<Field.Label>{t('Forward_to_user')}</Field.Label>
						<Field.Row>
							<UserAutoComplete
								conditions={conditions}
								placeholder={t('Username')}
								onChange={(value) => {
									setValue('username', value);
								}}
								value={getValues().username}
							/>
						</Field.Row>
					</Field>
					<Field marginBlock={15}>
						<Field.Label>
							{t('Leave_a_comment')}{' '}
							<Box is='span' color='annotation'>
								({t('Optional')})
							</Box>
						</Field.Label>
						<Field.Row>
							<TextAreaInput data-qa-id='ForwardChatModalTextAreaInputComment' {...register('comment')} rows={8} flexGrow={1} />
						</Field.Row>
					</Field>
				</FieldGroup>
			</Modal.Content>
			<Modal.Footer>
				<Modal.FooterControllers>
					<Button onClick={onCancel}>{t('Cancel')}</Button>
					<Button type='submit' disabled={!username && !department} primary>
						{t('Forward')}
					</Button>
				</Modal.FooterControllers>
			</Modal.Footer>
		</Modal>
	);
};

export default ForwardChatModal;


async removeRole(permission: string, role: IRole['_id']): Promise<void> {

await this.updateOne({ _id: permission, roles: role }, { $pull: { roles: role } });

findByLevel(level: 'settings', settingId?: string): FindCursor<IPermission> {

return this.find({ level, ...(settingId && { settingId }) });

/* eslint no-multi-spaces: 0 */
import type { IPermission, ISetting } from '@rocket.chat/core-typings';
import { Permissions, Settings } from '@rocket.chat/models';

import { createOrUpdateProtectedRoleAsync } from '../../../../server/lib/roles/createOrUpdateProtectedRole';
import { settings } from '../../../settings/server';
import { getSettingPermissionId, CONSTANTS } from '../../lib';
import { permissions } from '../constant/permissions';

export const upsertPermissions = async (): Promise<void> => {
	for await (const permission of permissions) {
		await Permissions.create(permission._id, permission.roles);
	}

	const defaultRoles = [
		{ name: 'admin', scope: 'Users', description: 'Admin' },
		{ name: 'moderator', scope: 'Subscriptions', description: 'Moderator' },
		{ name: 'leader', scope: 'Subscriptions', description: 'Leader' },
		{ name: 'owner', scope: 'Subscriptions', description: 'Owner' },
		{ name: 'user', scope: 'Users', description: '' },
		{ name: 'bot', scope: 'Users', description: '' },
		{ name: 'app', scope: 'Users', description: '' },
		{ name: 'guest', scope: 'Users', description: '' },
		{ name: 'anonymous', scope: 'Users', description: '' },
		{ name: 'livechat-agent', scope: 'Users', description: 'Livechat Agent' },
		{ name: 'livechat-manager', scope: 'Users', description: 'Livechat Manager' },
	] as const;

	for await (const role of defaultRoles) {
		await createOrUpdateProtectedRoleAsync(role.name, role);
	}

	const getPreviousPermissions = async function (settingId?: string): Promise<Record<string, IPermission>> {
		const previousSettingPermissions: {
			[key: string]: IPermission;
		} = {};

		const selector = { level: 'settings' as const, ...(settingId && { settingId }) };

		await Permissions.find(selector).forEach((permission: IPermission) => {
			previousSettingPermissions[permission._id] = permission;
		});
		return previousSettingPermissions;
	};

	const createSettingPermission = async function (
		setting: ISetting,
		previousSettingPermissions: {
			[key: string]: IPermission;
		},
	): Promise<void> {
		const permissionId = getSettingPermissionId(setting._id);
		const permission: Omit<IPermission, '_id' | '_updatedAt'> = {
			level: CONSTANTS.SETTINGS_LEVEL as 'settings' | undefined,
			// copy those setting-properties which are needed to properly publish the setting-based permissions
			settingId: setting._id,
			group: setting.group,
			section: setting.section ?? undefined,
			sorter: setting.sorter,
			roles: [],
		};
		// copy previously assigned roles if available
		if (previousSettingPermissions[permissionId]?.roles) {
			permission.roles = previousSettingPermissions[permissionId].roles;
		}
		if (setting.group) {
			permission.groupPermissionId = getSettingPermissionId(setting.group);
		}
		if (setting.section) {
			permission.sectionPermissionId = getSettingPermissionId(setting.section);
		}

		const existent = await Permissions.findOne(
			{
				_id: permissionId,
				...permission,
			},
			{ projection: { _id: 1 } },
		);

		if (!existent) {
			try {
				await Permissions.updateOne({ _id: permissionId }, { $set: permission }, { upsert: true });
			} catch (e) {
				if (!(e as Error).message.includes('E11000')) {
					// E11000 refers to a MongoDB error that can occur when using unique indexes for upserts
					// https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes
					await Permissions.updateOne({ _id: permissionId }, { $set: permission }, { upsert: true });
				}
			}
		}

		delete previousSettingPermissions[permissionId];
	};

	const createPermissionsForExistingSettings = async function (): Promise<void> {
		const previousSettingPermissions = await getPreviousPermissions();

		const settings = await Settings.findNotHidden().toArray();
		for await (const setting of settings) {
			await createSettingPermission(setting, previousSettingPermissions);
		}

		// remove permissions for non-existent settings
		for await (const obsoletePermission of Object.keys(previousSettingPermissions)) {
			if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) {
				await Permissions.deleteOne({ _id: obsoletePermission });
			}
		}
	};

	// for each setting which already exists, create a permission to allow changing just this one setting
	await createPermissionsForExistingSettings();

	// register a callback for settings for be create in higher-level-packages
	settings.on('*', async ([settingId]) => {
		const previousSettingPermissions = await getPreviousPermissions(settingId);
		const setting = await Settings.findOneById(settingId);
		if (setting && !setting.hidden) {
			await createSettingPermission(setting, previousSettingPermissions);
		}
	});
};


import { faker } from '@faker-js/faker';

import { Users } from './fixtures/userStates';
import { HomeTeam } from './page-objects';
import { createTargetChannel } from './utils';
import { test, expect } from './utils/test';

test.use({ storageState: Users.admin.state });

test.describe.serial('teams-management', () => {
	let poHomeTeam: HomeTeam;
	let targetChannel: string;
	const targetTeam = faker.string.uuid();

	test.beforeAll(async ({ api }) => {
		targetChannel = await createTargetChannel(api);
	});

	test.beforeEach(async ({ page }) => {
		poHomeTeam = new HomeTeam(page);

		await page.goto('/home');
	});

	test('expect create "targetTeam"', async ({ page }) => {
		await poHomeTeam.sidenav.openNewByLabel('Team');
		await poHomeTeam.inputTeamName.type(targetTeam);
		await poHomeTeam.addMember('user1');
		await poHomeTeam.btnTeamCreate.click();

		await expect(page).toHaveURL(`/group/${targetTeam}`);
	});

	test('expect throw validation error if team name already exists', async () => {
		await poHomeTeam.sidenav.openNewByLabel('Team');
		await poHomeTeam.inputTeamName.type(targetTeam);
		await poHomeTeam.btnTeamCreate.click();

		await expect(poHomeTeam.inputTeamName).toHaveAttribute('aria-invalid', 'true');
	});

	test('expect send hello in the "targetTeam" and reply in a thread', async ({ page }) => {
		await poHomeTeam.sidenav.openChat(targetTeam);
		await poHomeTeam.content.sendMessage('hello');
		await poHomeTeam.content.openLastMessageMenu();

		await page.locator('[data-qa-id="reply-in-thread"]').click();
		await page.locator('.rcx-vertical-bar').locator(`role=textbox[name="Message #${targetTeam}"]`).type('any-reply-message');
		await page.keyboard.press('Enter');

		await expect(poHomeTeam.tabs.flexTabViewThreadMessage).toHaveText('any-reply-message');
	});

	test('expect set "targetTeam" as readonly', async () => {
		await poHomeTeam.sidenav.openChat(targetTeam);
		await poHomeTeam.tabs.btnRoomInfo.click();
		await poHomeTeam.tabs.room.btnEdit.click();
		await poHomeTeam.tabs.room.checkboxReadOnly.click();
		await poHomeTeam.tabs.room.btnSave.click();
	});

	test('expect insert a channel inside "targetTeam"', async ({ page }) => {
		await poHomeTeam.sidenav.openChat(targetTeam);
		await poHomeTeam.tabs.btnChannels.click();
		await poHomeTeam.tabs.channels.btnAddExisting.click();
		await poHomeTeam.tabs.channels.inputChannels.type(targetChannel, { delay: 100 });
		await page.locator(`.rcx-option__content:has-text("${targetChannel}")`).click();
		await poHomeTeam.tabs.channels.btnAdd.click();
		await expect(page.locator('//main//aside >> li')).toContainText(targetChannel);
	});
});


@rocket.chat/instance-status

echo "Error: no test specified" && exit 1

tsc --watch --preserveWatchOutput -p tsconfig.json


export type ProgressStep =
	| 'importer_new'
	| 'importer_uploading'
	| 'importer_downloading_file'
	| 'importer_file_loaded'
	| 'importer_preparing_started'
	| 'importer_preparing_users'
	| 'importer_preparing_channels'
	| 'importer_preparing_messages'
	| 'importer_user_selection'
	| 'importer_importing_started'
	| 'importer_importing_users'
	| 'importer_importing_channels'
	| 'importer_importing_messages'
	| 'importer_importing_files'
	| 'importer_finishing'
	| 'importer_done'
	| 'importer_import_failed'
	| 'importer_import_cancelled';

export interface IImportProgress {
	key: string;
	name: string;
	step: ProgressStep;
	count: {
		completed: number;
		total: number;
	};
}


import type { FindCursor } from 'mongodb';

import type { IPermission, IRole } from '@rocket.chat/core-typings';

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

export interface IPermissionsModel extends IBaseModel<IPermission> {
	createOrUpdate(name: string, roles: IRole['_id'][]): Promise<IPermission['_id']>;
	create(id: string, roles: IRole['_id'][]): Promise<IPermission['_id']>;
	addRole(permission: string, role: IRole['_id']): Promise<void>;
	setRoles(permission: string, roles: IRole['_id'][]): Promise<void>;
	removeRole(permission: string, role: IRole['_id']): Promise<void>;
}


Nom

Voulez-vous vraiment terminer cette conversation ?

Voulez-vous vraiment supprimer toutes vos données personnelles ?

Êtes-vous sûr de vouloir changer de département ?

Un nouveau message depuis {{val, datetime}}

{{count}} nouveaux messages depuis {{val, datetime}}

Erreur lors de la fermeture de la conversation.

Erreur lors de la suppression des données utilisateur.

Erreur lors de la création d'une nouvelle conversation : {{reason}}

Le fichier dépasse la taille autorisée de {{size}}.

Ouvrir <1>menu options → Oublier/Supprimer mes données personnelles</1> pour demander la suppression immédiate de vos données personnelles.

Si vous avez une autre question, cliquez sur le bouton ci-dessous pour démarrer une nouvelle conversation.

Aucun agent disponible pour un transfert

Veuillez nous donner quelques informations pour démarrer la conversation

Veuillez patienter, attente d'un agent disponible...

L'opérateur de vos données personnelles est [Company Name], dont l'adresse est [Company Address]. Pour débuter le chat vous acceptez que vos données personnelles soient utilisées et transmises conformément au règlement général sur la protection des données (RGPD).

Nous ne sommes pas en ligne en ce moment. Merci de laisser un message.

Votre navigateur ne supporte pas les éléments audio

Votre navigateur ne supporte pas les éléments vidéo

Votre rang est #{{spot}} (Temps d'attente estimé: {{estimatedWaitTime}})


import { RegisterServerPage, StandaloneServerPage } from '@rocket.chat/onboarding-ui';
import { useRoute } from '@rocket.chat/ui-contexts';
import type { ReactElement, ComponentProps } from 'react';
import React, { useState } from 'react';

import { useSetupWizardContext } from '../contexts/SetupWizardContext';

const SERVER_OPTIONS = {
	REGISTERED: 'REGISTERED',
	STANDALONE: 'STANDALONE',
};

const RegisterServerStep = (): ReactElement => {
	const { goToPreviousStep, currentStep, setSetupWizardData, registerServer, maxSteps, offline, completeSetupWizard } =
		useSetupWizardContext();
	const [serverOption, setServerOption] = useState(SERVER_OPTIONS.REGISTERED);

	const router = useRoute('cloud');

	const handleRegisterOffline: ComponentProps<typeof RegisterServerPage>['onSubmit'] = async () => {
		await completeSetupWizard();
		router.push({}, { register: 'true' });
	};

	const handleRegister: ComponentProps<typeof RegisterServerPage>['onSubmit'] = async (data) => {
		if (data.registerType !== 'standalone') {
			setSetupWizardData((prevState) => ({ ...prevState, serverData: data }));
			await registerServer(data);
		}
	};

	const handleConfirmStandalone: ComponentProps<typeof StandaloneServerPage>['onSubmit'] = async ({ registerType }) => {
		if (registerType !== 'registered') {
			return completeSetupWizard();
		}
	};

	if (serverOption === SERVER_OPTIONS.STANDALONE) {
		return (
			<StandaloneServerPage
				currentStep={currentStep}
				onBackButtonClick={(): void => setServerOption(SERVER_OPTIONS.REGISTERED)}
				onSubmit={handleConfirmStandalone}
				stepCount={maxSteps}
			/>
		);
	}

	return (
		<RegisterServerPage
			onClickRegisterLater={(): void => setServerOption(SERVER_OPTIONS.STANDALONE)}
			onBackButtonClick={goToPreviousStep}
			stepCount={maxSteps}
			onSubmit={offline ? handleRegisterOffline : handleRegister}
			currentStep={currentStep}
			offline={offline}
		/>
	);
};

export default RegisterServerStep;


/* eslint-disable no-debugger */
import util from 'util';

import WS from 'jest-websocket-mock';
import { WebSocket } from 'ws';

import { DDPSDK } from '../src/DDPSDK';
import { fireStreamChange, fireStreamAdded, fireStreamRemove, handleConnection, handleSubscription, handleMethod } from './helpers';

(global as any).WebSocket = (global as any).WebSocket || WebSocket;

export let server: WS;

const callXTimes = <F extends (...args: any) => any>(fn: F, times: number): F => {
	return (async (...args) => {
		const methods = [].concat(...Array(times));
		// eslint-disable-next-line @typescript-eslint/no-unused-vars
		for (const _ of methods) {
			// eslint-disable-next-line no-await-in-loop
			await fn(...args);
		}
	}) as F;
};

beforeEach(async () => {
	server = new WS('ws://localhost:1234/websocket');
});

afterEach(() => {
	server.close();
	WS.clean();
	jest.useRealTimers();
});

it('should handle a stream of messages', async () => {
	const cb = jest.fn();

	const streamName = 'stream';
	const streamParams = '123';

	const sdk = DDPSDK.create('ws://localhost:1234');

	await handleConnection(server, sdk.connection.connect());

	const stream = sdk.stream(streamName, streamParams, cb);

	await handleSubscription(server, stream.id, streamName, streamParams);

	await stream.ready();

	fireStreamChange(server, streamName, streamParams);
	fireStreamChange(server, streamName, streamParams);
	fireStreamChange(server, streamName, streamParams);

	expect(cb).toBeCalledTimes(3);

	fireStreamChange(server, streamName, `${streamParams}-another`);

	expect(cb).toBeCalledTimes(3);

	expect(cb).toHaveBeenNthCalledWith(1, 1);
	sdk.connection.close();
});

it('should ignore messages other from changed', async () => {
	const cb = jest.fn();

	const streamName = 'stream';
	const streamParams = '123';

	const sdk = DDPSDK.create('ws://localhost:1234');

	await handleConnection(server, sdk.connection.connect());

	const stream = sdk.stream(streamName, streamParams, cb);

	await handleSubscription(server, stream.id, streamName, streamParams);

	await stream.ready();

	fireStreamAdded(server, streamName, streamParams);

	fireStreamRemove(server, streamName, streamParams);

	expect(cb).toBeCalledTimes(0);
	sdk.connection.close();
});

it('should handle streams after reconnect', async () => {
	const cb = jest.fn();

	const streamName = 'stream';
	const streamParams = '123';

	const sdk = DDPSDK.create('ws://localhost:1234');

	await handleConnection(server, sdk.connection.connect());

	const result = sdk.stream(streamName, streamParams, cb);

	expect(result.isReady).toBe(false);

	expect(sdk.client.subscriptions.size).toBe(1);

	await handleSubscription(server, result.id, streamName, streamParams);

	await result.ready();

	const fire = callXTimes(fireStreamChange, 3);

	await fire(server, streamName, streamParams);

	expect(cb).toBeCalledTimes(3);

	// Fake timers are used to avoid waiting for the reconnect timeout
	jest.useFakeTimers();
	server.close();
	WS.clean();

	server = new WS('ws://localhost:1234/websocket');

	const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
	const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
	const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));
	await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);

	// the client should reconnect and resubscribe
	await Promise.all([handleSubscription(server, result.id, streamName, streamParams), jest.advanceTimersByTimeAsync(1000)]);

	fire(server, streamName, streamParams);
	await jest.advanceTimersByTimeAsync(1000);

	expect(cb).toBeCalledTimes(6);

	jest.useRealTimers();
	sdk.connection.close();
});

it('should handle an unsubscribe stream after reconnect', async () => {
	const cb = jest.fn();

	const streamName = 'stream';
	const streamParams = '123';

	const sdk = DDPSDK.create('ws://localhost:1234');

	await handleConnection(server, sdk.connection.connect());

	const subscription = sdk.stream(streamName, streamParams, cb);

	expect(subscription.isReady).toBe(false);

	expect(sdk.client.subscriptions.size).toBe(1);

	await handleSubscription(server, subscription.id, streamName, streamParams);

	await expect(subscription.ready()).resolves.toBe(undefined);

	expect(subscription.isReady).toBe(true);

	fireStreamChange(server, streamName, streamParams);
	fireStreamChange(server, streamName, streamParams);
	fireStreamChange(server, streamName, streamParams);

	expect(cb).toBeCalledTimes(3);

	// Fake timers are used to avoid waiting for the reconnect timeout
	jest.useFakeTimers();

	server.close();
	WS.clean();
	server = new WS('ws://localhost:1234/websocket');

	const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
	const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
	const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));
	await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);

	await handleSubscription(server, subscription.id, streamName, streamParams);

	expect(subscription.isReady).toBe(true);

	fireStreamChange(server, streamName, streamParams);

	subscription.stop();

	expect(sdk.client.subscriptions.size).toBe(0);

	fireStreamChange(server, streamName, streamParams);
	fireStreamChange(server, streamName, streamParams);
	jest.advanceTimersByTimeAsync(1000);

	expect(cb).toBeCalledTimes(4);

	expect(sdk.client.subscriptions.size).toBe(0);
	jest.useRealTimers();
	sdk.connection.close();
	sdk.connection.close();
});

it('should create and connect to a stream', async () => {
	const promise = DDPSDK.createAndConnect('ws://localhost:1234');
	await handleConnection(server, promise);
	const sdk = await promise;
	sdk.connection.close();
});

describe('Method call and Disconnection cases', () => {
	it('should handle properly if the message was sent after disconnection', async () => {
		const sdk = DDPSDK.create('ws://localhost:1234');

		await handleConnection(server, sdk.connection.connect());

		const [result] = await handleMethod(server, 'method', ['args1'], sdk.client.callAsync('method', 'args1'));

		expect(result).toBe(1);
		// Fake timers are used to avoid waiting for the reconnect timeout
		jest.useFakeTimers();

		server.close();
		WS.clean();
		server = new WS('ws://localhost:1234/websocket');

		const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
		const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
		const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));

		const callResult = sdk.client.callAsync('method', 'args2');

		expect(util.inspect(callResult).includes('pending')).toBe(true);

		await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);

		const [result2] = await handleMethod(server, 'method', ['args2'], callResult);

		expect(util.inspect(callResult).includes('pending')).toBe(false);
		expect(result2).toBe(1);
		sdk.connection.close();
		jest.useRealTimers();
	});

	it.skip('should handle properly if the message was sent before disconnection but got disconnected before receiving the response', async () => {
		const sdk = DDPSDK.create('ws://localhost:1234');

		await handleConnection(server, sdk.connection.connect());

		const callResult = sdk.client.callAsync('method', 'args');

		expect(util.inspect(callResult).includes('pending')).toBe(true);

		// Fake timers are used to avoid waiting for the reconnect timeout
		jest.useFakeTimers();

		server.close();
		WS.clean();
		server = new WS('ws://localhost:1234/websocket');

		const reconnect = new Promise((resolve) => sdk.connection.once('reconnecting', () => resolve(undefined)));
		const connecting = new Promise((resolve) => sdk.connection.once('connecting', () => resolve(undefined)));
		const connected = new Promise((resolve) => sdk.connection.once('connected', () => resolve(undefined)));

		await handleConnection(server, jest.advanceTimersByTimeAsync(1000), reconnect, connecting, connected);

		expect(util.inspect(callResult).includes('pending')).toBe(true);

		const [result] = await handleMethod(server, 'method', ['args2'], callResult);

		expect(result).toBe(1);

		jest.useRealTimers();
		sdk.connection.close();
	});
});


create(id: string, roles: IRole['_id'][]): Promise<IPermission['_id']>;

addRole(permission: string, role: IRole['_id']): Promise<void>;

setRoles(permission: string, roles: IRole['_id'][]): Promise<void>;

removeRole(permission: string, role: IRole['_id']): Promise<void>;

findByLevel(level: 'settings', settingId?: string): FindCursor<IPermission>;


import { Box, Avatar } from '@rocket.chat/fuselage';
import type { FC } from 'react';
import React, { memo } from 'react';

const AttachmentThumb: FC<{ url: string }> = ({ url }) => (
	<Box mis={8}>
		<Avatar {...({ url, size: 'x48' } as any)} />
	</Box>
);

export default memo(AttachmentThumb);


import type { AppManager } from '@rocket.chat/apps-engine/server/AppManager';

import { API } from '../../../../../app/api/server';
import type { AppsRestApi } from '../rest';

export const actionButtonsHandler = (apiManager: AppsRestApi) =>
	[
		{
			authRequired: false,
		},
		{
			get(): any {
				const manager = apiManager._manager as AppManager;

				const buttons = manager.getUIActionButtonManager().getAllActionButtons();

				return API.v1.success(buttons);
			},
		},
	] as const;


import type {
	ILivechatVisitor,
	IOmnichannelRoom,
	VideoAttachmentProps,
	ImageAttachmentProps,
	AudioAttachmentProps,
} from '@rocket.chat/core-typings';
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, LivechatRooms, Messages } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import type { ParsedMail, Attachment } from 'mailparser';
import stripHtml from 'string-strip-html';

import { FileUpload } from '../../../app/file-upload/server';
import { Livechat } from '../../../app/livechat/server/lib/Livechat';
import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped';
import { QueueManager } from '../../../app/livechat/server/lib/QueueManager';
import { settings } from '../../../app/settings/server';
import { i18n } from '../../lib/i18n';
import { logger } from './logger';

type FileAttachment = VideoAttachmentProps & ImageAttachmentProps & AudioAttachmentProps;

const language = settings.get<string>('Language') || 'en';
const t = (s: string): string => i18n.t(s, { lng: language });

async function getGuestByEmail(email: string, name: string, department = ''): Promise<ILivechatVisitor | null> {
	logger.debug(`Attempt to register a guest for ${email} on department: ${department}`);
	const guest = await LivechatVisitors.findOneGuestByEmailAddress(email);

	if (guest) {
		logger.debug(`Guest with email ${email} found with id ${guest._id}`);
		if (guest.department !== department) {
			logger.debug({
				msg: 'Switching departments for guest',
				guest,
				previousDepartment: guest.department,
				newDepartment: department,
			});
			if (!department) {
				await LivechatVisitors.removeDepartmentById(guest._id);
				delete guest.department;
				return guest;
			}
			await LivechatTyped.setDepartmentForGuest({ token: guest.token, department });
			return LivechatVisitors.findOneById(guest._id, {});
		}
		return guest;
	}

	logger.debug({
		msg: 'Creating a new Omnichannel guest for visitor with email',
		email,
	});
	const userId = await LivechatTyped.registerGuest({
		token: Random.id(),
		name: name || email,
		email,
		department,
	});

	const newGuest = await LivechatVisitors.findOneById(userId);
	logger.debug(`Guest ${userId} for visitor ${email} created`);
	if (newGuest) {
		return newGuest;
	}

	throw new Error('Error getting guest');
}

async function uploadAttachment(attachmentParam: Attachment, rid: string, visitorToken: string): Promise<Partial<FileAttachment>> {
	const details = {
		name: attachmentParam.filename,
		size: attachmentParam.size,
		type: attachmentParam.contentType,
		rid,
		visitorToken,
	};

	const fileStore = FileUpload.getStore('Uploads');

	const file = await fileStore.insert(details, attachmentParam.content);

	const url = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`);

	const attachment: Partial<FileAttachment> = {
		title: file.name || '',
		title_link: url,
	};

	if (file.type && /^image\/.+/.test(file.type)) {
		attachment.image_url = url;
		attachment.image_type = file.type;
		attachment.image_size = file.size;
		attachment.image_dimensions = file.identify?.size != null ? file.identify.size : undefined;
	}

	if (file.type && /^audio\/.+/.test(file.type)) {
		attachment.audio_url = url;
		attachment.audio_type = file.type;
		attachment.audio_size = file.size;
	}

	if (file.type && /^video\/.+/.test(file.type)) {
		attachment.video_url = url;
		attachment.video_type = file.type;
		attachment.video_size = file.size;
	}

	return attachment;
}

export async function onEmailReceived(email: ParsedMail, inbox: string, department = ''): Promise<void> {
	logger.debug(`New email conversation received on inbox ${inbox}. Will be assigned to department ${department}`);
	if (!email.from?.value?.[0]?.address) {
		return;
	}

	const references = typeof email.references === 'string' ? [email.references] : email.references;
	const initialRef = [email.messageId, email.inReplyTo].filter(Boolean) as string[];
	const thread = (references?.length ? references : []).flatMap((t: string) => t.split(',')).concat(initialRef);

	logger.debug(`Received new email conversation with thread ${thread} on inbox ${inbox} from ${email.from.value[0].address}`);

	logger.debug(`Fetching guest for visitor ${email.from.value[0].address}`);
	const guest = await getGuestByEmail(email.from.value[0].address, email.from.value[0].name, department);

	if (!guest) {
		logger.debug(`No visitor found for ${email.from.value[0].address}`);
		return;
	}

	logger.debug(`Guest ${guest._id} obtained. Attempting to find or create a room on department ${department}`);

	let room: IOmnichannelRoom | null = await LivechatRooms.findOneByVisitorTokenAndEmailThreadAndDepartment(
		guest.token,
		thread,
		department,
		{},
	);

	logger.debug({
		msg: 'Room found for guest',
		room,
		guest,
	});

	if (room?.closedAt) {
		logger.debug(`Room ${room?._id} is closed. Reopening`);
		room = await QueueManager.unarchiveRoom(room);
	}

	// TODO: html => md with turndown
	const msg = email.html
		? stripHtml(email.html, {
				dumpLinkHrefsNearby: {
					enabled: true,
					putOnNewLine: false,
					wrapHeads: '(',
					wrapTails: ')',
				},
				skipHtmlDecoding: false,
		  }).result
		: email.text || '';

	const rid = room?._id ?? Random.id();
	const msgId = Random.id();

	logger.debug(`Sending email message to room ${rid} for visitor ${guest._id}. Conversation assigned to department ${department}`);

	Livechat.sendMessage({
		guest,
		message: {
			_id: msgId,
			groupable: false,
			msg,
			token: guest.token,
			attachments: [
				{
					actions: [
						{
							type: 'button',
							text: t('Reply_via_Email'),
							msg: 'msg',
							msgId,
							msg_in_chat_window: true,
							msg_processing_type: 'respondWithQuotedMessage',
						},
					],
				},
			],
			blocks: [
				{
					type: 'context',
					elements: [
						{
							type: 'mrkdwn',
							text: `**${t('From')}:** ${email.from.text}\n**${t('Subject')}:** ${email.subject}`,
						},
					],
				},
				{
					type: 'section',
					text: {
						type: 'mrkdwn',
						text: msg,
					},
				},
			],
			rid,
			email: {
				thread,
				messageId: email.messageId,
			},
		},
		roomInfo: {
			email: {
				inbox,
				thread,
				replyTo: email.from.value[0].address,
				subject: email.subject,
			},
			source: {
				type: OmnichannelSourceType.EMAIL,
				id: inbox,
				alias: 'email-inbox',
			},
		},
		agent: undefined,
	})
		.then(async () => {
			if (!email.attachments.length) {
				return;
			}

			const attachments = [];
			for await (const attachment of email.attachments) {
				if (attachment.type !== 'attachment') {
					continue;
				}

				try {
					attachments.push(await uploadAttachment(attachment, rid, guest.token));
				} catch (err) {
					Livechat.logger.error({ msg: 'Error uploading attachment from email', err });
				}
			}

			await Messages.updateOne(
				{ _id: msgId },
				{
					$addToSet: {
						attachments: {
							$each: attachments,
						},
					},
				},
			);
			room && (await LivechatRooms.updateEmailThreadByRoomId(room._id, thread));
		})
		.catch((err) => {
			Livechat.logger.error({
				msg: 'Error receiving email',
				err,
			});
		});
}


"Accounts_OAuth_Custom_Button_Color" : "Button Color",

keep keep keep keep add keep keep keep keep keep

"Allow_Invalid_SelfSigned_Certs": "Allow invalid and Self-Signed SSL certificate's for link validation and previews",

Name

Deleting a user will delete all messages from that user as well. This cannot be undone.

Follow our social profiles, fork us on github and share your thoughts about the rocket.chat app on our trello board.

You don't have any email as public email in your GitHub account

Have your own web chat. Developed with Meteor.com, the Rocket.Chat is a great solution for developers looking forward to build and evolve their own chat platform.

The password confirmation does not match password

%s is not a valid room name,
 use only letters, numbers and dashes

Message

You haven't started any conversations yet.

You don't have permission to view this room

No channel with name <strong>"%s"</strong> was found!

No private group with name <strong>"%s"</strong> was found!

No user with username <strong>"%s"</strong> was found!

Don't send my anonymous statistics to Rocket.Chat

By sending your anonymous statistics, you'll help us identify how many instances of Rocket.Chat are deployed, as well as how good the system is behaving, so we can further improve it. If you want to continue sending us your anonymous statistics, uncheck the above checkbox. Thank you.

Please wait, statistics are being generated.

Room name changed to: __room_name__ by __user_by__

Select a service to login to load your picture or upload one directly from your computer

Showing <b>__total_online__</b> of __total__ users

Use the side menu to access your rooms and chats

User __user_added__ added by __user_by__.

User __user_removed__ removed by __user_by__.

The username is used to allow others to mention you in messages.

%s is not a valid username,
 use only letters, numbers, dots and dashes

<strong>%s</strong> is already in use :(

Before you can login, your account must be manually activated by an administrator.

We have sent you an e-mail with password reset instructions. If you do not receive an e-mail shortly, please come back and try again.

We have sent you an e-mail to confirm your registration. If you do not receive an e-mail shortly, please come back and try again.

You are in preview mode of channel #<strong>__room_name__</strong>

You need to confirm your email to login!


//! moment.js locale configuration
//! locale : spanish (es)
//! author : Julio Napurí : https://github.com/julionc

(function (global, factory) {
   typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('../moment')) :
   typeof define === 'function' && define.amd ? define(['moment'], factory) :
   factory(global.moment)
}(this, function (moment) { 'use strict';

    var monthsShortDot = 'Ene._Feb._Mar._Abr._May._Jun._Jul._Ago._Sep._Oct._Nov._Dic.'.split('_'),
        monthsShort = 'Ene_Feb_Mar_Abr_May_Jun_Jul_Ago_Sep_Oct_Nov_Dic'.split('_');

    var es = moment.defineLocale('es', {
        months : 'Enero_Febrero_Marzo_Abril_Mayo_Junio_Julio_Agosto_Septiembre_Octubre_Noviembre_Diciembre'.split('_'),
        monthsShort : function (m, format) {
            if (/-MMM-/.test(format)) {
                return monthsShort[m.month()];
            } else {
                return monthsShortDot[m.month()];
            }
        },
        weekdays : 'Domingo_Lunes_Martes_Miércoles_Jueves_Viernes_Sábado'.split('_'),
        weekdaysShort : 'Dom._Lun._Mar._Mié._Jue._Vie._Sáb.'.split('_'),
        weekdaysMin : 'Do_Lu_Ma_Mi_Ju_Vi_Sá'.split('_'),
        longDateFormat : {
            LT : 'H:mm',
            LTS : 'LT:ss',
            L : 'DD/MM/YYYY',
            LL : 'D [de] MMMM [de] YYYY',
            LLL : 'D [de] MMMM [de] YYYY LT',
            LLLL : 'dddd, D [de] MMMM [de] YYYY LT'
        },
        calendar : {
            sameDay : function () {
                return '[hoy a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
            },
            nextDay : function () {
                return '[mañana a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
            },
            nextWeek : function () {
                return 'dddd [a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
            },
            lastDay : function () {
                return '[ayer a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
            },
            lastWeek : function () {
                return '[el] dddd [pasado a la' + ((this.hours() !== 1) ? 's' : '') + '] LT';
            },
            sameElse : 'L'
        },
        relativeTime : {
            future : 'en %s',
            past : 'hace %s',
            s : 'unos segundos',
            m : 'un minuto',
            mm : '%d minutos',
            h : 'una hora',
            hh : '%d horas',
            d : 'un día',
            dd : '%d días',
            M : 'un mes',
            MM : '%d meses',
            y : 'un año',
            yy : '%d años'
        },
        ordinalParse : /\d{1,2}º/,
        ordinal : '%dº',
        week : {
            dow : 1, // Monday is the first day of the week.
            doy : 4  // The week that contains Jan 4th is the first week of the year.
        }
    });

    return es;

}));


# https://github.com/TelescopeJS/Telescope/blob/master/packages/telescope-lib/lib/callbacks.js

###
# Callback hooks provide an easy way to add extra steps to common operations.
# @namespace RocketChat.callbacks
###
RocketChat.callbacks = {}

###
# Callback priorities
###
RocketChat.callbacks.priority =
	HIGH: -1000
	MEDIUM: 0
	LOW: 1000

###
# Add a callback function to a hook
# @param {String} hook - The name of the hook
# @param {Function} callback - The callback function
###

RocketChat.callbacks.add = (hook, callback, priority, id) ->
	# if callback array doesn't exist yet, initialize it
	priority ?= RocketChat.callbacks.priority.MEDIUM
	unless _.isNumber priority
		priority = RocketChat.callbacks.priority.MEDIUM
	callback.priority = priority
	callback.id = id or Random.id()
	RocketChat.callbacks[hook] ?= []

	# Avoid adding the same callback twice
	for cb in RocketChat.callbacks[hook]
		if cb.id is callback.id
			return

	RocketChat.callbacks[hook].push callback
	return

###
# Remove a callback from a hook
# @param {string} hook - The name of the hook
# @param {string} id - The callback's id
###

RocketChat.callbacks.remove = (hookName, id) ->
	RocketChat.callbacks[hookName] = _.reject RocketChat.callbacks[hookName], (callback) ->
		callback.id is id
	return

###
# Successively run all of a hook's callbacks on an item
# @param {String} hook - The name of the hook
# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks
# @param {Object} [constant] - An optional constant that will be passed along to each callback
# @returns {Object} Returns the item after it's been through all the callbacks for this hook
###

RocketChat.callbacks.run = (hook, item, constant) ->
	callbacks = RocketChat.callbacks[hook]
	if !!callbacks?.length
		# if the hook exists, and contains callbacks to run
		_.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.callbacks.priority.MEDIUM).reduce (result, callback) ->
			# console.log(callback.name);
			callback result, constant
		, item
	else
		# else, just return the item unchanged
		item

###
# Successively run all of a hook's callbacks on an item, in async mode (only works on server)
# @param {String} hook - The name of the hook
# @param {Object} item - The post, comment, modifier, etc. on which to run the callbacks
# @param {Object} [constant] - An optional constant that will be passed along to each callback
###

RocketChat.callbacks.runAsync = (hook, item, constant) ->
	callbacks = RocketChat.callbacks[hook]
	if Meteor.isServer and !!callbacks?.length
		# use defer to avoid holding up client
		Meteor.defer ->
			# run all post submit server callbacks on post object successively
			_.sortBy(callbacks, (callback) -> return callback.priority or RocketChat.callbacks.priority.MEDIUM).forEach (callback) ->
				# console.log(callback.name);
				callback item, constant
				return
			return
	else
		return item
	return