import { INodeProperties } from 'n8n-workflow';

export const videoOperations: INodeProperties[] = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		noDataExpression: true,
		displayOptions: {
			show: {
				resource: ['video'],
			},
		},
		options: [
			{
				name: 'Delete',
				value: 'delete',
				description: 'Delete a video',
				action: 'Delete a video',
			},
			{
				name: 'Get',
				value: 'get',
				description: 'Get a video',
				action: 'Get a video',
			},
			{
				name: 'Get Many',
				value: 'getAll',
				description: 'Retrieve many videos',
				action: 'Get many videos',
			},
			{
				name: 'Rate',
				value: 'rate',
				description: 'Rate a video',
				action: 'Rate a video',
			},
			{
				name: 'Update',
				value: 'update',
				description: 'Update a video',
				action: 'Update a video',
			},
			{
				name: 'Upload',
				value: 'upload',
				description: 'Upload a video',
				action: 'Upload a video',
			},
		],
		default: 'getAll',
	},
];

export const videoFields: INodeProperties[] = [
	/* -------------------------------------------------------------------------- */
	/*                                 video:upload                               */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Title',
		name: 'title',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				operation: ['upload'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
		displayName: 'Region Code',
		name: 'regionCode',
		type: 'options',
		description:
			'Choose from the list, or specify an ID using an expression',
		typeOptions: {
			loadOptionsMethod: 'getCountriesCodes',
		},
		displayOptions: {
			show: {
				operation: ['upload'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		displayName: 'Category Name or ID',
		name: 'categoryId',
		type: 'options',
		description:
			'Choose from the list, or specify an ID using an expression',
		typeOptions: {
			loadOptionsMethod: 'getVideoCategories',
			loadOptionsDependsOn: ['regionCode'],
		},
		displayOptions: {
			show: {
				operation: ['upload'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		displayName: 'Binary Property',
		name: 'binaryProperty',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				operation: ['upload'],
				resource: ['video'],
			},
		},
		default: 'data',
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				operation: ['upload'],
				resource: ['video'],
			},
		},
		options: [
			{
				displayName: 'Default Language Name or ID',
				name: 'defaultLanguage',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getLanguages',
				},
				default: '',
				description:
					'The language of the text in the playlist resource\'s title and description properties. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Description',
				name: 'description',
				type: 'string',
				default: '',
				description: "The playlist's description",
			},
			{
				displayName: 'Embeddable',
				name: 'embeddable',
				type: 'boolean',
				default: false,
				description: 'Whether the video can be embedded on another website',
			},
			{
				displayName: 'License',
				name: 'license',
				type: 'options',
				options: [
					{
						name: 'Creative Common',
						value: 'creativeCommon',
					},
					{
						name: 'Youtube',
						value: 'youtube',
					},
				],
				default: '',
				description: "The video's license",
			},
			{
				displayName: 'Notify Subscribers',
				name: 'notifySubscribers',
				type: 'boolean',
				default: false,
				description:
					"Whether YouTube should send a notification about the new video to users who subscribe to the video's channel",
			},
			{
				displayName: 'Privacy Status',
				name: 'privacyStatus',
				type: 'options',
				options: [
					{
						name: 'Private',
						value: 'private',
					},
					{
						name: 'Public',
						value: 'public',
					},
					{
						name: 'Unlisted',
						value: 'unlisted',
					},
				],
				default: '',
				description: "The playlist's privacy status",
			},
			{
				displayName: 'Public Stats Viewable',
				name: 'publicStatsViewable',
				type: 'boolean',
				default: true,
				description:
					"Whether the extended video statistics on the video's watch page are publicly viewable",
			},
			{
				displayName: 'Publish At',
				name: 'publishAt',
				type: 'dateTime',
				default: '',
				description:
					'If you set a value for this property, you must also set the status.privacyStatus property to private',
			},
			{
				displayName: 'Recording Date',
				name: 'recordingDate',
				type: 'dateTime',
				default: '',
				description: 'The date and time when the video was recorded',
			},
			{
				displayName: 'Self Declared Made For Kids',
				name: 'selfDeclaredMadeForKids',
				type: 'boolean',
				default: false,
				description:
					'Whether the video is designated as child-directed, and it contains the current "made for kids" status of the video',
			},
			{
				displayName: 'Tags',
				name: 'tags',
				type: 'string',
				default: '',
				description:
					'Keyword tags associated with the playlist. Mulplie can be defined separated by comma.',
			},
		],
	},
	/* -------------------------------------------------------------------------- */
	/*                                 video:delete                               */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Video ID',
		name: 'videoId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				operation: ['delete'],
				resource: ['video'],
			},
		},
		description: 'ID of the video',
		default: '',
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				operation: ['delete'],
				resource: ['video'],
			},
		},
		options: [
			{
				displayName: 'On Behalf Of Content Owner',
				name: 'onBehalfOfContentOwner',
				type: 'string',
				default: '',
				description:
					"The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value",
			},
		],
	},
	/* -------------------------------------------------------------------------- */
	/*                                 video:get                                  */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Video ID',
		name: 'videoId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				operation: ['get'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		displayName: 'Fields',
		name: 'part',
		type: 'multiOptions',
		options: [
			{
				name: '*',
				value: '*',
			},
			{
				name: 'Content Details',
				value: 'contentDetails',
			},
			{
				name: 'ID',
				value: 'id',
			},
			{
				name: 'Live Streaming Details',
				value: 'liveStreamingDetails',
			},
			{
				name: 'Localizations',
				value: 'localizations',
			},
			{
				name: 'Player',
				value: 'player',
			},
			{
				name: 'Recording Details',
				value: 'recordingDetails',
			},
			{
				name: 'Snippet',
				value: 'snippet',
			},
			{
				name: 'Statistics',
				value: 'statistics',
			},
			{
				name: 'Status',
				value: 'status',
			},
			{
				name: 'Topic Details',
				value: 'topicDetails',
			},
		],
		required: true,
		displayOptions: {
			show: {
				operation: ['get'],
				resource: ['video'],
			},
		},
		description:
			'The fields parameter specifies a comma-separated list of one or more video resource properties that the API response will include',
		default: ['*'],
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				operation: ['get'],
				resource: ['video'],
			},
		},
		options: [
			{
				displayName: 'On Behalf Of Content Owner',
				name: 'onBehalfOfContentOwner',
				type: 'string',
				default: '',
				description:
					"The onBehalfOfContentOwner parameter indicates that the request's authorization credentials identify a YouTube CMS user who is acting on behalf of the content owner specified in the parameter value",
			},
		],
	},
	/* -------------------------------------------------------------------------- */
	/*                                 video:getAll                               */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['video'],
			},
		},
		default: false,
		description: 'Whether to return all results or only up to a given limit',
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['video'],
				returnAll: [false],
			},
		},
		typeOptions: {
			minValue: 1,
			maxValue: 50,
		},
		default: 25,
		description: 'Max number of results to return',
	},
	{
		displayName: 'Filters',
		name: 'filters',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['video'],
			},
		},
		options: [
			{
				displayName: 'Channel ID',
				name: 'channelId',
				type: 'string',
				default: '',
				description:
					'The channelId parameter indicates that the API response should only contain resources created by the channel',
			},
			{
				displayName: 'For Developer',
				name: 'forDeveloper',
				type: 'boolean',
				default: false,
				description:
					"Whether to restrict the search to only retrieve videos uploaded via the developer's application or website",
			},
			{
				displayName: 'Published After',
				name: 'publishedAfter',
				type: 'dateTime',
				default: '',
				description:
					'The publishedAfter parameter indicates that the API response should only contain resources created at or after the specified time',
			},
			{
				displayName: 'Published Before',
				name: 'publishedBefore',
				type: 'dateTime',
				default: '',
				description:
					'The publishedBefore parameter indicates that the API response should only contain resources created before or at the specified time',
			},
			{
				displayName: 'Query',
				name: 'q',
				type: 'string',
				default: '',
				description: 'The q parameter specifies the query term to search for',
			},
			{
				// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
				displayName: 'Region Code',
				name: 'regionCode',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getCountriesCodes',
				},
				default: '',
				description:
					'The regionCode parameter instructs the API to select a video chart available in the specified region. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Related To Video ID',
				name: 'relatedToVideoId',
				type: 'string',
				default: '',
				description:
					'The relatedToVideoId parameter retrieves a list of videos that are related to the video that the parameter value identifies',
			},
			{
				displayName: 'Video Category ID',
				name: 'videoCategoryId',
				type: 'string',
				default: '',
				description:
					'The videoCategoryId parameter identifies the video category for which the chart should be retrieved',
			},
			{
				displayName: 'Video Syndicated',
				name: 'videoSyndicated',
				type: 'boolean',
				default: false,
				description:
					'Whether to restrict a search to only videos that can be played outside youtube.com',
			},
			{
				displayName: 'Video Type',
				name: 'videoType',
				type: 'options',
				options: [
					{
						name: 'Any',
						value: 'any',
					},
					{
						name: 'Episode',
						value: 'episode',
					},
					{
						name: 'Movie',
						value: 'movie',
					},
				],
				default: '',
				description:
					'The videoType parameter lets you restrict a search to a particular type of videos',
			},
		],
	},
	{
		displayName: 'Options',
		name: 'options',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				operation: ['getAll'],
				resource: ['video'],
			},
		},
		options: [
			{
				displayName: 'Order',
				name: 'order',
				type: 'options',
				options: [
					{
						name: 'Date',
						value: 'date',
					},
					{
						name: 'Relevance',
						value: 'relevance',
					},
				],
				default: 'relevance',
			},
			{
				displayName: 'Safe Search',
				name: 'safeSearch',
				type: 'options',
				options: [
					{
						name: 'Moderate',
						value: 'moderate',
						description:
							'YouTube will filter some content from search results and, at the least, will filter content that is restricted in your locale',
					},
					{
						name: 'None',
						value: 'none',
						description: 'YouTube will not filter the search result set',
					},
					{
						name: 'Strict',
						value: 'strict',
						description:
							'YouTube will try to exclude all restricted content from the search result set',
					},
				],
				default: '',
			},
		],
	},
	/* -------------------------------------------------------------------------- */
	/*                                 video:rate                                 */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Video ID',
		name: 'videoId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				operation: ['rate'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		displayName: 'Rating',
		name: 'rating',
		type: 'options',
		displayOptions: {
			show: {
				operation: ['rate'],
				resource: ['video'],
			},
		},
		options: [
			{
				name: 'Dislike',
				value: 'dislike',
				description: 'Records that the authenticated user disliked the video',
			},
			{
				name: 'Like',
				value: 'like',
				description: 'Records that the authenticated user liked the video',
			},
			{
				name: 'None',
				value: 'none',
				description:
					'Removes any rating that the authenticated user had previously set for the video',
			},
		],
		default: '',
	},
	/* -------------------------------------------------------------------------- */
	/*                                 video:update                               */
	/* -------------------------------------------------------------------------- */
	{
		displayName: 'Video ID',
		name: 'videoId',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		displayName: 'Title',
		name: 'title',
		type: 'string',
		required: true,
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
		displayName: 'Region Code',
		name: 'regionCode',
		type: 'options',
		description:
			'Choose from the list, or specify an ID using an expression',
		typeOptions: {
			loadOptionsMethod: 'getCountriesCodes',
		},
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		displayName: 'Category Name or ID',
		name: 'categoryId',
		type: 'options',
		description:
			'Choose from the list, or specify an ID using an expression',
		typeOptions: {
			loadOptionsMethod: 'getVideoCategories',
			loadOptionsDependsOn: ['regionCode'],
		},
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['video'],
			},
		},
		default: '',
	},
	{
		displayName: 'Update Fields',
		name: 'updateFields',
		type: 'collection',
		placeholder: 'Add Option',
		default: {},
		displayOptions: {
			show: {
				operation: ['update'],
				resource: ['video'],
			},
		},
		options: [
			{
				displayName: 'Default Language Name or ID',
				name: 'defaultLanguage',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getLanguages',
				},
				default: '',
				description:
					'The language of the text in the playlist resource\'s title and description properties. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Description',
				name: 'description',
				type: 'string',
				default: '',
				description: "The playlist's description",
			},
			{
				displayName: 'Embeddable',
				name: 'embeddable',
				type: 'boolean',
				default: false,
				description: 'Whether the video can be embedded on another website',
			},
			{
				displayName: 'License',
				name: 'license',
				type: 'options',
				options: [
					{
						name: 'Creative Common',
						value: 'creativeCommon',
					},
					{
						name: 'Youtube',
						value: 'youtube',
					},
				],
				default: '',
				description: "The video's license",
			},
			{
				displayName: 'Notify Subscribers',
				name: 'notifySubscribers',
				type: 'boolean',
				default: false,
				description:
					"Whether YouTube should send a notification about the new video to users who subscribe to the video's channel",
			},
			{
				displayName: 'Privacy Status',
				name: 'privacyStatus',
				type: 'options',
				options: [
					{
						name: 'Private',
						value: 'private',
					},
					{
						name: 'Public',
						value: 'public',
					},
					{
						name: 'Unlisted',
						value: 'unlistef',
					},
				],
				default: '',
				description: "The playlist's privacy status",
			},
			{
				displayName: 'Public Stats Viewable',
				name: 'publicStatsViewable',
				type: 'boolean',
				default: true,
				description:
					"Whether the extended video statistics on the video's watch page are publicly viewable",
			},
			{
				displayName: 'Publish At',
				name: 'publishAt',
				type: 'dateTime',
				default: '',
				description:
					'If you set a value for this property, you must also set the status.privacyStatus property to private',
			},
			{
				displayName: 'Recording Date',
				name: 'recordingDate',
				type: 'dateTime',
				default: '',
				description: 'The date and time when the video was recorded',
			},
			{
				displayName: 'Self Declared Made For Kids',
				name: 'selfDeclaredMadeForKids',
				type: 'boolean',
				default: false,
				description:
					'Whether the video is designated as child-directed, and it contains the current "made for kids" status of the video',
			},
			{
				displayName: 'Tags',
				name: 'tags',
				type: 'string',
				default: '',
				description:
					'Keyword tags associated with the playlist. Mulplie can be defined separated by comma.',
			},
		],
	},
];


return option + this.$locale.baseText('ndv.output.of') + (this.maxRunIndex+1) + itemsLabel;

getDataCount(runIndex: number, outputIndex: number) {

const runData: IRunData | null = this.workflowRunData;

if (this.pinData) {
return this.pinData.length;
}

import { IExecuteFunctions } from 'n8n-core';

import {
	IDataObject,
	ILoadOptionsFunctions,
	INodeExecutionData,
	INodePropertyOptions,
	INodeType,
	INodeTypeDescription,
	NodeApiError,
	NodeOperationError,
} from 'n8n-workflow';

import {
	getGoogleAnalyticsDomainsArray,
	getTags,
	getToEmailArray,
	mandrillApiRequest,
	validateJSON,
} from './GenericFunctions';

import moment from 'moment';

import _ from 'lodash';

interface Attachments {
	type: string;
	name: string;
	content: string;
}

interface Message {
	html?: string;
	text?: string;
	subject?: string;
	from_name?: string;
	from_email: string;
	to: string[];
	important?: boolean;
	track_opens?: boolean;
	track_clicks?: boolean;
	auto_text?: boolean;
	auto_html?: boolean;
	inline_css?: boolean;
	url_strip_qs?: boolean;
	preserve_recipients?: boolean;
	view_content_link?: boolean;
	async?: boolean;
	subaccount?: string;
	google_analytics_campaign?: string;
	google_analytics_domains?: string[];
	tags?: string[];
	ip_pool?: string;
	bcc_address?: string;
	tracking_domain?: string;
	signing_domain?: string;
	return_path_domain?: string;
	headers?: IDataObject;
	metadata?: IDataObject;
	global_merge_vars?: [IDataObject];
	attachments?: Attachments[];
}

interface Body {
	template_content: [];
	template_name?: string;
	message: Message;
	send_at?: string;
}

interface Options {
	html: string;
	fromName?: string;
	text: string;
	subject?: string;
	important: boolean;
	trackOpens: boolean;
	trackClicks: boolean;
	autoText: boolean;
	autoHtml: boolean;
	inlineCss: boolean;
	urlStripQs: boolean;
	preserveRecipients: boolean;
	viewContentLink: boolean;
	async: boolean;
	subaccount: string;
	googleAnalyticsCampaign: string;
	googleAnalyticsDomains?: string;
	tags: string;
	ipPool: string;
	bccAddress: string;
	trackingDomain: string;
	signingDomain: string;
	returnPathDomain: string;
	sendAt: string;
}

export class Mandrill implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Mandrill',
		name: 'mandrill',
		icon: 'file:mandrill.svg',
		group: ['output'],
		version: 1,
		subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
		description: 'Consume Mandrill API',
		defaults: {
			name: 'Mandrill',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'mandrillApi',
				required: true,
			},
		],
		properties: [
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Message',
						value: 'message',
						description: 'Send a message',
					},
				],
				default: 'message',
			},
			{
				displayName: 'Operation',
				name: 'operation',
				type: 'options',
				noDataExpression: true,
				displayOptions: {
					show: {
						resource: ['message'],
					},
				},
				options: [
					{
						name: 'Send Template',
						value: 'sendTemplate',
						description: 'Send message based on template',
						action: 'Send a message based on a template',
					},
					{
						name: 'Send HTML',
						value: 'sendHtml',
						description: 'Send message based on HTML',
						action: 'Send a message based on HTML',
					},
				],
				default: 'sendTemplate',
			},
			{
				displayName: 'Template Name or ID',
				name: 'template',
				type: 'options',
				typeOptions: {
					loadOptionsMethod: 'getTemplates',
				},
				displayOptions: {
					show: {
						operation: ['sendTemplate'],
					},
				},
				default: '',
				options: [],
				required: true,
				description:
					'The template you want to send. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'From Email',
				name: 'fromEmail',
				type: 'string',
				default: '',
				required: true,
				placeholder: 'Admin <example@yourdomain.com>',
				description: 'Email address of the sender optional with name',
				displayOptions: {
					show: {
						operation: ['sendHtml', 'sendTemplate'],
					},
				},
			},
			{
				displayName: 'To Email',
				name: 'toEmail',
				type: 'string',
				default: '',
				required: true,
				placeholder: 'info@example.com',
				description: 'Email address of the recipient. Multiple ones can be separated by comma.',
				displayOptions: {
					show: {
						operation: ['sendHtml', 'sendTemplate'],
					},
				},
			},
			{
				displayName: 'JSON Parameters',
				name: 'jsonParameters',
				type: 'boolean',
				default: false,
				displayOptions: {
					show: {
						operation: ['sendHtml', 'sendTemplate'],
					},
				},
			},
			{
				displayName: 'Options',
				name: 'options',
				type: 'collection',
				placeholder: 'Add Option',
				default: {},
				displayOptions: {
					show: {
						operation: ['sendHtml', 'sendTemplate'],
					},
				},
				options: [
					{
						displayName: 'Async',
						name: 'async',
						type: 'boolean',
						default: false,
						description:
							'Whether to enable a background sending mode that is optimized for bulk sending. In async mode, messages/send will immediately return a status of "queued" for every recipient. To handle rejections when sending in async mode, set up a webhook for the \'reject\' event. Defaults to false for messages with no more than 10 recipients; messages with more than 10 recipients are always sent asynchronously, regardless of the value of async.',
					},
					{
						displayName: 'Auto Text',
						name: 'autoText',
						type: 'boolean',
						default: false,
						description:
							'Whether or not to automatically generate a text part for messages that are not given text',
					},
					{
						displayName: 'Auto HTML',
						name: 'autoHtml',
						type: 'boolean',
						default: false,
						description:
							'Whether or not to automatically generate an HTML part for messages that are not given HTML',
					},
					{
						displayName: 'BCC Address',
						name: 'bccAddress',
						type: 'string',
						default: '',
						placeholder: 'message.bcc_address@example.com',
						description: "An optional address to receive an exact copy of each recipient's email",
					},
					{
						displayName: 'From Name',
						name: 'fromName',
						type: 'string',
						default: '',
						placeholder: 'John Doe',
						description: 'Optional from name to be used',
					},
					{
						displayName: 'Google Analytics Campaign',
						name: 'googleAnalyticsCampaign',
						type: 'string',
						default: '',
						placeholder: '',
						description:
							"Optional string indicating the value to set for the utm_campaign tracking parameter. If this isn't provided the email's from address will be used instead.",
					},
					{
						displayName: 'Google Analytics Domains',
						name: 'googleAnalyticsDomains',
						type: 'string',
						default: '',
						placeholder: '',
						description:
							'An array of strings separated by a comma (,) indicating for which any matching URLs will automatically have Google Analytics parameters appended to their query string automatically',
					},
					{
						displayName: 'HTML',
						name: 'html',
						type: 'string',
						default: '',
						typeOptions: {
							rows: 5,
						},
						options: [],
						description: 'The html you want to send',
					},
					{
						displayName: 'Important',
						name: 'important',
						type: 'boolean',
						default: false,
						description:
							'Whether or not this message is important, and should be delivered ahead of non-important messages',
					},
					{
						displayName: 'Inline CSS',
						name: 'inlineCss',
						type: 'boolean',
						default: false,
						description:
							'Whether or not to automatically inline all CSS styles provided in the message HTML - only for HTML documents less than 256KB in size',
					},
					{
						displayName: 'Ip Pool',
						name: 'ipPool',
						type: 'string',
						default: '',
						placeholder: '',
						description:
							'The name of the dedicated ip pool that should be used to send the message. If you do not have any dedicated IPs, this parameter has no effect. If you specify a pool that does not exist, your default pool will be used instead.',
					},
					{
						displayName: 'Preserve Recipients',
						name: 'preserveRecipients',
						type: 'boolean',
						default: false,
						description: 'Whether or not to expose all recipients in to "To" header for each email',
					},
					{
						displayName: 'Return Path Domain',
						name: 'returnPathDomain',
						type: 'string',
						default: '',
						placeholder: '',
						description: "A custom domain to use for the messages's return-path",
					},
					{
						displayName: 'Sent At',
						name: 'sendAt',
						type: 'dateTime',
						default: '',
						placeholder: '',
						description:
							'When this message should be sent as a UTC timestamp in YYYY-MM-DD HH:MM:SS format. If you specify a time in the past, the message will be sent immediately. An additional fee applies for scheduled email, and this feature is only available to accounts with a positive balance.',
					},
					{
						displayName: 'Signing Domain',
						name: 'signingDomain',
						type: 'string',
						default: '',
						placeholder: '',
						description:
							'A custom domain to use for SPF/DKIM signing instead of mandrill(for "via" or "on behalf of" in email clients)',
					},
					{
						displayName: 'Subaccount',
						name: 'subAccount',
						type: 'string',
						default: '',
						placeholder: '',
						description:
							'The unique ID of a subaccount for this message - must already exist or will fail with an error',
					},
					{
						displayName: 'Subject',
						name: 'subject',
						type: 'string',
						default: '',
						placeholder: 'My subject line',
						description: 'Subject line of the email',
					},
					{
						displayName: 'Tags',
						name: 'tags',
						type: 'string',
						default: '',
						placeholder: '',
						description:
							'An array of string separated by a comma (,) to tag the message with. Stats are accumulated using tags, though we only store the first 100 we see, so this should not be unique or change frequently. Tags should be 50 characters or less. Any tags starting with an underscore are reserved for internal use and will cause errors.',
					},
					{
						displayName: 'Text',
						name: 'text',
						type: 'string',
						default: '',
						typeOptions: {
							rows: 5,
						},
						options: [],
						description: 'Example text content',
					},
					{
						displayName: 'Track Clicks',
						name: 'trackClicks',
						type: 'boolean',
						default: false,
						description: 'Whether or not to turn on click tracking for the message',
					},
					{
						displayName: 'Track Opens',
						name: 'trackOpens',
						type: 'boolean',
						default: false,
						description: 'Whether or not to turn on open tracking for the message',
					},
					{
						displayName: 'Tracking Domain',
						name: 'trackingDomain',
						type: 'string',
						default: '',
						placeholder: '',
						description:
							'A custom domain to use for tracking opens and clicks instead of mandrillapp.com',
					},
					{
						displayName: 'Url Strip Qs',
						name: 'urlStripQs',
						type: 'boolean',
						default: false,
						description:
							'Whether or not to strip the query string from URLs when aggregating tracked URL data',
					},
					{
						displayName: 'View Content Link',
						name: 'viewContentLink',
						type: 'boolean',
						default: false,
						description: 'Whether to remove content logging for sensitive emails',
					},
				],
			},
			{
				displayName: 'Merge Vars',
				name: 'mergeVarsJson',
				type: 'json',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				default: '',
				placeholder: `[
	{
		{ "name": "name", "content": "content" }
	}
]`,
				displayOptions: {
					show: {
						jsonParameters: [true],
					},
				},
				description: 'Global merge variables',
			},
			{
				displayName: 'Merge Vars',
				name: 'mergeVarsUi',
				placeholder: 'Add Merge Vars',
				type: 'fixedCollection',
				default: {},
				typeOptions: {
					multipleValues: true,
				},
				displayOptions: {
					show: {
						jsonParameters: [false],
					},
				},
				description: 'Per-recipient merge variables',
				options: [
					{
						name: 'mergeVarsValues',
						displayName: 'Vars',
						values: [
							{
								displayName: 'Name',
								name: 'name',
								type: 'string',
								default: '',
							},
							{
								displayName: 'Content',
								name: 'content',
								type: 'string',
								default: '',
							},
						],
					},
				],
			},
			{
				displayName: 'Metadata',
				name: 'metadataUi',
				placeholder: 'Add Metadata',
				type: 'fixedCollection',
				default: {},
				typeOptions: {
					multipleValues: true,
				},
				displayOptions: {
					show: {
						jsonParameters: [false],
					},
				},
				description:
					'Metadata an associative array of user metadata. Mandrill will store this metadata and make it available for retrieval. In addition, you can select up to 10 metadata fields to index and make searchable using the Mandrill search api.',
				options: [
					{
						name: 'metadataValues',
						displayName: 'Metadata',
						values: [
							{
								displayName: 'Name',
								name: 'name',
								type: 'string',
								default: 'Name of the metadata key to add.',
							},
							{
								displayName: 'Value',
								name: 'value',
								type: 'string',
								default: '',
								description: 'Value to set for the metadata key',
							},
						],
					},
				],
			},
			{
				displayName: 'Metadata',
				name: 'metadataJson',
				type: 'json',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				displayOptions: {
					show: {
						jsonParameters: [true],
					},
				},
				default: '',
				placeholder: `{
	"website": "www.example.com"
}`,
				description:
					'Metadata an associative array of user metadata. Mandrill will store this metadata and make it available for retrieval. In addition, you can select up to 10 metadata fields to index and make searchable using the Mandrill search api.',
			},
			{
				displayName: 'Attachments',
				name: 'attachmentsJson',
				type: 'json',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				displayOptions: {
					show: {
						jsonParameters: [true],
					},
				},
				default: '',
				placeholder: `[
	{
		"type": "text/plain" (the MIME type of the attachment),
		"name": "myfile.txt" (the file name of the attachment),
		"content": "ZXhhbXBsZSBmaWxl" (the content of the attachment as a base64-encoded string)
	}
]`,
				description: 'An array of supported attachments to add to the message',
			},
			{
				displayName: 'Attachments',
				name: 'attachmentsUi',
				placeholder: 'Add Attachments',
				type: 'fixedCollection',
				typeOptions: {
					multipleValues: true,
				},
				displayOptions: {
					show: {
						jsonParameters: [false],
					},
				},
				options: [
					{
						name: 'attachmentsValues',
						displayName: 'Attachments Values',
						values: [
							{
								displayName: 'Type',
								name: 'type',
								type: 'string',
								default: '',
								placeholder: 'text/plain',
								description: 'The MIME type of the attachment',
							},
							{
								displayName: 'Name',
								name: 'name',
								type: 'string',
								default: '',
								placeholder: 'myfile.txt',
								description: 'The file name of the attachment',
							},
							{
								displayName: 'Content',
								name: 'content',
								type: 'string',
								default: '',
								placeholder: 'ZXhhbXBsZSBmaWxl',
								description: 'The content of the attachment as a base64-encoded string',
							},
						],
					},
					{
						name: 'attachmentsBinary',
						displayName: 'Attachments Binary',
						values: [
							{
								displayName: 'Property',
								name: 'property',
								type: 'string',
								default: '',
								description:
									'Name of the binary properties which contain data which should be added to email as attachment',
							},
						],
					},
				],
				default: {},
				description: 'Array of supported attachments to add to the message',
			},
			{
				displayName: 'Headers',
				name: 'headersJson',
				type: 'json',
				default: '',
				placeholder: `{
	"Reply-To": "replies@example.com"
}`,
				displayOptions: {
					show: {
						jsonParameters: [true],
					},
				},
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				description: 'Optional extra headers to add to the message (most headers are allowed)',
			},
			{
				displayName: 'Headers',
				name: 'headersUi',
				placeholder: 'Add Headers',
				type: 'fixedCollection',
				default: {},
				typeOptions: {
					multipleValues: true,
				},
				displayOptions: {
					show: {
						jsonParameters: [false],
					},
				},
				options: [
					{
						name: 'headersValues',
						displayName: 'Values',
						values: [
							{
								displayName: 'Name',
								name: 'name',
								type: 'string',
								default: '',
							},
							{
								displayName: 'Value',
								name: 'value',
								type: 'string',
								default: '',
							},
						],
					},
				],
				description: 'Optional extra headers to add to the message (most headers are allowed)',
			},
		],
	};

	methods = {
		loadOptions: {
			// Get all the available templates to display them to user so that he can
			// select them easily
			async getTemplates(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
				const returnData: INodePropertyOptions[] = [];
				let templates;
				try {
					templates = await mandrillApiRequest.call(this, '/templates', 'POST', '/list');
				} catch (error) {
					throw new NodeApiError(this.getNode(), error);
				}
				for (const template of templates) {
					const templateName = template.name;
					const templateSlug = template.slug;

					returnData.push({
						name: templateName,
						value: templateSlug,
					});
				}

				return returnData;
			},
		},
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const returnData: INodeExecutionData[] = [];
		const items = this.getInputData();
		let responseData;
		let emailSentResponse;
		const resource = this.getNodeParameter('resource', 0) as string;
		const operation = this.getNodeParameter('operation', 0) as string;

		for (let i = 0; i < items.length; i++) {
			try {
				if (resource === 'message') {
					const options = this.getNodeParameter('options', i) as Options;
					const fromEmail = this.getNodeParameter('fromEmail', i) as string;
					const toEmail = this.getNodeParameter('toEmail', i) as string;
					const jsonActive = this.getNodeParameter('jsonParameters', i) as boolean;
					const toEmailArray = getToEmailArray(toEmail);

					const message: Message = {
						html: options.html ? options.html : '',
						text: options.text ? options.text : '',
						subject: options.subject ? options.subject : '',
						from_email: fromEmail,
						to: toEmailArray,
						important: options.important ? options.important : false,
						track_opens: options.trackOpens ? options.trackOpens : false,
						track_clicks: options.trackClicks ? options.trackClicks : false,
						auto_text: options.autoText ? options.autoText : false,
						auto_html: options.autoHtml ? options.autoHtml : false,
						inline_css: options.inlineCss ? options.inlineCss : false,
						url_strip_qs: options.urlStripQs ? options.urlStripQs : false,
						preserve_recipients: options.preserveRecipients ? options.preserveRecipients : false,
						view_content_link: options.viewContentLink ? options.viewContentLink : false,
						async: options.async ? options.async : false,
						google_analytics_campaign: options.googleAnalyticsCampaign
							? options.googleAnalyticsCampaign
							: '',
						ip_pool: options.ipPool ? options.ipPool : '',
						bcc_address: options.bccAddress ? options.bccAddress : '',
						tracking_domain: options.trackingDomain ? options.trackingDomain : '',
						signing_domain: options.signingDomain ? options.signingDomain : '',
						return_path_domain: options.returnPathDomain ? options.returnPathDomain : '',
					};

					if (options.googleAnalyticsDomains) {
						message.google_analytics_domains = getGoogleAnalyticsDomainsArray(
							options.googleAnalyticsDomains,
						);
					}

					if (options.tags) {
						message.tags = getTags(options.tags);
					}

					if (options.fromName) {
						message.from_name = options.fromName;
					}

					if (options.subaccount) {
						message.subaccount = options.subaccount;
					}

					const body: Body = {
						template_content: [],
						message,
					};

					if (options.sendAt) {
						body.send_at = moment(options.sendAt).utc().format('YYYY-MM-DD HH:mm:ss');
					}

					if (jsonActive) {
						body.message.headers = validateJSON(this.getNodeParameter('headersJson', i) as string);
						body.message.metadata = validateJSON(
							this.getNodeParameter('metadataJson', i) as string,
						);
						body.message.global_merge_vars = validateJSON(
							this.getNodeParameter('mergeVarsJson', i) as string,
						);
						body.message.attachments = validateJSON(
							this.getNodeParameter('attachmentsJson', i) as string,
						);
					} else {
						const headersUi = this.getNodeParameter('headersUi', i) as IDataObject;
						if (!_.isEmpty(headersUi)) {
							// @ts-ignore
							body.message.headers = _.map(headersUi.headersValues, (o) => {
								const aux: IDataObject = {};
								// @ts-ignore
								aux[o.name] = o.value;
								return aux;
							});
						}

						const metadataUi = this.getNodeParameter('metadataUi', i) as IDataObject;
						if (!_.isEmpty(metadataUi)) {
							// @ts-ignore
							body.message.metadata = _.map(metadataUi.metadataValues, (o: IDataObject) => {
								const aux: IDataObject = {};
								aux[o.name as string] = o.value;
								return aux;
							});
						}

						const mergeVarsUi = this.getNodeParameter('mergeVarsUi', i) as IDataObject;
						if (!_.isEmpty(mergeVarsUi)) {
							// @ts-ignore
							body.message.global_merge_vars = _.map(
								// @ts-ignore
								mergeVarsUi.mergeVarsValues,
								(o: IDataObject) => {
									const aux: IDataObject = {};
									aux.name = o.name;
									aux.content = o.content;
									return aux;
								},
							);
						}

						const attachmentsUi = this.getNodeParameter('attachmentsUi', i) as IDataObject;
						let attachmentsBinary: Attachments[] = [],
							attachmentsValues: Attachments[] = [];
						if (!_.isEmpty(attachmentsUi)) {
							if (
								attachmentsUi.hasOwnProperty('attachmentsValues') &&
								!_.isEmpty(attachmentsUi.attachmentsValues)
							) {
								// @ts-ignore
								attachmentsValues = _.map(attachmentsUi.attachmentsValues, (o: IDataObject) => {
									const aux: IDataObject = {};
									// @ts-ignore
									aux.name = o.name;
									aux.content = o.content;
									aux.type = o.type;
									return aux;
								});
							}

							if (
								attachmentsUi.hasOwnProperty('attachmentsBinary') &&
								!_.isEmpty(attachmentsUi.attachmentsBinary) &&
								items[i].binary
							) {
								// @ts-ignore
								attachmentsBinary = _.map(attachmentsUi.attachmentsBinary, (o: IDataObject) => {
									if (items[i].binary!.hasOwnProperty(o.property as string)) {
										const aux: IDataObject = {};
										aux.name = items[i].binary![o.property as string].fileName || 'unknown';
										aux.content = items[i].binary![o.property as string].data;
										aux.type = items[i].binary![o.property as string].mimeType;
										return aux;
									}
								});
							}
						}

						body.message.attachments = attachmentsBinary.concat(attachmentsValues);
					}

					if (operation === 'sendTemplate') {
						const template = this.getNodeParameter('template', i) as string;
						body.template_name = template;
						emailSentResponse = mandrillApiRequest.call(
							this,
							'/messages',
							'POST',
							'/send-template',
							body,
						);
					} else if (operation === 'sendHtml') {
						emailSentResponse = mandrillApiRequest.call(this, '/messages', 'POST', '/send', body);
					}

					responseData = await emailSentResponse;
				}

				const executionData = this.helpers.constructExecutionMetaData(
					this.helpers.returnJsonArray(responseData),
					{ itemData: { item: i } },
				);

				returnData.push(...executionData);
			} catch (error) {
				if (this.continueOnFail()) {
					const executionErrorData = this.helpers.constructExecutionMetaData(
						this.helpers.returnJsonArray({ error: error.message }),
						{ itemData: { item: i } },
					);
					returnData.push(...executionErrorData);
					continue;
				}
				throw error;
			}
		}
		return this.prepareOutputData(returnData);
	}
}


if (!room || !(await canAccessRoomAsync(room, { _id: uid }))) {

const user: IUser | null = await Users.findOneById(uid, { fields: { username: 1 } });

const { cursor, totalCount } = Messages.findVisibleByMentionAndRoomId(user.username, roomId, {

keep keep keep replace keep keep keep keep keep

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

import { IRoom, ISubscription } from '@rocket.chat/core-typings';
import { Rooms } from '@rocket.chat/models';

import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Subscriptions } from '../../../models/server';
import { adminFields } from '../../../../lib/rooms/adminFields';

export async function findAdminRooms({
	uid,
	filter,
	types = [],
	pagination: { offset, count, sort },
}: {
	uid: string;
	filter: string;
	types: string[];
	pagination: { offset: number; count: number; sort: [string, number][] };
}): Promise<{
	rooms: IRoom[];
	count: number;
	offset: number;
	total: number;
}> {
	if (!(await hasPermissionAsync(uid, 'view-room-administration'))) {
		throw new Error('error-not-authorized');
	}
	const name = filter?.trim();
	const discussion = types?.includes('discussions');
	const includeTeams = types?.includes('teams');
	const showOnlyTeams = types.length === 1 && types.includes('teams');
	const typesToRemove = ['discussions', 'teams'];
	const showTypes = Array.isArray(types) ? types.filter((type) => !typesToRemove.includes(type)) : [];
	const options = {
		fields: adminFields,
		sort: sort || { default: -1, name: 1 },
		skip: offset,
		limit: count,
	};

	let result;
	if (name && showTypes.length) {
		result = Rooms.findByNameContainingAndTypes(name, showTypes, discussion, includeTeams, showOnlyTeams, options);
	} else if (showTypes.length) {
		result = Rooms.findByTypes(showTypes, discussion, includeTeams, showOnlyTeams, options);
	} else {
		result = Rooms.findByNameContaining(name, discussion, includeTeams, showOnlyTeams, options);
	}

	const { cursor, totalCount } = result;

	const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]);

	return {
		rooms,
		count: rooms.length,
		offset,
		total,
	};
}

export async function findAdminRoom({ uid, rid }: { uid: string; rid: string }): Promise<unknown> {
	if (!(await hasPermissionAsync(uid, 'view-room-administration'))) {
		throw new Error('error-not-authorized');
	}

	return Rooms.findOneById(rid, { fields: adminFields });
}

export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{
	items: IRoom[];
}> {
	const options = {
		fields: {
			_id: 1,
			fname: 1,
			name: 1,
			t: 1,
			avatarETag: 1,
		},
		limit: 10,
		sort: {
			name: 1,
		},
	};

	const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } })
		.fetch()
		.map((item: Pick<ISubscription, 'rid'>) => item.rid);

	const rooms = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options).toArray();

	return {
		items: rooms,
	};
}

export async function findAdminRoomsAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{
	items: IRoom[];
}> {
	if (!(await hasAtLeastOnePermissionAsync(uid, ['view-room-administration', 'can-audit']))) {
		throw new Error('error-not-authorized');
	}
	const options = {
		fields: {
			_id: 1,
			fname: 1,
			name: 1,
			t: 1,
			avatarETag: 1,
		},
		limit: 10,
		sort: {
			name: 1,
		},
	};

	const rooms = await Rooms.findRoomsByNameOrFnameStarting(selector.name, options).toArray();

	return {
		items: rooms,
	};
}

export async function findChannelAndPrivateAutocompleteWithPagination({
	uid,
	selector,
	pagination: { offset, count, sort },
}: {
	uid: string;
	selector: { name: string };
	pagination: { offset: number; count: number; sort: [string, number][] };
}): Promise<{
	items: IRoom[];
	total: number;
}> {
	const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } })
		.fetch()
		.map((item: Pick<ISubscription, 'rid'>) => item.rid);

	const options = {
		fields: {
			_id: 1,
			fname: 1,
			name: 1,
			t: 1,
			avatarETag: 1,
		},
		sort: sort || { name: 1 },
		skip: offset,
		limit: count,
	};

	const { cursor, totalCount } = Rooms.findPaginatedRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options);

	const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]);

	return {
		items: rooms,
		total,
	};
}

export async function findRoomsAvailableForTeams({ uid, name }: { uid: string; name: string }): Promise<{
	items: IRoom[];
}> {
	const options = {
		fields: {
			_id: 1,
			fname: 1,
			name: 1,
			t: 1,
			avatarETag: 1,
		},
		limit: 10,
		sort: {
			name: 1,
		},
	};

	const userRooms = (
		Subscriptions.findByUserIdAndRoles(uid, ['owner'], { fields: { rid: 1 } }).fetch() as Pick<ISubscription, 'rid'>[]
	).map((item) => item.rid);

	const rooms = await Rooms.findChannelAndGroupListWithoutTeamsByNameStartingByOwner(uid, name, userRooms, options).toArray();

	return {
		items: rooms,
	};
}


import { ImporterInfo } from '../../importer/lib/ImporterInfo';

export class CsvImporterInfo extends ImporterInfo {
	constructor() {
		super('csv', 'CSV', 'application/zip', [
			{
				text: 'Importer_CSV_Information',
				href: 'https://rocket.chat/docs/administrator-guides/import/csv/',
			},
		]);
	}
}


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

const ColTitle: FC = ({ children }) => (
	<Box fontScale='c2' m='none'>
		{children}
	</Box>
);

export default ColTitle;


import { Table } from '@rocket.chat/fuselage';
import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRouteParameter, useRoute, usePermission, useTranslation } from '@rocket.chat/ui-contexts';
import React, { useMemo, useCallback, useState } from 'react';

import GenericTable from '../../../components/GenericTable';
import { useEndpointData } from '../../../hooks/useEndpointData';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
import CustomFieldsPage from './CustomFieldsPage';
import EditCustomFieldsPage from './EditCustomFieldsPageContainer';
import NewCustomFieldsPage from './NewCustomFieldsPage';
import RemoveCustomFieldButton from './RemoveCustomFieldButton';

const sortDir = (sortDir) => (sortDir === 'asc' ? 1 : -1);

const useQuery = ({ text, itemsPerPage, current }, [column, direction]) =>
	useMemo(
		() => ({
			fields: JSON.stringify({ label: 1 }),
			text,
			sort: JSON.stringify({ [column]: sortDir(direction) }),
			...(itemsPerPage && { count: itemsPerPage }),
			...(current && { offset: current }),
		}),
		[text, itemsPerPage, current, column, direction],
	);

const CustomFieldsRoute = () => {
	const t = useTranslation();
	const canViewCustomFields = usePermission('view-livechat-customfields');

	const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 });
	const [sort, setSort] = useState(['field', 'asc']);

	const debouncedParams = useDebouncedValue(params, 500);
	const debouncedSort = useDebouncedValue(sort, 500);
	const query = useQuery(debouncedParams, debouncedSort);

	const departmentsRoute = useRoute('omnichannel-customfields');
	const context = useRouteParameter('context');

	const onHeaderClick = useMutableCallback((id) => {
		const [sortBy, sortDirection] = sort;

		if (sortBy === id) {
			setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
			return;
		}
		setSort([id, 'asc']);
	});

	const onRowClick = useMutableCallback(
		(id) => () =>
			departmentsRoute.push({
				context: 'edit',
				id,
			}),
	);

	const { value: data, reload } = useEndpointData('/v1/livechat/custom-fields', query);

	const header = useMemo(
		() =>
			[
				<GenericTable.HeaderCell key={'field'} direction={sort[1]} active={sort[0] === '_id'} onClick={onHeaderClick} sort='_id'>
					{t('Field')}
				</GenericTable.HeaderCell>,
				<GenericTable.HeaderCell key={'label'} direction={sort[1]} active={sort[0] === 'label'} onClick={onHeaderClick} sort='label'>
					{t('Label')}
				</GenericTable.HeaderCell>,
				<GenericTable.HeaderCell key={'scope'} direction={sort[1]} active={sort[0] === 'scope'} onClick={onHeaderClick} sort='scope'>
					{t('Scope')}
				</GenericTable.HeaderCell>,
				<GenericTable.HeaderCell
					key={'visibility'}
					direction={sort[1]}
					active={sort[0] === 'visibility'}
					onClick={onHeaderClick}
					sort='visibility'
				>
					{t('Visibility')}
				</GenericTable.HeaderCell>,
				<GenericTable.HeaderCell key={'remove'} w='x60'>
					{t('Remove')}
				</GenericTable.HeaderCell>,
			].filter(Boolean),
		[sort, onHeaderClick, t],
	);

	const renderRow = useCallback(
		({ label, _id, scope, visibility }) => (
			<Table.Row key={_id} tabIndex={0} role='link' onClick={onRowClick(_id)} action qa-user-id={_id}>
				<Table.Cell withTruncatedText>{_id}</Table.Cell>
				<Table.Cell withTruncatedText>{label}</Table.Cell>
				<Table.Cell withTruncatedText>{scope === 'visitor' ? t('Visitor') : t('Room')}</Table.Cell>
				<Table.Cell withTruncatedText>{visibility === 'visible' ? t('Visible') : t('Hidden')}</Table.Cell>
				<RemoveCustomFieldButton _id={_id} reload={reload} />
			</Table.Row>
		),
		[onRowClick, reload, t],
	);

	if (!canViewCustomFields) {
		return <NotAuthorizedPage />;
	}

	if (context === 'new') {
		return <NewCustomFieldsPage reload={reload} />;
	}

	if (context === 'edit') {
		return <EditCustomFieldsPage reload={reload} />;
	}

	return (
		<CustomFieldsPage
			setParams={setParams}
			params={params}
			onHeaderClick={onHeaderClick}
			data={data}
			useQuery={useQuery}
			reload={reload}
			header={header}
			renderRow={renderRow}
			title={t('Custom_Fields')}
		/>
	);
};

export default CustomFieldsRoute;


const room = await Rooms.findOneById(roomId);

const user = await Users.findOneById(uid, { fields: { username: 1 } });

keep keep keep keep keep replace keep keep keep

import {
	BulkWriteOptions,
	ChangeStream,
	Collection,
	CollectionOptions,
	Db,
	Filter,
	FindOneAndUpdateOptions,
	IndexDescription,
	InsertOneOptions,
	ModifyResult,
	ObjectId,
	OptionalUnlessRequiredId,
	UpdateFilter,
	WithId,
	UpdateOptions,
	Document,
	FindOptions,
	FindCursor,
	UpdateResult,
	InsertManyResult,
	InsertOneResult,
	DeleteResult,
	DeleteOptions,
} from 'mongodb';
import type { IRocketChatRecord, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { IBaseModel, DefaultFields, ResultFields, FindPaginated, InsertionModel } from '@rocket.chat/model-typings';
import { getCollectionName } from '@rocket.chat/models';

import { setUpdatedAt } from '../../../app/models/server/lib/setUpdatedAt';

const warnFields =
	process.env.NODE_ENV !== 'production' || process.env.SHOW_WARNINGS === 'true'
		? (...rest: any): void => {
				console.warn(...rest, new Error().stack);
		  }
		: new Function();

type ModelOptions = {
	preventSetUpdatedAt?: boolean;
	collectionNameResolver?: (name: string) => string;
	collection?: CollectionOptions;
};

export abstract class BaseRaw<T, C extends DefaultFields<T> = undefined> implements IBaseModel<T, C> {
	public readonly defaultFields: C;

	public readonly col: Collection<T>;

	private preventSetUpdatedAt: boolean;

	/**
	 * Collection name to store data.
	 */
	private collectionName: string;

	/**
	 * @param db MongoDB instance
	 * @param name Name of the model without any prefix. Used by trash records to set the `__collection__` field.
	 * @param trash Trash collection instance
	 * @param options Model options
	 */
	constructor(private db: Db, protected name: string, protected trash?: Collection<RocketChatRecordDeleted<T>>, options?: ModelOptions) {
		this.collectionName = options?.collectionNameResolver ? options.collectionNameResolver(name) : getCollectionName(name);

		this.col = this.db.collection(this.collectionName, options?.collection || {});

		const indexes = this.modelIndexes();
		if (indexes?.length) {
			this.col.createIndexes(indexes).catch((e) => {
				console.warn(`Some indexes for collection '${this.collectionName}' could not be created:\n\t${e.message}`);
			});
		}

		this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false;
	}

	protected modelIndexes(): IndexDescription[] | void {
		// noop
	}

	private doNotMixInclusionAndExclusionFields(options: FindOptions<T> = {}): FindOptions<T> {
		const optionsDef = this.ensureDefaultFields(options);
		if (optionsDef?.projection === undefined) {
			return optionsDef;
		}

		const projection: Record<string, any> = optionsDef?.projection;
		const keys = Object.keys(projection);
		const removeKeys = keys.filter((key) => projection[key] === 0);
		if (keys.length > removeKeys.length) {
			removeKeys.forEach((key) => delete projection[key]);
		}

		return {
			...optionsDef,
			projection,
		};
	}

	private ensureDefaultFields<P>(options: FindOptions<P>): FindOptions<P>;

	private ensureDefaultFields<P>(options?: any): FindOptions<P> | undefined | FindOptions<T> {
		if (this.defaultFields === undefined) {
			return options;
		}

		const { fields: deprecatedFields, projection, ...rest } = options || {};

		if (deprecatedFields) {
			warnFields("Using 'fields' in models is deprecated.", options);
		}

		const fields = { ...deprecatedFields, ...projection };

		return {
			projection: this.defaultFields,
			...(fields && Object.values(fields).length && { projection: fields }),
			...rest,
		};
	}

	public findOneAndUpdate(query: Filter<T>, update: UpdateFilter<T> | T, options?: FindOneAndUpdateOptions): Promise<ModifyResult<T>> {
		return this.col.findOneAndUpdate(query, update, options || {});
	}

	async findOneById(_id: string, options?: FindOptions<T> | undefined): Promise<T | null>;

	async findOneById<P = T>(_id: string, options?: FindOptions<P>): Promise<P | null>;

	async findOneById(_id: string, options?: any): Promise<T | null> {
		const query = { _id } as unknown as Filter<T>;
		if (options) {
			return this.findOne(query, options);
		}
		return this.findOne(query);
	}

	async findOne(query?: Filter<T> | string, options?: undefined): Promise<T | null>;

	async findOne<P = T>(query: Filter<T> | string, options: FindOptions<P extends T ? T : P>): Promise<P | null>;

	async findOne<P>(query: Filter<T> | string = {}, options?: any): Promise<WithId<T> | WithId<P> | null> {
		const q = typeof query === 'string' ? ({ _id: query } as unknown as Filter<T>) : query;

		const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
		if (optionsDef) {
			return this.col.findOne(q, optionsDef);
		}
		return this.col.findOne(q);
	}

	// findUsersInRoles(): void {
	// 	throw new Error('[overwrite-function] You must overwrite this function in the extended classes');
	// }

	find(query?: Filter<T>): FindCursor<ResultFields<T, C>>;

	find<P = T>(query: Filter<T>, options: FindOptions<P extends T ? T : P>): FindCursor<P>;

	find<P>(query: Filter<T> | undefined = {}, options?: FindOptions<P extends T ? T : P>): FindCursor<WithId<P>> | FindCursor<WithId<T>> {
		const optionsDef = this.doNotMixInclusionAndExclusionFields(options);
		return this.col.find(query, optionsDef);
	}

	findPaginated<P = T>(query: Filter<T>, options?: FindOptions<P extends T ? T : P>): FindPaginated<FindCursor<WithId<P>>>;

	findPaginated(query: Filter<T> | undefined = {}, options?: any): FindPaginated<FindCursor<WithId<T>>> {
		const optionsDef = this.doNotMixInclusionAndExclusionFields(options);

		const cursor = optionsDef ? this.col.find(query, optionsDef) : this.col.find(query);
		const totalCount = this.col.countDocuments(query);

		return {
			cursor,
			totalCount,
		};
	}

	/**
	 * @deprecated use updateOne or updateAny instead
	 */
	update(
		filter: Filter<T>,
		update: UpdateFilter<T> | Partial<T>,
		options?: UpdateOptions & { multi?: true },
	): Promise<UpdateResult | Document> {
		const operation = options?.multi ? 'updateMany' : 'updateOne';

		return this[operation](filter, update, options);
	}

	updateOne(filter: Filter<T>, update: UpdateFilter<T> | Partial<T>, options?: UpdateOptions): Promise<UpdateResult> {
		this.setUpdatedAt(update);
		if (options) {
			return this.col.updateOne(filter, update, options);
		}
		return this.col.updateOne(filter, update);
	}

	updateMany(filter: Filter<T>, update: UpdateFilter<T> | Partial<T>, options?: UpdateOptions): Promise<Document | UpdateResult> {
		this.setUpdatedAt(update);
		if (options) {
			return this.col.updateMany(filter, update, options);
		}
		return this.col.updateMany(filter, update);
	}

	insertMany(docs: InsertionModel<T>[], options?: BulkWriteOptions): Promise<InsertManyResult<T>> {
		docs = docs.map((doc) => {
			if (!doc._id || typeof doc._id !== 'string') {
				const oid = new ObjectId();
				return { _id: oid.toHexString(), ...doc };
			}
			this.setUpdatedAt(doc);
			return doc;
		});

		// TODO reavaluate following type casting
		return this.col.insertMany(docs as unknown as Array<OptionalUnlessRequiredId<T>>, options || {});
	}

	insertOne(doc: InsertionModel<T>, options?: InsertOneOptions): Promise<InsertOneResult<T>> {
		if (!doc._id || typeof doc._id !== 'string') {
			const oid = new ObjectId();
			doc = { _id: oid.toHexString(), ...doc };
		}

		this.setUpdatedAt(doc);

		// TODO reavaluate following type casting
		return this.col.insertOne(doc as unknown as OptionalUnlessRequiredId<T>, options || {});
	}

	removeById(_id: string): Promise<DeleteResult> {
		return this.deleteOne({ _id } as unknown as Filter<T>);
	}

	async deleteOne(filter: Filter<T>, options?: DeleteOptions & { bypassDocumentValidation?: boolean }): Promise<DeleteResult> {
		if (!this.trash) {
			if (options) {
				return this.col.deleteOne(filter, options);
			}
			return this.col.deleteOne(filter);
		}

		const doc = (await this.findOne(filter)) as unknown as (IRocketChatRecord & T) | undefined;

		if (doc) {
			const { _id, ...record } = doc;

			const trash = {
				...record,
				_deletedAt: new Date(),
				__collection__: this.name,
			} as RocketChatRecordDeleted<T>;

			// since the operation is not atomic, we need to make sure that the record is not already deleted/inserted
			await this.trash?.updateOne(
				{ _id } as Filter<RocketChatRecordDeleted<T>>,
				{ $set: trash },
				{
					upsert: true,
				},
			);
		}

		if (options) {
			return this.col.deleteOne(filter, options);
		}
		return this.col.deleteOne(filter);
	}

	async deleteMany(filter: Filter<T>, options?: DeleteOptions): Promise<DeleteResult> {
		if (!this.trash) {
			if (options) {
				return this.col.deleteMany(filter, options);
			}
			return this.col.deleteMany(filter);
		}

		const cursor = this.find(filter);

		const ids: string[] = [];
		for await (const doc of cursor) {
			const { _id, ...record } = doc as unknown as IRocketChatRecord & T;

			const trash = {
				...record,
				_deletedAt: new Date(),
				__collection__: this.name,
			} as RocketChatRecordDeleted<T>;

			ids.push(_id);

			// since the operation is not atomic, we need to make sure that the record is not already deleted/inserted
			await this.trash?.updateOne(
				{ _id } as Filter<RocketChatRecordDeleted<T>>,
				{ $set: trash },
				{
					upsert: true,
				},
			);
		}

		if (options) {
			return this.col.deleteMany({ _id: { $in: ids } } as unknown as Filter<T>, options);
		}
		return this.col.deleteMany({ _id: { $in: ids } } as unknown as Filter<T>);
	}

	// Trash
	trashFind<P extends RocketChatRecordDeleted<T>>(
		query: Filter<RocketChatRecordDeleted<T>>,
		options?: FindOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
	): FindCursor<WithId<RocketChatRecordDeleted<T>>> | undefined {
		if (!this.trash) {
			return undefined;
		}
		const { trash } = this;

		if (options) {
			return trash.find(
				{
					__collection__: this.name,
					...query,
				},
				options,
			);
		}
		return trash.find({
			__collection__: this.name,
			...query,
		});
	}

	trashFindOneById(_id: string): Promise<RocketChatRecordDeleted<T> | null>;

	trashFindOneById<P>(
		_id: string,
		options: FindOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
	): Promise<P | null>;

	async trashFindOneById<P extends RocketChatRecordDeleted<T>>(
		_id: string,
		options?: FindOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
	): Promise<WithId<RocketChatRecordDeleted<P> | RocketChatRecordDeleted<T>> | null> {
		const query = {
			_id,
			__collection__: this.name,
		} as Filter<RocketChatRecordDeleted<T>>;

		if (!this.trash) {
			return null;
		}

		if (options) {
			return this.trash.findOne(query, options);
		}
		return this.trash.findOne(query);
	}

	private setUpdatedAt(record: UpdateFilter<T> | InsertionModel<T>): void {
		if (this.preventSetUpdatedAt) {
			return;
		}
		setUpdatedAt(record);
	}

	trashFindDeletedAfter(deletedAt: Date): FindCursor<WithId<RocketChatRecordDeleted<T>>>;

	trashFindDeletedAfter<P = RocketChatRecordDeleted<T>>(
		deletedAt: Date,
		query?: Filter<RocketChatRecordDeleted<T>>,
		options?: FindOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
	): FindCursor<WithId<RocketChatRecordDeleted<T>>> {
		const q = {
			__collection__: this.name,
			_deletedAt: {
				$gt: deletedAt,
			},
			...query,
		} as Filter<RocketChatRecordDeleted<T>>;

		if (!this.trash) {
			throw new Error('Trash is not enabled for this collection');
		}

		if (options) {
			return this.trash.find(q, options);
		}
		return this.trash.find(q);
	}

	trashFindPaginatedDeletedAfter<P = RocketChatRecordDeleted<T>>(
		deletedAt: Date,
		query?: Filter<RocketChatRecordDeleted<T>>,
		options?: FindOptions<P extends RocketChatRecordDeleted<T> ? RocketChatRecordDeleted<T> : P>,
	): FindPaginated<FindCursor<WithId<RocketChatRecordDeleted<T>>>> {
		const q = {
			__collection__: this.name,
			_deletedAt: {
				$gt: deletedAt,
			},
			...query,
		} as Filter<RocketChatRecordDeleted<T>>;

		if (!this.trash) {
			throw new Error('Trash is not enabled for this collection');
		}

		const cursor = options ? this.trash.find(q, options) : this.trash.find(q);
		const totalCount = this.trash.countDocuments(q);

		return {
			cursor,
			totalCount,
		};
	}

	watch(pipeline?: object[]): ChangeStream<T> {
		return this.col.watch(pipeline);
	}
}


//! moment.js locale configuration
//! locale : Swahili [sw]
//! author : Fahad Kassim : https://github.com/fadsel

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

    var sw = moment.defineLocale('sw', {
        months : 'Januari_Februari_Machi_Aprili_Mei_Juni_Julai_Agosti_Septemba_Oktoba_Novemba_Desemba'.split('_'),
        monthsShort : 'Jan_Feb_Mac_Apr_Mei_Jun_Jul_Ago_Sep_Okt_Nov_Des'.split('_'),
        weekdays : 'Jumapili_Jumatatu_Jumanne_Jumatano_Alhamisi_Ijumaa_Jumamosi'.split('_'),
        weekdaysShort : 'Jpl_Jtat_Jnne_Jtan_Alh_Ijm_Jmos'.split('_'),
        weekdaysMin : 'J2_J3_J4_J5_Al_Ij_J1'.split('_'),
        weekdaysParseExact : true,
        longDateFormat : {
            LT : 'HH:mm',
            LTS : 'HH:mm:ss',
            L : 'DD.MM.YYYY',
            LL : 'D MMMM YYYY',
            LLL : 'D MMMM YYYY HH:mm',
            LLLL : 'dddd, D MMMM YYYY HH:mm'
        },
        calendar : {
            sameDay : '[leo saa] LT',
            nextDay : '[kesho saa] LT',
            nextWeek : '[wiki ijayo] dddd [saat] LT',
            lastDay : '[jana] LT',
            lastWeek : '[wiki iliyopita] dddd [saat] LT',
            sameElse : 'L'
        },
        relativeTime : {
            future : '%s baadaye',
            past : 'tokea %s',
            s : 'hivi punde',
            m : 'dakika moja',
            mm : 'dakika %d',
            h : 'saa limoja',
            hh : 'masaa %d',
            d : 'siku moja',
            dd : 'masiku %d',
            M : 'mwezi mmoja',
            MM : 'miezi %d',
            y : 'mwaka mmoja',
            yy : 'miaka %d'
        },
        week : {
            dow : 1, // Monday is the first day of the week.
            doy : 7  // The week that contains Jan 1st is the first week of the year.
        }
    });

    return sw;

}));


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

const Field: FC = ({ children }) => <Box mb='x16'>{children}</Box>;

export default Field;


import { Page, test, expect } from '@playwright/test';
import { faker } from '@faker-js/faker';

import { Auth } from './page-objects';

test.describe('Register', () => {
	let page: Page;
	let pageAuth: Auth;

	test.beforeAll(async ({ browser }) => {
		page = await browser.newPage();
		pageAuth = new Auth(page);
	});

	test.beforeEach(async () => {
		await page.goto('/');
		await pageAuth.btnRegister.click();
	});

	test('expect trigger a validation error if no data is provided', async () => {
		await pageAuth.btnSubmit.click();

		await expect(pageAuth.textErrorName).toBeVisible();
		await expect(pageAuth.textErrorEmail).toBeVisible();
		await expect(pageAuth.textErrorPassword).toBeVisible();
	});

	test('expect trigger a validation error if different password is provided', async () => {
		await pageAuth.inputName.type(faker.name.firstName());
		await pageAuth.inputEmail.type(faker.internet.email());
		await pageAuth.inputPassword.type('any_password');
		await pageAuth.inputPasswordConfirm.type('any_password_2');
		await pageAuth.btnSubmit.click();

		await expect(pageAuth.textErrorPasswordConfirm).toBeVisible();
	});

	test('expect successfully register a new user', async () => {
		await pageAuth.inputName.type(faker.name.firstName());
		await pageAuth.inputEmail.type(faker.internet.email());
		await pageAuth.inputPassword.type('any_password');
		await pageAuth.inputPasswordConfirm.type('any_password');
		await pageAuth.btnSubmit.click();
		await pageAuth.btnRegisterConfirmUsername.click();
	});
});


const showOnlyTeams = types.length === 1 && types.includes('teams');

const typesToRemove = ['discussions', 'teams'];

const showTypes = Array.isArray(types) ? types.filter((type) => !typesToRemove.includes(type)) : [];

keep keep keep keep replace keep keep keep keep keep

import { LivechatInquiry, Users, LivechatPriority } from '@rocket.chat/models';

import { hasPermissionAsync } from '../../../../../../app/authorization/server/functions/hasPermission';
import { LivechatEnterprise } from '../../lib/LivechatEnterprise';

export async function setPriorityToInquiry({ userId, roomId, priority }) {
	if (!(await hasPermissionAsync(userId, 'manage-livechat-priorities')) && !(await hasPermissionAsync(userId, 'view-l-room'))) {
		throw new Error('error-not-authorized');
	}
	const inquiry = await LivechatInquiry.findOneByRoomId(roomId, { fields: { status: 1 } });
	if (!inquiry || inquiry.status !== 'queued') {
		throw new Error('error-invalid-inquiry');
	}

	const priorityData = priority && (await LivechatPriority.findOneByIdOrName(priority));
	if (!priorityData) {
		throw new Error('error-invalid-priority');
	}

	LivechatEnterprise.updateRoomPriority(roomId, await Users.findOneById(userId, { fields: { username: 1 } }), priorityData);
}


import { Button, ButtonGroup, Field, Modal, TextAreaInput } from '@rocket.chat/fuselage';
import { useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
import React, { ReactElement, useEffect } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form';

import Tags from '../../../../../client/components/Omnichannel/Tags';

type WrapUpCallPayload = {
	comment: string;
	tags?: string[];
};

type WrapUpCallModalProps = {
	closeRoom: (data?: { comment?: string; tags?: string[] }) => void;
};

export const WrapUpCallModal = ({ closeRoom }: WrapUpCallModalProps): ReactElement => {
	const setModal = useSetModal();

	const closeModal = (): void => setModal(null);
	const t = useTranslation();

	const { register, handleSubmit, setValue, watch } = useForm<WrapUpCallPayload>();

	const tags = watch('tags');

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

	const handleTags = (value: string[]): void => {
		setValue('tags', value);
	};

	const onSubmit: SubmitHandler<WrapUpCallPayload> = (data: { comment?: string; tags?: string[] }): void => {
		closeRoom(data);
		closeModal();
	};

	const onCancel = (): void => {
		closeRoom();
		closeModal();
	};

	return (
		<Modal is='form' onSubmit={handleSubmit(onSubmit)}>
			<Modal.Header>
				<Modal.Title>{t('Wrap_up_the_call')}</Modal.Title>
				<Modal.Close onClick={closeModal} />
			</Modal.Header>
			<Modal.Content>
				<Field mbe='24px'>
					<Field.Label>{t('Notes')}</Field.Label>
					<Field.Row>
						<TextAreaInput placeholder={t('Do_you_have_any_notes_for_this_conversation')} {...register('comment')} />
					</Field.Row>
					<Field.Hint>{t('These_notes_will_be_available_in_the_call_summary')}</Field.Hint>
				</Field>
				<Tags tags={tags} handler={handleTags as () => void} />
			</Modal.Content>
			<Modal.Footer>
				<ButtonGroup align='end'>
					<Button secondary onClick={onCancel}>
						{t('Cancel')}
					</Button>
					<Button type='submit' primary>
						{t('Save')}
					</Button>
				</ButtonGroup>
			</Modal.Footer>
		</Modal>
	);
};


import { Component, createContext } from 'preact';

import { createToken } from '../lib/random';
import Store from './Store';

export const initialState = () => ({
	token: createToken(),
	typing: [],
	config: {
		messages: {},
		settings: {},
		theme: {},
		triggers: [],
		departments: [],
		resources: {},
	},
	messages: [],
	user: null,
	sound: {
		src: '',
		enabled: true,
		play: false,
	},
	iframe: {
		guest: {},
		theme: {},
		visible: true,
	},
	gdpr: {
		accepted: false,
	},
	alerts: [],
	visible: true,
	minimized: true,
	unread: null,
	incomingCallAlert: null,
	ongoingCall: null,	// TODO: store call info like url, startTime, timeout, etc here
	businessUnit: null,
});

const dontPersist = ['messages', 'typing', 'loading', 'alerts', 'unread', 'noMoreMessages', 'modal', 'incomingCallAlert', 'ongoingCall'];
export const store = new Store(initialState(), { dontPersist });

if (process.env.NODE_ENV === 'development') {
	store.on('change', ([, , partialState]) => {
		// eslint-disable-next-line no-console
		console.log('%cstore.setState %c%o', 'color: blue', 'color: initial', partialState);
	});
}

const StoreContext = createContext();

export class Provider extends Component {
	static displayName = 'StoreProvider'

	state = { ...store.state, dispatch: store.setState.bind(store) }

	handleStoreChange = () => {
		this.setState({ ...store.state });
	}

	componentDidMount() {
		store.on('change', this.handleStoreChange);
	}

	componentWillUnmount() {
		store.off('change', this.handleStoreChange);
	}

	render = ({ children }) => (
		<StoreContext.Provider value={this.state}>
			{children}
		</StoreContext.Provider>
	)
}

export const { Consumer } = StoreContext;

export default store;


import type { ILivechatDepartment, IOmnichannelCannedResponse } from '@rocket.chat/core-typings';
import { Box, Button, ButtonGroup, Tag } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { FC, memo, MouseEventHandler } from 'react';

import VerticalBar from '../../../../../../client/components/VerticalBar';
import { useScopeDict } from '../../../hooks/useScopeDict';

const CannedResponse: FC<{
	canEdit: boolean;
	data: {
		departmentName: ILivechatDepartment['name'];
		shortcut: IOmnichannelCannedResponse['shortcut'];
		text: IOmnichannelCannedResponse['text'];
		scope: IOmnichannelCannedResponse['scope'];
		tags: IOmnichannelCannedResponse['tags'];
	};
	onClickBack: MouseEventHandler<HTMLOrSVGElement>;
	onClickEdit: MouseEventHandler<HTMLOrSVGElement>;
	onClickUse: MouseEventHandler<HTMLOrSVGElement>;
}> = ({ canEdit, data: { departmentName, shortcut, text, scope: dataScope, tags }, onClickBack, onClickEdit, onClickUse }) => {
	const t = useTranslation();
	const scope = useScopeDict(dataScope, departmentName);

	return (
		<VerticalBar display='flex' flexDirection='column' width={'full'} overflow='hidden' zIndex={100} insetBlock={0}>
			<VerticalBar.Header>
				{onClickBack && <VerticalBar.Action onClick={onClickBack} title={t('Back_to_threads')} name='arrow-back' />}
				<VerticalBar.Text>!{shortcut}</VerticalBar.Text>
			</VerticalBar.Header>
			<VerticalBar.Content>
				<Box pb='24px'>
					<Box mbe='16px'>
						<Box fontScale='p2m' mbe='8px'>
							{t('Shortcut')}:
						</Box>
						<Box fontScale='c1' color='info'>
							!{shortcut}
						</Box>
					</Box>
					<Box mbe='16px'>
						<Box fontScale='p2m' mbe='8px'>
							{t('Content')}:
						</Box>
						<Box fontScale='c1' color='info'>
							"{text}"
						</Box>
					</Box>
					<Box mbe='16px'>
						<Box fontScale='p2m' mbe='8px'>
							{t('Sharing')}:
						</Box>
						<Box fontScale='c1' color='info'>
							{scope}
						</Box>
					</Box>
					<Box mbe='16px'>
						<Box fontScale='p2m' mbe='8px'>
							{t('Tags')}:
						</Box>
						<Box display='flex' flexDirection='row'>
							{tags && tags.length > 0 ? (
								<Box display='flex' w='full' flexDirection='row' mbs='8px' flexWrap='wrap'>
									{tags.map((tag: string, idx: number) => (
										<Box key={idx} mie='4px' mbe='4px'>
											<Tag>{tag}</Tag>
										</Box>
									))}
								</Box>
							) : (
								'-'
							)}
						</Box>
					</Box>
				</Box>
			</VerticalBar.Content>
			<VerticalBar.Footer>
				<ButtonGroup stretch>
					{canEdit && <Button onClick={onClickEdit}>{t('Edit')}</Button>}
					<Button primary onClick={onClickUse}>
						{t('Use')}
					</Button>
				</ButtonGroup>
			</VerticalBar.Footer>
		</VerticalBar>
	);
};

export default memo(CannedResponse);


if (!(await hasPermissionAsync(uid, 'view-room-administration'))) {

return Rooms.findOneById(rid, { fields: adminFields });

export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{

return Rooms.findOneById(rid, { projection: adminFields });

import { Permissions, Roles } from '@rocket.chat/models';

import { createOrUpdateProtectedRoleAsync } from '../../../../server/lib/roles/createOrUpdateProtectedRole';

export const createPermissions = async (): Promise<void> => {
	const livechatMonitorRole = 'livechat-monitor';
	const livechatManagerRole = 'livechat-manager';
	const adminRole = 'admin';

	const monitorRole = await Roles.findOneById(livechatMonitorRole, { fields: { _id: 1 } });
	if (!monitorRole) {
		await createOrUpdateProtectedRoleAsync(livechatMonitorRole, {
			name: livechatMonitorRole,
		});
	}

	await Promise.all([
		Permissions.create('manage-livechat-units', [adminRole, livechatManagerRole]),
		Permissions.create('manage-livechat-monitors', [adminRole, livechatManagerRole]),
		Permissions.create('manage-livechat-tags', [adminRole, livechatManagerRole]),
		Permissions.create('manage-livechat-priorities', [adminRole, livechatManagerRole]),
		Permissions.create('manage-livechat-canned-responses', [adminRole, livechatManagerRole, livechatMonitorRole]),
		Permissions.create('spy-voip-calls', [adminRole, livechatManagerRole, livechatMonitorRole]),
		Permissions.create('outbound-voip-calls', [adminRole, livechatManagerRole]),
	]);
};


import { slashCommands } from '../../utils/lib/slashCommand';

slashCommands.add({
	command: 'invite-all-to',
	options: {
		description: 'Invite_user_to_join_channel_all_to',
		params: '#room',
		permission: ['add-user-to-joined-room', 'add-user-to-any-c-room', 'add-user-to-any-p-room'],
	},
});
slashCommands.add({
	command: 'invite-all-from',
	options: {
		description: 'Invite_user_to_join_channel_all_from',
		params: '#room',
		permission: 'add-user-to-joined-room',
	},
});


import { DDPCommon } from 'meteor/ddp-common';
import { DDP } from 'meteor/ddp';

class MethodInvocation extends DDPCommon.MethodInvocation {
	constructor(options) {
		const result = super(options);
		const currentInvocation = DDP._CurrentInvocation.get();

		if (currentInvocation) {
			this.twoFactorChecked = currentInvocation.twoFactorChecked;
		}

		return result;
	}
}

DDPCommon.MethodInvocation = MethodInvocation;


/*
 * Mentions is a named function that will process Mentions
 * @param {Object} message - The message object
 */
import { MentionsParser } from '../lib/MentionsParser';

export default class MentionsServer extends MentionsParser {
	constructor(args) {
		super(args);
		this.messageMaxAll = args.messageMaxAll;
		this.getChannel = args.getChannel;
		this.getChannels = args.getChannels;
		this.getUsers = args.getUsers;
		this.getUser = args.getUser;
		this.getTotalChannelMembers = args.getTotalChannelMembers;
		this.onMaxRoomMembersExceeded = args.onMaxRoomMembersExceeded || (() => {});
	}

	set getUsers(m) {
		this._getUsers = m;
	}

	get getUsers() {
		return typeof this._getUsers === 'function' ? this._getUsers : () => this._getUsers;
	}

	set getChannels(m) {
		this._getChannels = m;
	}

	get getChannels() {
		return typeof this._getChannels === 'function' ? this._getChannels : () => this._getChannels;
	}

	set getChannel(m) {
		this._getChannel = m;
	}

	get getChannel() {
		return typeof this._getChannel === 'function' ? this._getChannel : () => this._getChannel;
	}

	set messageMaxAll(m) {
		this._messageMaxAll = m;
	}

	get messageMaxAll() {
		return typeof this._messageMaxAll === 'function' ? this._messageMaxAll() : this._messageMaxAll;
	}

	getUsersByMentions({ msg, rid, u: sender }) {
		let mentions = this.getUserMentions(msg);
		const mentionsAll = [];
		const userMentions = [];

		mentions.forEach((m) => {
			const mention = m.trim().substr(1);
			if (mention !== 'all' && mention !== 'here') {
				return userMentions.push(mention);
			}
			if (this.messageMaxAll > 0 && this.getTotalChannelMembers(rid) > this.messageMaxAll) {
				return this.onMaxRoomMembersExceeded({ sender, rid });
			}
			mentionsAll.push({
				_id: mention,
				username: mention,
			});
		});
		mentions = userMentions.length ? this.getUsers(userMentions) : [];
		return [...mentionsAll, ...mentions];
	}

	getChannelbyMentions({ msg }) {
		const channels = this.getChannelMentions(msg);
		return this.getChannels(channels.map((c) => c.trim().substr(1)));
	}

	execute(message) {
		const mentionsAll = this.getUsersByMentions(message);
		const channels = this.getChannelbyMentions(message);

		message.mentions = mentionsAll;
		message.channels = channels;

		return message;
	}
}


import { Box, Modal, ButtonGroup, Button, TextInput, Field, ToggleSwitch, FieldGroup } from '@rocket.chat/fuselage';
import { useAutoFocus } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { memo, ReactElement } from 'react';

import UserAutoCompleteMultiple from '../../../components/UserAutoCompleteMultiple';
import TeamNameInput from './TeamNameInput';
import { useCreateTeamModalState } from './useCreateTeamModalState';

const CreateTeamModal = ({ onClose }: { onClose: () => void }): ReactElement => {
	const {
		name,
		nameError,
		onChangeName,
		description,
		onChangeDescription,
		type,
		onChangeType,
		readOnly,
		canChangeReadOnly,
		onChangeReadOnly,
		encrypted,
		canChangeEncrypted,
		onChangeEncrypted,
		broadcast,
		onChangeBroadcast,
		members,
		onChangeMembers,
		hasUnsavedChanges,
		isCreateButtonEnabled,
		onCreate,
	} = useCreateTeamModalState(onClose);

	const t = useTranslation();
	const focusRef = useAutoFocus<HTMLInputElement>();

	return (
		<Modal>
			<Modal.Header>
				<Modal.Title>{t('Teams_New_Title')}</Modal.Title>
				<Modal.Close title={t('Close')} onClick={onClose} />
			</Modal.Header>
			<Modal.Content>
				<FieldGroup>
					<Field>
						<Field.Label>{t('Teams_New_Name_Label')}</Field.Label>
						<Field.Row>
							<TeamNameInput
								ref={focusRef}
								private={type}
								error={hasUnsavedChanges ? nameError : undefined}
								value={name}
								onChange={onChangeName}
							/>
						</Field.Row>
						{hasUnsavedChanges && nameError && <Field.Error>{nameError}</Field.Error>}
					</Field>
					<Field>
						<Field.Label>
							{t('Teams_New_Description_Label')}{' '}
							<Box is='span' color='neutral-600'>
								({t('optional')})
							</Box>
						</Field.Label>
						<Field.Row>
							<TextInput placeholder={t('Teams_New_Description_Placeholder')} value={description} onChange={onChangeDescription} />
						</Field.Row>
					</Field>
					<Field>
						<Box display='flex' justifyContent='space-between' alignItems='start'>
							<Box display='flex' flexDirection='column' width='full'>
								<Field.Label>{t('Teams_New_Private_Label')}</Field.Label>
								<Field.Description>
									{type ? t('Teams_New_Private_Description_Enabled') : t('Teams_New_Private_Description_Disabled')}
								</Field.Description>
							</Box>
							<ToggleSwitch checked={type} onChange={onChangeType} />
						</Box>
					</Field>
					<Field disabled={!canChangeReadOnly}>
						<Box display='flex' justifyContent='space-between' alignItems='start'>
							<Box display='flex' flexDirection='column' width='full'>
								<Field.Label>{t('Teams_New_Read_only_Label')}</Field.Label>
								<Field.Description>
									{readOnly ? t('Only_authorized_users_can_write_new_messages') : t('Teams_New_Read_only_Description')}
								</Field.Description>
							</Box>
							<ToggleSwitch checked={readOnly} disabled={!canChangeReadOnly} onChange={onChangeReadOnly} />
						</Box>
					</Field>
					<Field disabled={!canChangeEncrypted}>
						<Box display='flex' justifyContent='space-between' alignItems='start'>
							<Box display='flex' flexDirection='column' width='full'>
								<Field.Label>{t('Teams_New_Encrypted_Label')}</Field.Label>
								<Field.Description>
									{type ? t('Teams_New_Encrypted_Description_Enabled') : t('Teams_New_Encrypted_Description_Disabled')}
								</Field.Description>
							</Box>
							<ToggleSwitch checked={encrypted} disabled={!canChangeEncrypted} onChange={onChangeEncrypted} />
						</Box>
					</Field>
					<Field>
						<Box display='flex' justifyContent='space-between' alignItems='start'>
							<Box display='flex' flexDirection='column' width='full'>
								<Field.Label>{t('Teams_New_Broadcast_Label')}</Field.Label>
								<Field.Description>{t('Teams_New_Broadcast_Description')}</Field.Description>
							</Box>
							<ToggleSwitch checked={broadcast} onChange={onChangeBroadcast} />
						</Box>
					</Field>
					<Field>
						<Field.Label>
							{t('Teams_New_Add_members_Label')}{' '}
							<Box is='span' color='neutral-600'>
								({t('optional')})
							</Box>
						</Field.Label>
						<UserAutoCompleteMultiple value={members} onChange={onChangeMembers} />
					</Field>
				</FieldGroup>
			</Modal.Content>
			<Modal.Footer>
				<ButtonGroup align='end'>
					<Button onClick={onClose}>{t('Cancel')}</Button>
					<Button disabled={!isCreateButtonEnabled} onClick={onCreate} primary>
						{t('Create')}
					</Button>
				</ButtonGroup>
			</Modal.Footer>
		</Modal>
	);
};

export default memo(CreateTeamModal);


import { IconButton, TextInput } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { ChangeEvent, FocusEvent, forwardRef } from 'react';

type DialInputProps = {
	inputName: string;
	inputError: string | undefined;
	isButtonDisabled: boolean;
	onBlur?: (event: FocusEvent<HTMLElement, Element>) => void;
	handleBackspaceClick: () => void;
	handleOnChange: (e: ChangeEvent<HTMLInputElement>) => void;
};

export const DialInput = forwardRef<HTMLInputElement, DialInputProps>(function DialInput(
	{ handleBackspaceClick, handleOnChange, isButtonDisabled, inputError, inputName },
	ref,
) {
	const t = useTranslation();

	return (
		<TextInput
			ref={ref}
			textAlign='center'
			placeholder={t('Phone_number')}
			addon={<IconButton icon='backspace' small size='20px' disabled={isButtonDisabled} onClick={handleBackspaceClick} />}
			error={inputError}
			onChange={handleOnChange}
			name={inputName}
		/>
	);
});