chatMessages.onDestroyed && chatMessages.onDestroyed(rid, tmid);

import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { Template } from 'meteor/templating';
import { Session } from 'meteor/session';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';

import { chatMessages, ChatMessages } from '../../../ui';
import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling';
import { messageContext } from '../../../ui-utils/client/lib/messageContext';
import { upsertMessageBulk } from '../../../ui-utils/client/lib/RoomHistoryManager';
import { Messages } from '../../../models';
import { fileUpload } from '../../../ui/client/lib/fileUpload';
import { dropzoneEvents, dropzoneHelpers } from '../../../ui/client/views/app/room';
import './thread.html';
import { getUserPreference } from '../../../utils';
import { settings } from '../../../settings/client';
import { callbacks } from '../../../callbacks/client';
import './messageBoxFollow';
import { getCommonRoomEvents } from '../../../ui/client/views/app/lib/getCommonRoomEvents';
import { keyCodes } from '../../../../client/lib/utils/keyCodes';

const sort = { ts: 1 };

Template.thread.events({
	...dropzoneEvents,
	...getCommonRoomEvents(),
	'click .js-close'(e) {
		e.preventDefault();
		e.stopPropagation();
		const { close } = this;
		return close && close();
	},
	'scroll .js-scroll-thread': _.throttle(({ currentTarget: e }, i) => {
		i.atBottom = e.scrollTop >= e.scrollHeight - e.clientHeight;
	}, 150),
	'click .toggle-hidden'(e) {
		const id = e.currentTarget.dataset.message;
		document.querySelector(`#thread-${ id }`).classList.toggle('message--ignored');
	},
});

Template.thread.helpers({
	...dropzoneHelpers,
	mainMessage() {
		const { Threads, state } = Template.instance();
		const tmid = state.get('tmid');
		return Threads.findOne({ _id: tmid });
	},
	isLoading() {
		return Template.instance().state.get('loading') !== false;
	},
	messages() {
		const { Threads, state } = Template.instance();
		const tmid = state.get('tmid');

		return Threads.find({ tmid, _id: { $ne: tmid } }, { sort });
	},
	messageContext() {
		const result = messageContext.call(this, { rid: this.mainMessage.rid });
		return {
			...result,
			settings: {
				...result.settings,
				showReplyButton: false,
				showreply: false,
			},
		};
	},
	messageBoxData() {
		const instance = Template.instance();
		const { mainMessage: { rid, _id: tmid }, subscription } = Template.currentData();

		const showFormattingTips = settings.get('Message_ShowFormattingTips');
		return {
			showFormattingTips,
			tshow: instance.state.get('sendToChannel'),
			subscription,
			rid,
			tmid,
			onSend: (...args) => {
				instance.sendToBottom();
				instance.state.set('sendToChannel', false);
				return instance.chatMessages && instance.chatMessages.send.apply(instance.chatMessages, args);
			},
			onKeyUp: (...args) => instance.chatMessages && instance.chatMessages.keyup.apply(instance.chatMessages, args),
			onKeyDown: (...args) => {
				const result = instance.chatMessages && instance.chatMessages.keydown.apply(instance.chatMessages, args);
				const [event] = args;

				const { which: keyCode } = event;

				if (keyCode === keyCodes.ESCAPE && !result && !event.target.value.trim()) {
					const { route: { name }, params: { context, tab, ...params } } = FlowRouter.current();
					FlowRouter.go(name, params);
				}
			},
		};
	},
	hideUsername() {
		return getUserPreference(Meteor.userId(), 'hideUsernames') ? 'hide-usernames' : undefined;
	},
	checkboxData() {
		const instance = Template.instance();
		const checked = instance.state.get('sendToChannel');
		return {
			id: 'sendAlso',
			checked,
			onChange: () => instance.state.set('sendToChannel', !checked),
		};
	},
});

Template.thread.onRendered(function() {
	const rid = Tracker.nonreactive(() => this.state.get('rid'));
	const tmid = Tracker.nonreactive(() => this.state.get('tmid'));
	this.atBottom = true;

	this.chatMessages = new ChatMessages(this.Threads);
	this.chatMessages.initializeWrapper(this.find('.js-scroll-thread'));
	this.chatMessages.initializeInput(this.find('.js-input-message'), { rid, tmid });

	this.sendToBottom = _.throttle(() => {
		this.atBottom = true;
		this.chatMessages.wrapper.scrollTop = this.chatMessages.wrapper.scrollHeight;
	}, 300);

	this.sendToBottomIfNecessary = () => {
		this.atBottom && this.sendToBottom();
	};

	const observer = new ResizeObserver(this.sendToBottomIfNecessary);
	observer.observe(this.firstNode.querySelector('.js-scroll-thread ul'));

	this.onFile = (filesToUpload) => {
		fileUpload(filesToUpload, this.chatMessages.input, { rid: this.state.get('rid'), tmid: this.state.get('tmid') });
	};

	this.autorun(() => {
		const rid = this.state.get('rid');
		const tmid = this.state.get('tmid');
		if (!rid) {
			return;
		}
		this.callbackRemove && this.callbackRemove();

		this.callbackRemove = () => callbacks.remove('streamNewMessage', `thread-${ rid }`);

		callbacks.add('streamNewMessage', _.debounce((msg) => {
			if (Session.get('openedRoom') !== msg.rid || rid !== msg.rid || msg.editedAt || msg.tmid !== tmid) {
				return;
			}
			Meteor.call('readThreads', tmid);
		}, 1000), callbacks.priority.MEDIUM, `thread-${ rid }`);
	});

	this.autorun(() => {
		const tmid = this.state.get('tmid');
		this.threadsObserve && this.threadsObserve.stop();

		this.threadsObserve = Messages.find({ $or: [{ tmid }, { _id: tmid }], _hidden: { $ne: true } }, {
			fields: {
				collapsed: 0,
				threadMsg: 0,
				repliesCount: 0,
			},
		}).observe({
			added: ({ _id, ...message }) => {
				this.Threads.upsert({ _id }, message);
			},
			changed: ({ _id, ...message }) => {
				this.Threads.update({ _id }, message);
			},
			removed: ({ _id }) => this.Threads.remove(_id),
		});

		this.loadMore();
	});

	this.autorun(() => {
		const rid = this.state.get('rid');
		const tmid = this.state.get('tmid');
		this.chatMessages.initializeInput(this.find('.js-input-message'), { rid, tmid });
		if (rid && tmid) {
			chatMessages[`${ rid }-${ tmid }`] = this.chatMessages;
		}
	});

	this.autorun(() => {
		FlowRouter.watchPathChange();
		const jump = FlowRouter.getQueryParam('jump');
		const { mainMessage } = Template.currentData();
		this.state.set({
			tmid: mainMessage._id,
			rid: mainMessage.rid,
			jump,
		});
	});

	this.autorun(() => {
		const jump = this.state.get('jump');
		const loading = this.state.get('loading');

		if (jump && this.lastJump !== jump && loading === false) {
			this.lastJump = jump;
			this.find('.js-scroll-thread').style.scrollBehavior = 'smooth';
			this.state.set('jump', null);
			Tracker.afterFlush(() => {
				const message = this.find(`#thread-${ jump }`);
				message.classList.add('highlight');
				const removeClass = () => {
					message.classList.remove('highlight');
					message.removeEventListener('animationend', removeClass);
				};
				message.addEventListener('animationend', removeClass);
				setTimeout(() => {
					message.scrollIntoView();
				}, 300);
			});
		}
	});
});

Template.thread.onCreated(async function() {
	this.Threads = new Mongo.Collection(null);

	this.state = new ReactiveDict({
		sendToChannel: !this.data.mainMessage.tcount,
	});

	this.loadMore = async () => {
		const { tmid } = Tracker.nonreactive(() => this.state.all());
		if (!tmid) {
			return;
		}

		this.state.set('loading', true);

		const messages = await callWithErrorHandling('getThreadMessages', { tmid });

		upsertMessageBulk({ msgs: messages }, this.Threads);

		Tracker.afterFlush(() => {
			this.state.set('loading', false);
		});
	};
});

Template.thread.onDestroyed(function() {
	const { Threads, threadsObserve, callbackRemove, state } = this;
	Threads.remove({});
	threadsObserve && threadsObserve.stop();

	callbackRemove && callbackRemove();

	const tmid = state.get('tmid');
	const rid = state.get('rid');
	if (rid && tmid) {
		delete chatMessages[`${ rid }-${ tmid }`];
	}
});


import { expect, spy } from 'chai';
import { describe, it } from 'mocha';

import { download, downloadAs, downloadCsvAs, downloadJsonAs } from './download';

describe('download', () => {
	it('should work', () => {
		const listener = spy();
		document.addEventListener('click', listener, false);

		download('about:blank', 'blank');

		document.removeEventListener('click', listener, false);
		expect(listener).to.have.been.called();
	});
});

describe('downloadAs', () => {
	it('should work', () => {
		const listener = spy();
		document.addEventListener('click', listener, false);

		downloadAs({ data: [] }, 'blank');

		document.removeEventListener('click', listener, false);
		expect(listener).to.have.been.called();
	});
});

describe('downloadJsonAs', () => {
	it('should work', () => {
		const listener = spy();
		document.addEventListener('click', listener, false);

		downloadJsonAs({}, 'blank');

		document.removeEventListener('click', listener, false);
		expect(listener).to.have.been.called();
	});
});

describe('downloadCsvAs', () => {
	it('should work', () => {
		const listener = spy();
		document.addEventListener('click', listener, false);

		downloadCsvAs(
			[
				[1, 2, 3],
				[4, 5, 6],
			],
			'blank',
		);

		document.removeEventListener('click', listener, false);
		expect(listener).to.have.been.called();
	});
});


import { WebApp } from 'meteor/webapp';
import { Cookies } from 'meteor/ostrio:cookies';

import { Users, Rooms, Messages } from '../../models';

WebApp.connectHandlers.use('/snippet/download', function(req, res) {
	let rawCookies;
	let token;
	let uid;
	const cookie = new Cookies();

	if (req.headers && req.headers.cookie !== null) {
		rawCookies = req.headers.cookie;
	}

	if (rawCookies !== null) {
		uid = cookie.get('rc_uid', rawCookies);
	}

	if (rawCookies !== null) {
		token = cookie.get('rc_token', rawCookies);
	}

	if (uid === null) {
		uid = req.query.rc_uid;
		token = req.query.rc_token;
	}

	const user = Users.findOneByIdAndLoginToken(uid, token);

	if (!(uid && token && user)) {
		res.writeHead(403);
		res.end();
		return false;
	}
	const match = /^\/([^\/]+)\/(.*)/.exec(req.url);

	if (match[1]) {
		const snippet = Messages.findOne(
			{
				_id: match[1],
				snippeted: true,
			},
		);
		const room = Rooms.findOne({ _id: snippet.rid, usernames: { $in: [user.username] } });
		if (room === undefined) {
			res.writeHead(403);
			res.end();
			return false;
		}

		res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${ encodeURIComponent(snippet.snippetName) }`);
		res.setHeader('Content-Type', 'application/octet-stream');

		// Removing the ``` contained in the msg.
		const snippetContent = snippet.msg.substr(3, snippet.msg.length - 6);
		res.setHeader('Content-Length', snippetContent.length);
		res.write(snippetContent);
		res.end();
		return;
	}

	res.writeHead(404);
	res.end();
});


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 moment from 'moment';
import _ from 'underscore';
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Session } from 'meteor/session';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { escapeHTML } from '@rocket.chat/string-helpers';

import { KonchatNotification } from './notification';
import { UserAction, USER_ACTIVITIES } from '../index';
import { fileUpload } from './fileUpload';
import { t, slashCommands } from '../../../utils/client';
import {
	messageProperties,
	MessageTypes,
	readMessage,
} from '../../../ui-utils/client';
import { settings } from '../../../settings/client';
import { callbacks } from '../../../callbacks/client';
import { hasAtLeastOnePermission } from '../../../authorization/client';
import { Messages, Rooms, ChatMessage, ChatSubscription } from '../../../models/client';
import { emoji } from '../../../emoji/client';
import { generateTriggerId } from '../../../ui-message/client/ActionManager';
import { imperativeModal } from '../../../../client/lib/imperativeModal';
import GenericModal from '../../../../client/components/GenericModal';
import { keyCodes } from '../../../../client/lib/utils/keyCodes';
import { prependReplies } from '../../../../client/lib/utils/prependReplies';
import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling';
import { handleError } from '../../../../client/lib/utils/handleError';
import { dispatchToastMessage } from '../../../../client/lib/toast';
import { onClientBeforeSendMessage } from '../../../../client/lib/onClientBeforeSendMessage';

const messageBoxState = {
	saveValue: _.debounce(({ rid, tmid }, value) => {
		const key = ['messagebox', rid, tmid].filter(Boolean).join('_');
		value ? Meteor._localStorage.setItem(key, value) : Meteor._localStorage.removeItem(key);
	}, 1000),

	restoreValue: ({ rid, tmid }) => {
		const key = ['messagebox', rid, tmid].filter(Boolean).join('_');
		return Meteor._localStorage.getItem(key);
	},

	restore: ({ rid, tmid }, input) => {
		const value = messageBoxState.restoreValue({ rid, tmid });
		if (typeof value === 'string') {
			messageBoxState.set(input, value);
		}
	},

	save: ({ rid, tmid }, input) => {
		messageBoxState.saveValue({ rid, tmid }, input.value);
	},

	set: (input, value) => {
		input.value = value;
		$(input).trigger('change').trigger('input');
	},

	purgeAll: () => {
		Object.keys(Meteor._localStorage)
			.filter((key) => key.indexOf('messagebox_') === 0)
			.forEach((key) => Meteor._localStorage.removeItem(key));
	},
};

callbacks.add('afterLogoutCleanUp', messageBoxState.purgeAll, callbacks.priority.MEDIUM, 'chatMessages-after-logout-cleanup');

export class ChatMessages {
	constructor(collection = ChatMessage) {
		this.collection = collection;
	}

	editing = {}

	records = {}

	initializeWrapper(wrapper) {
		this.wrapper = wrapper;
	}

	initializeInput(input, { rid, tmid }) {
		this.input = input;
		this.$input = $(this.input);

		if (!input || !rid) {
			return;
		}

		messageBoxState.restore({ rid, tmid }, input);
		this.restoreReplies();
		this.requestInputFocus();
	}

	async restoreReplies() {
		const mid = FlowRouter.getQueryParam('reply');
		if (!mid) {
			return;
		}

		const message = Messages.findOne(mid) || await callWithErrorHandling('getSingleMessage', mid);
		if (!message) {
			return;
		}

		this.$input.data('reply', [message]).trigger('dataChange');
	}

	requestInputFocus() {
		setTimeout(() => {
			if (this.input && window.matchMedia('screen and (min-device-width: 500px)').matches) {
				this.input.focus();
			}
		}, 200);
	}

	recordInputAsDraft() {
		const message = this.collection.findOne(this.editing.id);
		const record = this.records[this.editing.id] || {};
		const draft = this.input.value;

		if (draft === message.msg) {
			this.clearCurrentDraft();
			return;
		}

		record.draft = draft;
		this.records[this.editing.id] = record;
	}

	clearCurrentDraft() {
		const hasValue = this.records[this.editing.id];
		delete this.records[this.editing.id];
		return !!hasValue;
	}

	resetToDraft(id) {
		const message = this.collection.findOne(id);
		const oldValue = this.input.value;
		messageBoxState.set(this.input, message.msg);
		return oldValue !== message.msg;
	}

	toPrevMessage() {
		const { element } = this.editing;
		if (!element) {
			const messages = Array.from(this.wrapper.querySelectorAll('.own:not(.system)'));
			const message = messages.pop();
			return message && this.edit(message, false);
		}

		for (
			let previous = element.previousElementSibling;
			previous;
			previous = previous.previousElementSibling
		) {
			if (previous.matches('.own:not(.system)')) {
				return this.edit(previous, false);
			}
		}
		this.clearEditing();
	}

	toNextMessage() {
		const { element } = this.editing;
		if (element) {
			let next;
			for (next = element.nextElementSibling; next; next = next.nextElementSibling) {
				if (next.matches('.own:not(.system)')) {
					break;
				}
			}

			next ? this.edit(next, true) : this.clearEditing();
		} else {
			this.clearEditing();
		}
	}

	edit(element, isEditingTheNextOne) {
		const message = this.collection.findOne(element.dataset.id);

		const hasPermission = hasAtLeastOnePermission('edit-message', message.rid);
		const editAllowed = settings.get('Message_AllowEditing');
		const editOwn = message && message.u && message.u._id === Meteor.userId();

		if (!hasPermission && (!editAllowed || !editOwn)) {
			return;
		}

		if (element.classList.contains('system')) {
			return;
		}

		const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes');
		if (blockEditInMinutes && blockEditInMinutes !== 0) {
			let currentTsDiff;
			let msgTs;
			if (message.ts) {
				msgTs = moment(message.ts);
			}
			if (msgTs) {
				currentTsDiff = moment().diff(msgTs, 'minutes');
			}
			if (currentTsDiff > blockEditInMinutes) {
				return;
			}
		}

		const draft = this.records[message._id];
		let msg = draft && draft.draft;
		msg = msg || message.msg;

		this.clearEditing();

		this.editing.element = element;
		this.editing.id = message._id;
		this.input.parentElement.classList.add('editing');
		element.classList.add('editing');

		if (message.attachments && message.attachments[0].description) {
			messageBoxState.set(this.input, message.attachments[0].description);
		} else {
			messageBoxState.set(this.input, msg);
		}

		const cursorPosition = isEditingTheNextOne ? 0 : -1;
		this.input.focus();
		this.$input.setCursorPosition(cursorPosition);
	}

	clearEditing() {
		if (!this.editing.element) {
			this.editing.saved = this.input.value;
			this.editing.savedCursor = this.input.selectionEnd;
			return;
		}

		this.recordInputAsDraft();
		this.input.parentElement.classList.remove('editing');
		this.editing.element.classList.remove('editing');
		delete this.editing.id;
		delete this.editing.element;

		messageBoxState.set(this.input, this.editing.saved || '');
		const cursorPosition = this.editing.savedCursor ? this.editing.savedCursor : -1;
		this.$input.setCursorPosition(cursorPosition);
	}

	async send(event, { rid, tmid, value, tshow }, done = () => {}) {
		const threadsEnabled = settings.get('Threads_enabled');

		UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid });

		if (!ChatSubscription.findOne({ rid })) {
			await callWithErrorHandling('joinRoom', rid);
		}

		messageBoxState.save({ rid, tmid }, this.input);

		let msg = value.trim();
		if (msg) {
			const mention = this.$input.data('mention-user') || false;
			const replies = this.$input.data('reply') || [];
			if (!mention || !threadsEnabled) {
				msg = await prependReplies(msg, replies, mention);
			}

			if (mention && threadsEnabled && replies.length) {
				tmid = replies[0]._id;
			}
		}

		// don't add tmid or tshow if the message isn't part of a thread (it can happen if editing the main message of a thread)
		const originalMessage = this.collection.findOne({ _id: this.editing.id }, { fields: { tmid: 1 }, reactive: false });
		if (originalMessage && tmid && !originalMessage.tmid) {
			tmid = undefined;
			tshow = undefined;
		}

		if (msg) {
			readMessage.readNow(rid);
			readMessage.refreshUnreadMark(rid);

			const message = await onClientBeforeSendMessage({
				_id: Random.id(),
				rid,
				tshow,
				tmid,
				msg,
			});

			try {
				await this.processMessageSend(message);
				this.$input.removeData('reply').trigger('dataChange');
			} catch (error) {
				handleError(error);
			}
			return done();
		}

		if (this.editing.id) {
			const message = this.collection.findOne(this.editing.id);
			const isDescription = message.attachments && message.attachments[0] && message.attachments[0].description;

			try {
				if (isDescription) {
					await this.processMessageEditing({ _id: this.editing.id, rid, msg: '' });
					return done();
				}

				this.resetToDraft(this.editing.id);
				this.confirmDeleteMsg(message, done);
				return;
			} catch (error) {
				handleError(error);
			}
		}

		return done();
	}

	async processMessageSend(message) {
		if (await this.processSetReaction(message)) {
			return;
		}

		this.clearCurrentDraft();

		if (await this.processTooLongMessage(message)) {
			return;
		}

		if (await this.processMessageEditing({ ...message, _id: this.editing.id })) {
			return;
		}

		KonchatNotification.removeRoomNotification(message.rid);

		if (await this.processSlashCommand(message)) {
			return;
		}

		await callWithErrorHandling('sendMessage', message);
	}

	async processSetReaction({ rid, tmid, msg }) {
		if (msg.slice(0, 2) !== '+:') {
			return false;
		}

		const reaction = msg.slice(1).trim();
		if (!emoji.list[reaction]) {
			return false;
		}

		const lastMessage = this.collection.findOne({ rid, tmid }, { fields: { ts: 1 }, sort: { ts: -1 } });
		await callWithErrorHandling('setReaction', reaction, lastMessage._id);
		return true;
	}

	async processTooLongMessage({ msg, rid, tmid }) {
		const adjustedMessage = messageProperties.messageWithoutEmojiShortnames(msg);
		if (messageProperties.length(adjustedMessage) <= settings.get('Message_MaxAllowedSize') && msg) {
			return false;
		}

		if (!settings.get('FileUpload_Enabled') || !settings.get('Message_AllowConvertLongMessagesToAttachment') || this.editing.id) {
			throw new Error({ error: 'Message_too_long' });
		}

		const onConfirm = () => {
			const contentType = 'text/plain';
			const messageBlob = new Blob([msg], { type: contentType });
			const fileName = `${ Meteor.user().username } - ${ new Date() }.txt`;
			const file = new File([messageBlob], fileName, { type: contentType, lastModified: Date.now() });
			fileUpload([{ file, name: fileName }], this.input, { rid, tmid });
			imperativeModal.close();
		};

		const onClose = () => {
			messageBoxState.set(this.input, msg);
			imperativeModal.close();
		};

		imperativeModal.open({
			component: GenericModal,
			props: {
				title: t('Message_too_long'),
				children: t('Send_it_as_attachment_instead_question'),
				onConfirm,
				onClose,
				onCancel: onClose,
				variant: 'warning',
			},
		});

		return true;
	}

	async processMessageEditing(message) {
		if (!message._id) {
			return false;
		}

		if (MessageTypes.isSystemMessage(message)) {
			return false;
		}

		this.clearEditing();
		await callWithErrorHandling('updateMessage', message);
		return true;
	}

	async processSlashCommand(msgObject) {
		if (msgObject.msg[0] === '/') {
			const match = msgObject.msg.match(/^\/([^\s]+)(?:\s+(.*))?$/m);
			if (match) {
				let command;
				if (slashCommands.commands[match[1]]) {
					const commandOptions = slashCommands.commands[match[1]];
					command = match[1];
					const param = match[2] || '';

					if (!commandOptions.permission || hasAtLeastOnePermission(commandOptions.permission, Session.get('openedRoom'))) {
						if (commandOptions.clientOnly) {
							commandOptions.callback(command, param, msgObject);
						} else {
							const triggerId = generateTriggerId(slashCommands.commands[command].appId);
							Meteor.call('slashCommand', { cmd: command, params: param, msg: msgObject, triggerId }, (err, result) => {
								typeof commandOptions.result === 'function' && commandOptions.result(err, result, { cmd: command, params: param, msg: msgObject });
							});
						}

						return true;
					}
				}

				if (!settings.get('Message_AllowUnrecognizedSlashCommand')) {
					const invalidCommandMsg = {
						_id: Random.id(),
						rid: msgObject.rid,
						ts: new Date(),
						msg: TAPi18n.__('No_such_command', { command: escapeHTML(match[1]) }),
						u: {
							username: settings.get('InternalHubot_Username') || 'rocket.cat',
						},
						private: true,
					};

					this.collection.upsert({ _id: invalidCommandMsg._id }, invalidCommandMsg);
					return true;
				}
			}
		}

		return false;
	}

	confirmDeleteMsg(message, done = () => {}) {
		if (MessageTypes.isSystemMessage(message)) {
			return done();
		}

		const room = message.drid && Rooms.findOne({
			_id: message.drid,
			prid: { $exists: true },
		});

		const onConfirm = () => {
			if (this.editing.id === message._id) {
				this.clearEditing();
			}

			this.deleteMsg(message);

			this.$input.focus();
			done();

			imperativeModal.close();
			dispatchToastMessage({ type: 'success', message: t('Your_entry_has_been_deleted') });
		};

		const onCloseModal = () => {
			imperativeModal.close();
			if (this.editing.id === message._id) {
				this.clearEditing();
			}
			this.$input.focus();
			done();
		};

		imperativeModal.open({
			component: GenericModal,
			props: {
				title: t('Are_you_sure'),
				children: room ? t('The_message_is_a_discussion_you_will_not_be_able_to_recover') : t('You_will_not_be_able_to_recover'),
				variant: 'danger',
				confirmText: t('Yes_delete_it'),
				onConfirm,
				onClose: onCloseModal,
				onCancel: onCloseModal,
			},
		});
	}

	async deleteMsg({ _id, rid, ts }) {
		const forceDelete = hasAtLeastOnePermission('force-delete-message', rid);
		const blockDeleteInMinutes = settings.get('Message_AllowDeleting_BlockDeleteInMinutes');
		if (blockDeleteInMinutes && forceDelete === false) {
			let msgTs;
			if (ts) {
				msgTs = moment(ts);
			}
			let currentTsDiff;
			if (msgTs) {
				currentTsDiff = moment().diff(msgTs, 'minutes');
			}
			if (currentTsDiff > blockDeleteInMinutes) {
				dispatchToastMessage({ type: 'error', message: t('Message_deleting_blocked') });
				return;
			}
		}

		await callWithErrorHandling('deleteMessage', { _id });
	}

	keydown(event) {
		const { currentTarget: input, which: keyCode } = event;

		if (keyCode === keyCodes.ESCAPE && this.editing.element) {
			event.preventDefault();
			event.stopPropagation();

			if (!this.resetToDraft(this.editing.id)) {
				this.clearCurrentDraft();
				this.clearEditing();
				return true;
			}

			return;
		}

		if (keyCode === keyCodes.ARROW_UP || keyCode === keyCodes.ARROW_DOWN) {
			if (event.shiftKey) {
				return;
			}

			const cursorPosition = input.selectionEnd;

			if (keyCode === keyCodes.ARROW_UP) {
				if (cursorPosition === 0) {
					this.toPrevMessage();
				} else if (!event.altKey) {
					return;
				}

				if (event.altKey) {
					this.$input.setCursorPosition(0);
				}
			} else {
				if (cursorPosition === input.value.length) {
					this.toNextMessage();
				} else if (!event.altKey) {
					return;
				}

				if (event.altKey) {
					this.$input.setCursorPosition(-1);
				}
			}

			event.preventDefault();
			event.stopPropagation();
		}
	}

	keyup(event, { rid, tmid }) {
		const { currentTarget: input, which: keyCode } = event;

		if (!Object.values(keyCodes).includes(keyCode)) {
			if (input.value.trim()) {
				UserAction.start(rid, USER_ACTIVITIES.USER_TYPING, { tmid });
			} else {
				UserAction.stop(rid, USER_ACTIVITIES.USER_TYPING, { tmid });
			}
		}

		messageBoxState.save({ rid, tmid }, input);
	}

	onDestroyed(rid) {
		UserAction.cancel(rid);
	}
}


import { Settings } from '../../../app/models/server/raw';
import { addMigration } from '../../lib/migrations';

addMigration({
	version: 234,
	up() {
		return Settings.deleteMany({
			_id: {
				$in: [
					'GoogleVision_Enable',
					'GoogleVision_ServiceAccount',
					'GoogleVision_Max_Monthly_Calls',
					'GoogleVision_Current_Month',
					'GoogleVision_Current_Month_Calls',
					'GoogleVision_Type_Document',
					'GoogleVision_Type_Faces',
					'GoogleVision_Type_Landmarks',
					'GoogleVision_Type_Labels',
					'GoogleVision_Type_Logos',
					'GoogleVision_Type_Properties',
					'GoogleVision_Type_SafeSearch',
					'GoogleVision_Block_Adult_Images',
					'GoogleVision_Type_Similar',
				],
			},
		});
	},
});


/* eslint-disable @typescript-eslint/interface-name-prefix */

interface Navigator {
	/** @deprecated */
	readonly userLanguage?: string;
}

interface Window {
	setLanguage?: (language: string) => void;
	defaultUserLanguage?: () => string;
	DISABLE_ANIMATION?: boolean;
	lastMessageWindow?: Record<string, unknown>;
	lastMessageWindowHistory?: Record<string, unknown>;
	favico?: any;
	USE_REST_FOR_DDP_CALLS?: boolean;
	ECDH_Enabled?: boolean;
	__meteor_runtime_config__: {
		ROOT_URL_PATH_PREFIX: string;
		ROOT_URL: string;
	};
}

interface PromiseConstructor {
	await<T>(promise: Promise<T>): T;
	await<T>(value: T): T;
}


if (this.input.parentElement.classList.contains('editing') === true) {

messageBoxState.save({ rid, tmid }, this.$input);


import { Meteor } from 'meteor/meteor';

import { API } from '../api';

API.v1.addRoute('e2e.fetchMyKeys', { authRequired: true }, {
	get() {
		let result;
		Meteor.runAsUser(this.userId, () => { result = Meteor.call('e2e.fetchMyKeys'); });

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

API.v1.addRoute('e2e.getUsersOfRoomWithoutKey', { authRequired: true }, {
	get() {
		const { rid } = this.queryParams;

		let result;
		Meteor.runAsUser(this.userId, () => { result = Meteor.call('e2e.getUsersOfRoomWithoutKey', rid); });

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

/**
 * @openapi
 *  /api/v1/e2e.setRoomKeyID:
 *    post:
 *      description: Sets the end-to-end encryption key ID for a room
 *      security:
 *        - autenticated: {}
 *      requestBody:
 *        description: A tuple containing the room ID and the key ID
 *        content:
 *          application/json:
 *            schema:
 *              type: object
 *              properties:
 *                rid:
 *                  type: string
 *                keyID:
 *                  type: string
 *      responses:
 *        200:
 *          content:
 *            application/json:
 *              schema:
 *                $ref: '#/components/schemas/ApiSuccessV1'
 *        default:
 *          description: Unexpected error
 *          content:
 *            application/json:
 *              schema:
 *                $ref: '#/components/schemas/ApiFailureV1'
 */
API.v1.addRoute('e2e.setRoomKeyID', { authRequired: true }, {
	post() {
		const { rid, keyID } = this.bodyParams;

		Meteor.runAsUser(this.userId, () => {
			API.v1.success(Meteor.call('e2e.setRoomKeyID', rid, keyID));
		});

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

/**
 * @openapi
 *  /api/v1/e2e.setUserPublicAndPrivateKeys:
 *    post:
 *      description: Sets the end-to-end encryption keys for the authenticated user
 *      security:
 *        - autenticated: {}
 *      requestBody:
 *        description: A tuple containing the public and the private keys
 *        content:
 *          application/json:
 *            schema:
 *              type: object
 *              properties:
 *                public_key:
 *                  type: string
 *                private_key:
 *                  type: string
 *      responses:
 *        200:
 *          content:
 *            application/json:
 *              schema:
 *                $ref: '#/components/schemas/ApiSuccessV1'
 *        default:
 *          description: Unexpected error
 *          content:
 *            application/json:
 *              schema:
 *                $ref: '#/components/schemas/ApiFailureV1'
 */
API.v1.addRoute('e2e.setUserPublicAndPrivateKeys', { authRequired: true }, {
	post() {
		const { public_key, private_key } = this.bodyParams;

		Meteor.runAsUser(this.userId, () => {
			API.v1.success(Meteor.call('e2e.setUserPublicAndPrivateKeys', {
				public_key,
				private_key,
			}));
		});

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

/**
 * @openapi
 *  /api/v1/e2e.updateGroupKey:
 *    post:
 *      description: Updates the end-to-end encryption key for a user on a room
 *      security:
 *        - autenticated: {}
 *      requestBody:
 *        description: A tuple containing the user ID, the room ID, and the key
 *        content:
 *          application/json:
 *            schema:
 *              type: object
 *              properties:
 *                uid:
 *                  type: string
 *                rid:
 *                  type: string
 *                key:
 *                  type: string
 *      responses:
 *        200:
 *          content:
 *            application/json:
 *              schema:
 *                $ref: '#/components/schemas/ApiSuccessV1'
 *        default:
 *          description: Unexpected error
 *          content:
 *            application/json:
 *              schema:
 *                $ref: '#/components/schemas/ApiFailureV1'
 */
API.v1.addRoute('e2e.updateGroupKey', { authRequired: true }, {
	post() {
		const { uid, rid, key } = this.bodyParams;

		Meteor.runAsUser(this.userId, () => {
			API.v1.success(Meteor.call('e2e.updateGroupKey', rid, uid, key));
		});

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


import moment from 'moment-timezone';

import { settings } from '../../../settings/server';
import { SettingValue } from '../../../../definition/ISetting';

const padOffset = (offset: string | number): string => {
	const numberOffset = Number(offset);
	const absOffset = Math.abs(numberOffset);
	const isNegative = !(numberOffset === absOffset);

	return `${ isNegative ? '-' : '+' }${ absOffset < 10 ? `0${ absOffset }` : absOffset }:00`;
};

const guessTimezoneFromOffset = (offset: string | number): string => moment.tz.names()
	.find((tz) => padOffset(offset) === moment.tz(tz).format('Z').toString()) || moment.tz.guess();

export const getTimezone = (user: { utcOffset: string | number }): string | void | SettingValue => {
	const timezone = settings.get('Default_Timezone_For_Reporting');

	switch (timezone) {
		case 'custom':
			return settings.get('Default_Custom_Timezone');
		case 'user':
			return guessTimezoneFromOffset(user.utcOffset);
		default:
			return moment.tz.guess();
	}
};


import type { TranslationKey } from '../../../client/contexts/TranslationContext';

export type LDAPEndpoints = {
	'ldap.testConnection': {
		POST: () => {
			message: TranslationKey;
		};
	};
	'ldap.testSearch': {
		POST: (params: { username: string }) => {
			message: TranslationKey;
		};
	};
	'ldap.syncNow': {
		POST: () => {
			message: TranslationKey;
		};
	};
};


description: 'Filter Rundeck nodes by name',

import type {
	IExecuteFunctions,
	IDataObject,
	INodeExecutionData,
	INodeType,
	INodeTypeDescription,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { RundeckApi } from './RundeckApi';

export class Rundeck implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'Rundeck',
		name: 'rundeck',
		// eslint-disable-next-line n8n-nodes-base/node-class-description-icon-not-svg
		icon: 'file:rundeck.png',
		group: ['transform'],
		version: 1,
		subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
		description: 'Manage Rundeck API',
		defaults: {
			name: 'Rundeck',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'rundeckApi',
				required: true,
			},
		],
		properties: [
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Job',
						value: 'job',
					},
				],
				default: 'job',
			},
			{
				displayName: 'Operation',
				name: 'operation',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Execute',
						value: 'execute',
						description: 'Execute a job',
						action: 'Execute a job',
					},
					{
						name: 'Get Metadata',
						value: 'getMetadata',
						description: 'Get metadata of a job',
						action: 'Get metadata of a job',
					},
				],
				default: 'execute',
			},

			// ----------------------------------
			//         job:execute
			// ----------------------------------
			{
				displayName: 'Job ID',
				name: 'jobid',
				type: 'string',
				displayOptions: {
					show: {
						operation: ['execute'],
						resource: ['job'],
					},
				},
				default: '',
				placeholder: 'Rundeck Job ID',
				required: true,
				description: 'The job ID to execute',
			},
			{
				displayName: 'Arguments',
				name: 'arguments',
				placeholder: 'Add Argument',
				type: 'fixedCollection',
				typeOptions: {
					multipleValues: true,
				},
				displayOptions: {
					show: {
						operation: ['execute'],
						resource: ['job'],
					},
				},
				default: {},
				options: [
					{
						name: 'arguments',
						displayName: 'Arguments',
						values: [
							{
								displayName: 'Name',
								name: 'name',
								type: 'string',
								default: '',
							},
							{
								displayName: 'Value',
								name: 'value',
								type: 'string',
								default: '',
							},
						],
					},
				],
			},

			// ----------------------------------
			//         job:getMetadata
			// ----------------------------------
			{
				displayName: 'Job ID',
				name: 'jobid',
				type: 'string',
				displayOptions: {
					show: {
						operation: ['getMetadata'],
						resource: ['job'],
					},
				},
				default: '',
				placeholder: 'Rundeck Job ID',
				required: true,
				description: 'The job ID to get metadata off',
			},
		],
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		// Input data
		const items = this.getInputData();
		const returnData: IDataObject[] = [];
		const length = items.length;

		const operation = this.getNodeParameter('operation', 0);
		const resource = this.getNodeParameter('resource', 0);
		const rundeckApi = new RundeckApi(this);
		await rundeckApi.init();

		for (let i = 0; i < length; i++) {
			if (resource === 'job') {
				if (operation === 'execute') {
					// ----------------------------------
					//         job: execute
					// ----------------------------------
					const jobid = this.getNodeParameter('jobid', i) as string;
					const rundeckArguments = (this.getNodeParameter('arguments', i) as IDataObject)
						.arguments as IDataObject[];
					const response = await rundeckApi.executeJob(jobid, rundeckArguments);

					returnData.push(response);
				} else if (operation === 'getMetadata') {
					// ----------------------------------
					//         job: getMetadata
					// ----------------------------------
					const jobid = this.getNodeParameter('jobid', i) as string;
					const response = await rundeckApi.getJobMetadata(jobid);

					returnData.push(response);
				} else {
					throw new NodeOperationError(
						this.getNode(),
						`The operation "${operation}" is not supported!`,
						{ itemIndex: i },
					);
				}
			} else {
				throw new NodeOperationError(
					this.getNode(),
					`The resource "${resource}" is not supported!`,
					{ itemIndex: i },
				);
			}
		}

		return [this.helpers.returnJsonArray(returnData)];
	}
}


/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable consistent-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-shadow */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/unbound-method */
import 'source-map-support/register';
import 'reflect-metadata';
import { Container } from 'typedi';
import type { IProcessMessage } from 'n8n-core';
import { BinaryDataManager, UserSettings, WorkflowExecute } from 'n8n-core';

import type {
	ExecutionError,
	IDataObject,
	IExecuteResponsePromiseData,
	IExecuteWorkflowInfo,
	ILogger,
	INode,
	INodeExecutionData,
	IRun,
	ITaskData,
	IWorkflowExecuteAdditionalData,
	IWorkflowExecuteHooks,
	IWorkflowSettings,
	NodeOperationError,
	WorkflowExecuteMode,
} from 'n8n-workflow';
import {
	ErrorReporterProxy as ErrorReporter,
	LoggerProxy,
	Workflow,
	WorkflowHooks,
	WorkflowOperationError,
} from 'n8n-workflow';
import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import * as Db from '@/Db';
import { ExternalHooks } from '@/ExternalHooks';
import type {
	IWorkflowExecuteProcess,
	IWorkflowExecutionDataProcessWithExecution,
} from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowHelpers from '@/WorkflowHelpers';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { getLogger } from '@/Logger';

import config from '@/config';
import { generateFailedExecutionFromError } from '@/WorkflowHelpers';
import { initErrorHandling } from '@/ErrorReporting';
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
import { License } from '@/License';
import { InternalHooks } from '@/InternalHooks';
import { PostHogClient } from '@/posthog';

class WorkflowRunnerProcess {
	data: IWorkflowExecutionDataProcessWithExecution | undefined;

	logger: ILogger;

	startedAt = new Date();

	workflow: Workflow | undefined;

	workflowExecute: WorkflowExecute | undefined;

	// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
	executionIdCallback: (executionId: string) => void | undefined;

	childExecutions: {
		[key: string]: IWorkflowExecuteProcess;
	} = {};

	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
	static async stopProcess() {
		setTimeout(() => {
			// Attempt a graceful shutdown, giving executions 30 seconds to finish
			process.exit(0);
		}, 30000);
	}

	async runWorkflow(inputData: IWorkflowExecutionDataProcessWithExecution): Promise<IRun> {
		process.once('SIGTERM', WorkflowRunnerProcess.stopProcess);
		process.once('SIGINT', WorkflowRunnerProcess.stopProcess);

		await initErrorHandling();

		// eslint-disable-next-line no-multi-assign
		const logger = (this.logger = getLogger());
		LoggerProxy.init(logger);

		this.data = inputData;
		const { userId } = inputData;

		logger.verbose('Initializing n8n sub-process', {
			pid: process.pid,
			workflowId: this.data.workflowData.id,
		});

		this.startedAt = new Date();

		const userSettings = await UserSettings.prepareUserSettings();

		const loadNodesAndCredentials = Container.get(LoadNodesAndCredentials);
		await loadNodesAndCredentials.init();

		const nodeTypes = Container.get(NodeTypes);
		const credentialTypes = Container.get(CredentialTypes);
		CredentialsOverwrites(credentialTypes);

		// Load all external hooks
		const externalHooks = Container.get(ExternalHooks);
		await externalHooks.init();

		// Init db since we need to read the license.
		await Db.init();

		const instanceId = userSettings.instanceId ?? '';
		await Container.get(PostHogClient).init(instanceId);
		await Container.get(InternalHooks).init(instanceId);

		const binaryDataConfig = config.getEnv('binaryDataManager');
		await BinaryDataManager.init(binaryDataConfig);

		const license = Container.get(License);
		await license.init(instanceId);

		const workflowSettings = this.data.workflowData.settings ?? {};

		// Start timeout for the execution
		let workflowTimeout = workflowSettings.executionTimeout ?? config.getEnv('executions.timeout'); // initialize with default
		if (workflowTimeout > 0) {
			workflowTimeout = Math.min(workflowTimeout, config.getEnv('executions.maxTimeout'));
		}

		this.workflow = new Workflow({
			id: this.data.workflowData.id,
			name: this.data.workflowData.name,
			nodes: this.data.workflowData.nodes,
			connections: this.data.workflowData.connections,
			active: this.data.workflowData.active,
			nodeTypes,
			staticData: this.data.workflowData.staticData,
			settings: this.data.workflowData.settings,
			pinData: this.data.pinData,
		});
		try {
			await PermissionChecker.check(this.workflow, userId);
		} catch (error) {
			const caughtError = error as NodeOperationError;
			const failedExecutionData = generateFailedExecutionFromError(
				this.data.executionMode,
				caughtError,
				caughtError.node,
			);

			// Force the `workflowExecuteAfter` hook to run since
			// it's the one responsible for saving the execution
			await this.sendHookToParentProcess('workflowExecuteAfter', [failedExecutionData]);
			// Interrupt the workflow execution since we don't have all necessary creds.
			return failedExecutionData;
		}
		const additionalData = await WorkflowExecuteAdditionalData.getBase(
			userId,
			undefined,
			workflowTimeout <= 0 ? undefined : Date.now() + workflowTimeout * 1000,
		);
		additionalData.restartExecutionId = this.data.restartExecutionId;
		additionalData.hooks = this.getProcessForwardHooks();

		additionalData.hooks.hookFunctions.sendResponse = [
			async (response: IExecuteResponsePromiseData): Promise<void> => {
				await sendToParentProcess('sendResponse', {
					response: WebhookHelpers.encodeWebhookResponse(response),
				});
			},
		];

		additionalData.executionId = inputData.executionId;

		additionalData.setExecutionStatus = WorkflowExecuteAdditionalData.setExecutionStatus.bind({
			executionId: inputData.executionId,
		});
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
		additionalData.sendMessageToUI = async (source: string, message: any) => {
			if (workflowRunner.data!.executionMode !== 'manual') {
				return;
			}

			try {
				// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
				await sendToParentProcess('sendMessageToUI', { source, message });
			} catch (error) {
				ErrorReporter.error(error);
				this.logger.error(
					// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
					`There was a problem sending UI data to parent process: "${error.message}"`,
				);
			}
		};
		const executeWorkflowFunction = additionalData.executeWorkflow;
		additionalData.executeWorkflow = async (
			workflowInfo: IExecuteWorkflowInfo,
			additionalData: IWorkflowExecuteAdditionalData,
			options?: {
				parentWorkflowId?: string;
				inputData?: INodeExecutionData[];
				parentWorkflowSettings?: IWorkflowSettings;
			},
		): Promise<Array<INodeExecutionData[] | null> | IRun> => {
			const workflowData = await WorkflowExecuteAdditionalData.getWorkflowData(
				workflowInfo,
				options?.parentWorkflowId,
				options?.parentWorkflowSettings,
			);
			const runData = await WorkflowExecuteAdditionalData.getRunData(
				workflowData,
				additionalData.userId,
				options?.inputData,
				options?.parentWorkflowId,
			);
			await sendToParentProcess('startExecution', { runData });
			const executionId: string = await new Promise((resolve) => {
				this.executionIdCallback = (executionId: string) => {
					resolve(executionId);
				};
			});

			void Container.get(InternalHooks).onWorkflowBeforeExecute(executionId || '', runData);

			let result: IRun;
			try {
				const executeWorkflowFunctionOutput = (await executeWorkflowFunction(
					workflowInfo,
					additionalData,
					{
						parentWorkflowId: options?.parentWorkflowId,
						inputData: options?.inputData,
						parentExecutionId: executionId,
						loadedWorkflowData: workflowData,
						loadedRunData: runData,
						parentWorkflowSettings: options?.parentWorkflowSettings,
					},
				)) as { workflowExecute: WorkflowExecute; workflow: Workflow } as IWorkflowExecuteProcess;
				const { workflowExecute } = executeWorkflowFunctionOutput;
				this.childExecutions[executionId] = executeWorkflowFunctionOutput;
				const { workflow } = executeWorkflowFunctionOutput;
				result = await workflowExecute.processRunExecutionData(workflow);
				await externalHooks.run('workflow.postExecute', [result, workflowData, executionId]);
				void Container.get(InternalHooks).onWorkflowPostExecute(
					executionId,
					workflowData,
					result,
					additionalData.userId,
				);
				await sendToParentProcess('finishExecution', { executionId, result });
				delete this.childExecutions[executionId];
			} catch (e) {
				await sendToParentProcess('finishExecution', { executionId });
				delete this.childExecutions[executionId];
				// Throw same error we had
				throw e;
			}

			await sendToParentProcess('finishExecution', { executionId, result });

			const returnData = WorkflowHelpers.getDataLastExecutedNodeData(result);

			if (returnData!.error) {
				const error = new Error(returnData!.error.message);
				error.stack = returnData!.error.stack;
				throw error;
			}

			return returnData!.data!.main;
		};

		if (this.data.executionData !== undefined) {
			this.workflowExecute = new WorkflowExecute(
				additionalData,
				this.data.executionMode,
				this.data.executionData,
			);
			return this.workflowExecute.processRunExecutionData(this.workflow);
		}
		if (
			this.data.runData === undefined ||
			this.data.startNodes === undefined ||
			this.data.startNodes.length === 0 ||
			this.data.destinationNode === undefined
		) {
			// Execute all nodes

			let startNode;
			if (
				this.data.startNodes?.length === 1 &&
				Object.keys(this.data.pinData ?? {}).includes(this.data.startNodes[0])
			) {
				startNode = this.workflow.getNode(this.data.startNodes[0]) ?? undefined;
			}

			// Can execute without webhook so go on
			this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
			return this.workflowExecute.run(
				this.workflow,
				startNode,
				this.data.destinationNode,
				this.data.pinData,
			);
		}
		// Execute only the nodes between start and destination nodes
		this.workflowExecute = new WorkflowExecute(additionalData, this.data.executionMode);
		return this.workflowExecute.runPartialWorkflow(
			this.workflow,
			this.data.runData,
			this.data.startNodes,
			this.data.destinationNode,
			this.data.pinData,
		);
	}

	/**
	 * Sends hook data to the parent process that it executes them
	 *
	 */
	// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
	async sendHookToParentProcess(hook: string, parameters: any[]) {
		try {
			await sendToParentProcess('processHook', {
				hook,
				parameters,
			});
		} catch (error) {
			ErrorReporter.error(error);
			this.logger.error(`There was a problem sending hook: "${hook}"`, { parameters, error });
		}
	}

	/**
	 * Create a wrapper for hooks which simply forwards the data to
	 * the parent process where they then can be executed with access
	 * to database and to PushService
	 *
	 */
	getProcessForwardHooks(): WorkflowHooks {
		const hookFunctions: IWorkflowExecuteHooks = {
			nodeExecuteBefore: [
				async (nodeName: string): Promise<void> => {
					await this.sendHookToParentProcess('nodeExecuteBefore', [nodeName]);
				},
			],
			nodeExecuteAfter: [
				async (nodeName: string, data: ITaskData): Promise<void> => {
					await this.sendHookToParentProcess('nodeExecuteAfter', [nodeName, data]);
				},
			],
			workflowExecuteBefore: [
				async (): Promise<void> => {
					await this.sendHookToParentProcess('workflowExecuteBefore', []);
				},
			],
			workflowExecuteAfter: [
				async (fullRunData: IRun, newStaticData?: IDataObject): Promise<void> => {
					await this.sendHookToParentProcess('workflowExecuteAfter', [fullRunData, newStaticData]);
				},
			],
			nodeFetchedData: [
				async (workflowId: string, node: INode) => {
					await this.sendHookToParentProcess('nodeFetchedData', [workflowId, node]);
				},
			],
		};

		const preExecuteFunctions = WorkflowExecuteAdditionalData.hookFunctionsPreExecute();
		// eslint-disable-next-line no-restricted-syntax
		for (const key of Object.keys(preExecuteFunctions)) {
			if (hookFunctions[key] === undefined) {
				hookFunctions[key] = [];
			}
			hookFunctions[key]!.push.apply(hookFunctions[key], preExecuteFunctions[key]);
		}

		return new WorkflowHooks(
			hookFunctions,
			this.data!.executionMode,
			this.data!.executionId,
			this.data!.workflowData,
			{ sessionId: this.data!.sessionId, retryOf: this.data!.retryOf as string },
		);
	}
}

/**
 * Sends data to parent process
 *
 * @param {string} type The type of data to send
 * @param {*} data The data
 */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function sendToParentProcess(type: string, data: any): Promise<void> {
	return new Promise((resolve, reject) => {
		process.send!(
			{
				type,
				data,
			},
			(error: Error) => {
				if (error) {
					return reject(error);
				}

				resolve();
			},
		);
	});
}

const workflowRunner = new WorkflowRunnerProcess();

// Listen to messages from parent process which send the data of
// the workflow to process
process.on('message', async (message: IProcessMessage) => {
	try {
		if (message.type === 'startWorkflow') {
			await sendToParentProcess('start', {});

			const runData = await workflowRunner.runWorkflow(message.data);

			await sendToParentProcess('end', {
				runData,
			});

			// Once the workflow got executed make sure the process gets killed again
			process.exit();
		} else if (message.type === 'stopExecution' || message.type === 'timeout') {
			// The workflow execution should be stopped
			let runData: IRun;

			if (workflowRunner.workflowExecute !== undefined) {
				const executionIds = Object.keys(workflowRunner.childExecutions);

				// eslint-disable-next-line no-restricted-syntax
				for (const executionId of executionIds) {
					const childWorkflowExecute = workflowRunner.childExecutions[executionId];
					runData = childWorkflowExecute.workflowExecute.getFullRunData(
						workflowRunner.childExecutions[executionId].startedAt,
					);
					const timeOutError =
						message.type === 'timeout'
							? new WorkflowOperationError('Workflow execution timed out!')
							: new WorkflowOperationError('Workflow-Execution has been canceled!');

					// If there is any data send it to parent process, if execution timedout add the error
					// eslint-disable-next-line no-await-in-loop
					await childWorkflowExecute.workflowExecute.processSuccessExecution(
						workflowRunner.childExecutions[executionId].startedAt,
						childWorkflowExecute.workflow,
						timeOutError,
					);
				}

				// Workflow started already executing
				runData = workflowRunner.workflowExecute.getFullRunData(workflowRunner.startedAt);

				const timeOutError =
					message.type === 'timeout'
						? new WorkflowOperationError('Workflow execution timed out!')
						: new WorkflowOperationError('Workflow-Execution has been canceled!');

				runData.status = message.type === 'timeout' ? 'failed' : 'canceled';

				// If there is any data send it to parent process, if execution timedout add the error
				await workflowRunner.workflowExecute.processSuccessExecution(
					workflowRunner.startedAt,
					workflowRunner.workflow!,
					timeOutError,
				);
			} else {
				// Workflow did not get started yet
				runData = {
					data: {
						resultData: {
							runData: {},
						},
					},
					finished: false,
					mode: workflowRunner.data
						? workflowRunner.data.executionMode
						: ('own' as WorkflowExecuteMode),
					startedAt: workflowRunner.startedAt,
					stoppedAt: new Date(),
					status: 'canceled',
				};

				await workflowRunner.sendHookToParentProcess('workflowExecuteAfter', [runData]);
			}

			await sendToParentProcess(message.type === 'timeout' ? message.type : 'end', {
				runData,
			});

			// Stop process
			process.exit();
		} else if (message.type === 'executionId') {
			// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
			workflowRunner.executionIdCallback(message.data.executionId);
		}
	} catch (error) {
		workflowRunner.logger.error(error.message);

		// Catch all uncaught errors and forward them to parent process
		const executionError = {
			...error,
			name: error.name || 'Error',
			message: error.message,
			stack: error.stack,
		} as ExecutionError;

		await sendToParentProcess('processError', {
			executionError,
		});
		process.exit();
	}
});


import type {
	IExecuteFunctions,
	IDataObject,
	INodeExecutionData,
	INodeType,
	INodeTypeDescription,
} from 'n8n-workflow';

import { rocketchatApiRequest, validateJSON } from './GenericFunctions';

interface IField {
	short?: boolean;
	title?: string;
	value?: string;
}

interface IAttachment {
	color?: string;
	text?: string;
	ts?: string;
	title?: string;
	thumb_url?: string;
	message_link?: string;
	collapsed?: boolean;
	author_name?: string;
	author_link?: string;
	author_icon?: string;
	title_link?: string;
	title_link_download?: boolean;
	image_url?: string;
	audio_url?: string;
	video_url?: string;
	fields?: IField[];
}

interface IPostMessageBody {
	channel: string;
	text?: string;
	alias?: string;
	emoji?: string;
	avatar?: string;
	attachments?: IAttachment[];
}

export class Rocketchat implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'RocketChat',
		name: 'rocketchat',
		icon: 'file:rocketchat.svg',
		group: ['output'],
		version: 1,
		subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
		description: 'Consume RocketChat API',
		defaults: {
			name: 'RocketChat',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [
			{
				name: 'rocketchatApi',
				required: true,
			},
		],
		properties: [
			{
				displayName: 'Resource',
				name: 'resource',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Chat',
						value: 'chat',
					},
				],
				default: 'chat',
			},
			{
				displayName: 'Operation',
				name: 'operation',
				type: 'options',
				noDataExpression: true,
				displayOptions: {
					show: {
						resource: ['chat'],
					},
				},
				options: [
					{
						name: 'Post Message',
						value: 'postMessage',
						description: 'Post a message to a channel or a direct message',
						action: 'Post a message',
					},
				],
				default: 'postMessage',
			},
			{
				displayName: 'Channel',
				name: 'channel',
				type: 'string',
				required: true,
				displayOptions: {
					show: {
						resource: ['chat'],
						operation: ['postMessage'],
					},
				},
				default: '',
				description: 'The channel name with the prefix in front of it',
			},
			{
				displayName: 'Text',
				name: 'text',
				type: 'string',
				displayOptions: {
					show: {
						resource: ['chat'],
						operation: ['postMessage'],
					},
				},
				default: '',
				description: 'The text of the message to send, is optional because of attachments',
			},
			{
				displayName: 'JSON Parameters',
				name: 'jsonParameters',
				type: 'boolean',
				default: false,
				displayOptions: {
					show: {
						resource: ['chat'],
						operation: ['postMessage'],
					},
				},
			},
			{
				displayName: 'Options',
				name: 'options',
				type: 'collection',
				placeholder: 'Add Option',
				default: {},
				displayOptions: {
					show: {
						resource: ['chat'],
						operation: ['postMessage'],
					},
				},
				options: [
					{
						displayName: 'Alias',
						name: 'alias',
						type: 'string',
						default: '',
						description:
							'This will cause the message’s name to appear as the given alias, but your username will still display',
					},
					{
						displayName: 'Avatar',
						name: 'avatar',
						type: 'string',
						default: '',
						description: 'If provided, this will make the avatar use the provided image URL',
					},
					{
						displayName: 'Emoji',
						name: 'emoji',
						type: 'string',
						default: '',
						description:
							'This will cause the message’s name to appear as the given alias, but your username will still display',
					},
				],
			},
			{
				displayName: 'Attachments',
				name: 'attachments',
				type: 'collection',
				default: {},
				placeholder: 'Add Attachment Item',
				typeOptions: {
					multipleValues: true,
					multipleValueButtonText: 'Add Attachment',
				},
				displayOptions: {
					show: {
						resource: ['chat'],
						operation: ['postMessage'],
						jsonParameters: [false],
					},
				},
				options: [
					{
						displayName: 'Color',
						name: 'color',
						type: 'color',
						default: '#ff0000',
						description:
							'The color you want the order on the left side to be, any value background-css supports',
					},
					{
						displayName: 'Text',
						name: 'text',
						type: 'string',
						default: '',
						description:
							'The text to display for this attachment, it is different than the message’s text',
					},
					{
						displayName: 'Timestamp',
						name: 'ts',
						type: 'dateTime',
						default: '',
						description: 'Displays the time next to the text portion',
					},
					{
						displayName: 'Thumb URL',
						name: 'thumbUrl',
						type: 'string',
						default: '',
						description:
							'An image that displays to the left of the text, looks better when this is relatively small',
					},
					{
						displayName: 'Message Link',
						name: 'messageLink',
						type: 'string',
						default: '',
						description:
							'Only applicable if the timestamp is provided, as it makes the time clickable to this link',
					},
					{
						displayName: 'Collapsed',
						name: 'collapsed',
						type: 'boolean',
						default: false,
						// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
						description:
							'Causes the image, audio, and video sections to be hiding when collapsed is true',
					},
					{
						displayName: 'Author Name',
						name: 'authorName',
						type: 'string',
						default: '',
						description: 'Name of the author',
					},
					{
						displayName: 'Author Link',
						name: 'authorLink',
						type: 'string',
						default: '',
						description: 'Providing this makes the author name clickable and points to this link',
					},
					{
						displayName: 'Author Icon',
						name: 'authorIcon',
						type: 'string',
						default: '',
						placeholder: 'https://site.com/img.png',
						description: 'Displays a tiny icon to the left of the Author’s name',
					},
					{
						displayName: 'Title',
						name: 'title',
						type: 'string',
						default: '',
						description: 'Title to display for this attachment, displays under the author',
					},
					{
						displayName: 'Title Link',
						name: 'titleLink',
						type: 'string',
						default: '',
						description: 'Providing this makes the title clickable, pointing to this link',
					},
					{
						displayName: 'Title Link Download',
						name: 'titleLinkDownload',
						type: 'boolean',
						default: false,
						// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
						description:
							'When this is true, a download icon appears and clicking this saves the link to file',
					},
					{
						displayName: 'Image URL',
						name: 'imageUrl',
						type: 'string',
						default: '',
						description: 'The image to display, will be “big” and easy to see',
					},
					{
						displayName: 'Audio URL',
						name: 'audioUrl',
						type: 'string',
						default: '',
						placeholder: 'https://site.com/aud.mp3',
						description: 'Audio file to play, only supports what html audio does',
					},
					{
						displayName: 'Video URL',
						name: 'videoUrl',
						type: 'string',
						default: '',
						placeholder: 'https://site.com/vid.mp4',
						description: 'Video file to play, only supports what html video does',
					},
					{
						displayName: 'Fields',
						name: 'fields',
						type: 'fixedCollection',
						placeholder: 'Add Field Item',
						typeOptions: {
							multipleValues: true,
						},
						default: {},
						options: [
							{
								name: 'fieldsValues',
								displayName: 'Fields',
								values: [
									{
										displayName: 'Short',
										name: 'short',
										type: 'boolean',
										default: false,
										description: 'Whether this field should be a short field',
									},
									{
										displayName: 'Title',
										name: 'title',
										type: 'string',
										default: '',
										description: 'The title of this field',
									},
									{
										displayName: 'Value',
										name: 'value',
										type: 'string',
										default: '',
										description: 'The value of this field, displayed underneath the title value',
									},
								],
							},
						],
					},
				],
			},
			{
				displayName: 'Attachments',
				name: 'attachmentsJson',
				type: 'json',
				typeOptions: {
					alwaysOpenEditWindow: true,
				},
				displayOptions: {
					show: {
						resource: ['chat'],
						operation: ['postMessage'],
						jsonParameters: [true],
					},
				},
				default: '',
			},
		],
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const items = this.getInputData();
		const length = items.length;
		let responseData;
		const returnData: INodeExecutionData[] = [];
		const resource = this.getNodeParameter('resource', 0);
		const operation = this.getNodeParameter('operation', 0);
		for (let i = 0; i < length; i++) {
			try {
				if (resource === 'chat') {
					//https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage
					if (operation === 'postMessage') {
						const channel = this.getNodeParameter('channel', i) as string;
						const text = this.getNodeParameter('text', i) as string;
						const options = this.getNodeParameter('options', i);
						const jsonActive = this.getNodeParameter('jsonParameters', i);

						const body: IPostMessageBody = {
							channel,
							text,
						};

						if (options.alias) {
							body.alias = options.alias as string;
						}
						if (options.avatar) {
							body.avatar = options.avatar as string;
						}
						if (options.emoji) {
							body.emoji = options.emoji as string;
						}

						if (!jsonActive) {
							const optionsAttachments = this.getNodeParameter('attachments', i) as IDataObject[];
							if (optionsAttachments.length > 0) {
								const attachments: IAttachment[] = [];
								for (let index = 0; index < optionsAttachments.length; index++) {
									const attachment: IAttachment = {};
									for (const option of Object.keys(optionsAttachments[index])) {
										if (option === 'color') {
											attachment.color = optionsAttachments[index][option] as string;
										} else if (option === 'text') {
											attachment.text = optionsAttachments[index][option] as string;
										} else if (option === 'ts') {
											attachment.ts = optionsAttachments[index][option] as string;
										} else if (option === 'messageLinks') {
											attachment.message_link = optionsAttachments[index][option] as string;
										} else if (option === 'thumbUrl') {
											attachment.thumb_url = optionsAttachments[index][option] as string;
										} else if (option === 'collapsed') {
											attachment.collapsed = optionsAttachments[index][option] as boolean;
										} else if (option === 'authorName') {
											attachment.author_name = optionsAttachments[index][option] as string;
										} else if (option === 'authorLink') {
											attachment.author_link = optionsAttachments[index][option] as string;
										} else if (option === 'authorIcon') {
											attachment.author_icon = optionsAttachments[index][option] as string;
										} else if (option === 'title') {
											attachment.title = optionsAttachments[index][option] as string;
										} else if (option === 'titleLink') {
											attachment.title_link = optionsAttachments[index][option] as string;
										} else if (option === 'titleLinkDownload') {
											attachment.title_link_download = optionsAttachments[index][option] as boolean;
										} else if (option === 'imageUrl') {
											attachment.image_url = optionsAttachments[index][option] as string;
										} else if (option === 'audioUrl') {
											attachment.audio_url = optionsAttachments[index][option] as string;
										} else if (option === 'videoUrl') {
											attachment.video_url = optionsAttachments[index][option] as string;
										} else if (option === 'fields') {
											const fieldsValues = (optionsAttachments[index][option] as IDataObject)
												.fieldsValues as IDataObject[];
											if (fieldsValues.length > 0) {
												const fields: IField[] = [];
												for (let j = 0; j < fieldsValues.length; j++) {
													const field: IField = {};
													for (const key of Object.keys(fieldsValues[j])) {
														if (key === 'short') {
															field.short = fieldsValues[j][key] as boolean;
														} else if (key === 'title') {
															field.title = fieldsValues[j][key] as string;
														} else if (key === 'value') {
															field.value = fieldsValues[j][key] as string;
														}
													}
													fields.push(field);
													attachment.fields = fields;
												}
											}
										}
									}
									attachments.push(attachment);
								}
								body.attachments = attachments;
							}
						} else {
							body.attachments = validateJSON(
								this.getNodeParameter('attachmentsJson', i) as string,
							);
						}

						responseData = await rocketchatApiRequest.call(
							this,
							'/chat',
							'POST',
							'postMessage',
							body,
						);
					}
				}
				const executionData = this.helpers.constructExecutionMetaData(
					this.helpers.returnJsonArray(responseData as IDataObject[]),
					{ itemData: { item: i } },
				);
				returnData.push(...executionData);
			} catch (error) {
				if (this.continueOnFail()) {
					const executionData = this.helpers.constructExecutionMetaData(
						this.helpers.returnJsonArray({ error: error.message }),
						{ itemData: { item: i } },
					);
					returnData.push(...executionData);
					continue;
				}
				throw error;
			}
		}

		return this.prepareOutputData(returnData);
	}
}


If node unit test

Code

return [
  { value: true, value2: false },
  { value: false, value2: true },
];


const jobid = this.getNodeParameter('jobid', i) as string;

const rundeckArguments = (this.getNodeParameter('arguments', i) as IDataObject)

const response = await rundeckApi.executeJob(jobid, rundeckArguments);

} else if (operation === 'getMetadata') {

keep keep keep keep replace keep keep keep keep keep

const filter = this.getNodeParameter('filter', i) as string;

const response = await rundeckApi.executeJob(jobid, rundeckArguments, filter);

import type { OptionsWithUri } from 'request';
import type { IDataObject, IExecuteFunctions, JsonObject } from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';

export interface RundeckCredentials {
	url: string;
	token: string;
}

export class RundeckApi {
	private credentials?: RundeckCredentials;

	private executeFunctions: IExecuteFunctions;

	constructor(executeFunctions: IExecuteFunctions) {
		this.executeFunctions = executeFunctions;
	}

	protected async request(method: string, endpoint: string, body: IDataObject, query: object) {
		const credentialType = 'rundeckApi';

		const options: OptionsWithUri = {
			rejectUnauthorized: false,
			method,
			qs: query,
			uri: (this.credentials?.url as string) + endpoint,
			body,
			json: true,
		};

		try {
			return await this.executeFunctions.helpers.requestWithAuthentication.call(
				this.executeFunctions,
				credentialType,
				options,
			);
		} catch (error) {
			throw new NodeApiError(this.executeFunctions.getNode(), error as JsonObject);
		}
	}

	async init() {
		const credentials = await this.executeFunctions.getCredentials('rundeckApi');

		if (credentials === undefined) {
			throw new NodeOperationError(this.executeFunctions.getNode(), 'No credentials got returned!');
		}

		this.credentials = credentials as unknown as RundeckCredentials;
	}

	async executeJob(jobId: string, args: IDataObject[]): Promise<IDataObject> {
		let params = '';

		if (args) {
			for (const arg of args) {
				params += '-' + (arg.name as string) + ' ' + (arg.value as string) + ' ';
			}
		}

		const body = {
			argString: params,
		};

		return this.request('POST', `/api/14/job/${jobId}/run`, body, {});
	}

	async getJobMetadata(jobId: string): Promise<IDataObject> {
		return this.request('GET', `/api/18/job/${jobId}/info`, {}, {});
	}
}


export interface IAttachment {
	ParentId?: string;
	Name?: string;
	OwnerId?: string;
	IsPrivate?: boolean;
	ContentType?: string;
	Description?: string;
	Body?: string;
}


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

export const dealOperations: INodeProperties[] = [
	{
		displayName: 'Operation',
		name: 'operation',
		type: 'options',
		noDataExpression: true,
		displayOptions: {
			show: {
				resource: ['deal'],
			},
		},
		options: [
			{
				name: 'Create',
				value: 'create',
				description: 'Create a deal',
				action: 'Create a deal',
			},
			{
				name: 'Delete',
				value: 'delete',
				description: 'Delete a deal',
				action: 'Delete a deal',
			},
			{
				name: 'Get',
				value: 'get',
				description: 'Retrieve a deal',
				action: 'Get a deal',
			},
			{
				name: 'Get Many',
				value: 'getAll',
				description: 'Retrieve many deals',
				action: 'Get many deals',
			},
			{
				name: 'Update',
				value: 'update',
				description: 'Update a deal',
				action: 'Update a deal',
			},
		],
		default: 'create',
	},
];

export const dealFields: INodeProperties[] = [
	// ----------------------------------------
	//               deal: create
	// ----------------------------------------
	{
		displayName: 'Amount',
		name: 'amount',
		description: 'Value of the deal',
		type: 'number',
		required: true,
		default: 0,
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['create'],
			},
		},
	},
	{
		displayName: 'Name',
		name: 'name',
		description: 'Name of the deal',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['create'],
			},
		},
	},
	{
		displayName: 'Additional Fields',
		name: 'additionalFields',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['create'],
			},
		},
		options: [
			{
				displayName: 'Base Currency Amount',
				name: 'base_currency_amount',
				type: 'number',
				default: 0,
				description: 'Value of the deal in base currency',
			},
			{
				displayName: 'Campaign Name or ID',
				name: 'campaign_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getCampaigns',
				},
				description:
					'ID of the campaign that landed this deal. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Currency Name or ID',
				name: 'currency_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getCurrencies',
				},
				description:
					'ID of the currency that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Payment Status Name or ID',
				name: 'deal_payment_status_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealPaymentStatuses',
				},
				description:
					'ID of the mode of payment for the deal. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Pipeline Name or ID',
				name: 'deal_pipeline_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealPipelines',
				},
				description:
					'ID of the deal pipeline that it belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Product Name or ID',
				name: 'deal_product_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealProducts',
				},
				description:
					'ID of the product that the deal belongs to (in a multi-product company). Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Reason Name or ID',
				name: 'deal_reason_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealReasons',
				},
				description:
					'ID of the reason for losing the deal. Can only be set if the deal is in \'Lost\' stage. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Stage Name or ID',
				name: 'deal_stage_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealStages',
				},
				description:
					'ID of the deal stage that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Type Name or ID',
				name: 'deal_type_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealTypes',
				},
				description:
					'ID of the deal type that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Lead Source ID',
				name: 'lead_source_id',
				type: 'string', // not obtainable from API
				default: '',
				description: 'ID of the source where deal came from',
			},
			{
				displayName: 'Owner Name or ID',
				name: 'owner_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getUsers',
				},
				description:
					'ID of the user to whom the deal is assigned. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Probability',
				name: 'probability',
				type: 'number',
				default: 0,
				typeOptions: {
					minValue: 0,
					maxValue: 100,
				},
				description: 'Probability of winning the deal as a number between 0 and 100',
			},
			{
				displayName: 'Sales Account Name or ID',
				name: 'sales_account_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getAccounts',
				},
				description:
					'ID of the account that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Territory Name or ID',
				name: 'territory_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getTerritories',
				},
				description:
					'ID of the territory that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
		],
	},

	// ----------------------------------------
	//               deal: delete
	// ----------------------------------------
	{
		displayName: 'Deal ID',
		name: 'dealId',
		description: 'ID of the deal to delete',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['delete'],
			},
		},
	},

	// ----------------------------------------
	//                deal: get
	// ----------------------------------------
	{
		displayName: 'Deal ID',
		name: 'dealId',
		description: 'ID of the deal to retrieve',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['get'],
			},
		},
	},

	// ----------------------------------------
	//               deal: getAll
	// ----------------------------------------
	{
		displayName: 'View Name or ID',
		name: 'view',
		type: 'options',
		description:
			'Choose from the list, or specify an ID using an expression',
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['getAll'],
			},
		},
		typeOptions: {
			loadOptionsMethod: 'getDealViews',
		},
		default: '',
	},
	{
		displayName: 'Return All',
		name: 'returnAll',
		type: 'boolean',
		default: false,
		description: 'Whether to return all results or only up to a given limit',
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['getAll'],
			},
		},
	},
	{
		displayName: 'Limit',
		name: 'limit',
		type: 'number',
		default: 50,
		description: 'Max number of results to return',
		typeOptions: {
			minValue: 1,
		},
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['getAll'],
				returnAll: [false],
			},
		},
	},

	// ----------------------------------------
	//               deal: update
	// ----------------------------------------
	{
		displayName: 'Deal ID',
		name: 'dealId',
		description: 'ID of the deal to update',
		type: 'string',
		required: true,
		default: '',
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['update'],
			},
		},
	},
	{
		displayName: 'Update Fields',
		name: 'updateFields',
		type: 'collection',
		placeholder: 'Add Field',
		default: {},
		displayOptions: {
			show: {
				resource: ['deal'],
				operation: ['update'],
			},
		},
		options: [
			{
				displayName: 'Amount',
				name: 'amount',
				type: 'number',
				default: 0,
				typeOptions: {
					minValue: 0,
				},
				description: 'Value of the deal',
			},
			{
				displayName: 'Base Currency Amount',
				name: 'base_currency_amount',
				type: 'number',
				default: 0,
				typeOptions: {
					minValue: 0,
				},
				description: 'Value of the deal in base currency',
			},
			{
				displayName: 'Campaign Name or ID',
				name: 'campaign_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getCampaigns',
				},
				description:
					'ID of the campaign that landed this deal. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Currency Name or ID',
				name: 'currency_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getCurrencies',
				},
				description:
					'ID of the currency that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Payment Status Name or ID',
				name: 'deal_payment_status_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealPaymentStatuses',
				},
				description:
					'ID of the mode of payment for the deal. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Pipeline Name or ID',
				name: 'deal_pipeline_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealPipelines',
				},
				description:
					'ID of the deal pipeline that it belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Product Name or ID',
				name: 'deal_product_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealProducts',
				},
				description:
					'ID of the product that the deal belongs to (in a multi-product company). Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Reason Name or ID',
				name: 'deal_reason_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealReasons',
				},
				description:
					'ID of the reason for losing the deal. Can only be set if the deal is in \'Lost\' stage. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Stage Name or ID',
				name: 'deal_stage_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealStages',
				},
				description:
					'ID of the deal stage that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Deal Type Name or ID',
				name: 'deal_type_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getDealTypes',
				},
				description:
					'ID of the deal type that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Lead Source ID',
				name: 'lead_source_id',
				type: 'string', // not obtainable from API
				default: '',
				description: 'ID of the source where deal came from',
			},
			{
				displayName: 'Name',
				name: 'name',
				type: 'string',
				default: '',
				description: 'Name of the deal',
			},
			{
				displayName: 'Owner Name or ID',
				name: 'owner_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getUsers',
				},
				description:
					'ID of the user to whom the deal is assigned. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Probability',
				name: 'probability',
				type: 'number',
				default: 0,
				typeOptions: {
					minValue: 0,
					maxValue: 100,
				},
				description: 'Probability of winning the deal as a number between 0 and 100',
			},
			{
				displayName: 'Sales Account Name or ID',
				name: 'sales_account_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getAccounts',
				},
				description:
					'ID of the account that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
			{
				displayName: 'Territory Name or ID',
				name: 'territory_id',
				type: 'options',
				default: '',
				typeOptions: {
					loadOptionsMethod: 'getTerritories',
				},
				description:
					'ID of the territory that the deal belongs to. Choose from the list, or specify an ID using an expression.',
			},
		],
	},
];


import { EventEmitter } from 'events';
import { Service } from 'typedi';
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import { LoggerProxy } from 'n8n-workflow';
import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { WorkflowStatisticsRepository } from '@db/repositories';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { UserService } from '@/user/user.service';

@Service()
export class EventsService extends EventEmitter {
	constructor(private repository: WorkflowStatisticsRepository) {
		super({ captureRejections: true });
		if ('SKIP_STATISTICS_EVENTS' in process.env) return;

		this.on('nodeFetchedData', async (workflowId, node) => this.nodeFetchedData(workflowId, node));
		this.on('workflowExecutionCompleted', async (workflowData, runData) =>
			this.workflowExecutionCompleted(workflowData, runData),
		);
	}

	async workflowExecutionCompleted(workflowData: IWorkflowBase, runData: IRun): Promise<void> {
		// Determine the name of the statistic
		const finished = runData.finished ? runData.finished : false;
		const manual = runData.mode === 'manual';
		let name: StatisticsNames;

		if (finished) {
			if (manual) name = StatisticsNames.manualSuccess;
			else name = StatisticsNames.productionSuccess;
		} else {
			if (manual) name = StatisticsNames.manualError;
			else name = StatisticsNames.productionError;
		}

		// Get the workflow id
		const workflowId = workflowData.id;
		if (!workflowId) return;

		try {
			const upsertResult = await this.repository.upsertWorkflowStatistics(name, workflowId);

			if (name === 'production_success' && upsertResult === 'insert') {
				const owner = await getWorkflowOwner(workflowId);
				const metrics = {
					user_id: owner.id,
					workflow_id: workflowId,
				};

				if (!owner.settings?.userActivated) {
					await UserService.updateUserSettings(owner.id, {
						firstSuccessfulWorkflowId: workflowId,
						userActivated: true,
					});
				}

				// Send the metrics
				this.emit('telemetry.onFirstProductionWorkflowSuccess', metrics);
			}
		} catch (error) {
			LoggerProxy.verbose('Unable to fire first workflow success telemetry event');
		}
	}

	async nodeFetchedData(workflowId: string | undefined | null, node: INode): Promise<void> {
		if (!workflowId) return;

		const insertResult = await this.repository.insertWorkflowStatistics(
			StatisticsNames.dataLoaded,
			workflowId,
		);
		if (insertResult === 'failed') return;

		// Compile the metrics since this was a new data loaded event
		const owner = await getWorkflowOwner(workflowId);

		let metrics = {
			user_id: owner.id,
			workflow_id: workflowId,
			node_type: node.type,
			node_id: node.id,
		};

		// This is probably naive but I can't see a way for a node to have multiple credentials attached so..
		if (node.credentials) {
			Object.entries(node.credentials).forEach(([credName, credDetails]) => {
				metrics = Object.assign(metrics, {
					credential_type: credName,
					credential_id: credDetails.id,
				});
			});
		}

		// Send metrics to posthog
		this.emit('telemetry.onFirstWorkflowDataLoad', metrics);
	}
}

export declare interface EventsService {
	on(
		event: 'nodeFetchedData',
		listener: (workflowId: string | undefined | null, node: INode) => void,
	): this;
	on(
		event: 'workflowExecutionCompleted',
		listener: (workflowData: IWorkflowBase, runData: IRun) => void,
	): this;
	on(
		event: 'telemetry.onFirstProductionWorkflowSuccess',
		listener: (metrics: { user_id: string; workflow_id: string }) => void,
	): this;
	on(
		event: 'telemetry.onFirstWorkflowDataLoad',
		listener: (metrics: {
			user_id: string;
			workflow_id: string;
			node_type: string;
			node_id: string;
			credential_type?: string;
			credential_id?: string;
		}) => void,
	): this;
}


this.credentials = credentials as unknown as RundeckCredentials;

async executeJob(jobId: string, args: IDataObject[]): Promise<IDataObject> {

params += '-' + (arg.name as string) + ' ' + (arg.value as string) + ' ';

keep keep keep keep keep replace keep keep keep keep keep

async executeJob(jobId: string, args: IDataObject[], filter?: string): Promise<IDataObject> {


import { t } from '../locale';

export default {
	methods: {
		t(...args: string[]) {
			return t.apply(this, args);
		},
	},
};


import {
	CORE_NODES_CATEGORY,
	WEBHOOK_NODE_TYPE,
	OTHER_TRIGGER_NODES_SUBCATEGORY,
	EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
	MANUAL_TRIGGER_NODE_TYPE,
	SCHEDULE_TRIGGER_NODE_TYPE,
	REGULAR_NODE_CREATOR_VIEW,
	TRANSFORM_DATA_SUBCATEGORY,
	FILES_SUBCATEGORY,
	FLOWS_CONTROL_SUBCATEGORY,
	HELPERS_SUBCATEGORY,
	TRIGGER_NODE_CREATOR_VIEW,
	EMAIL_IMAP_NODE_TYPE,
	DEFAULT_SUBCATEGORY,
} from '@/constants';

export function TriggerView($locale: any) {
	return {
		value: TRIGGER_NODE_CREATOR_VIEW,
		title: $locale.baseText('nodeCreator.triggerHelperPanel.selectATrigger'),
		subtitle: $locale.baseText('nodeCreator.triggerHelperPanel.selectATriggerDescription'),
		items: [
			{
				key: DEFAULT_SUBCATEGORY,
				type: 'subcategory',
				properties: {
					forceIncludeNodes: [WEBHOOK_NODE_TYPE, EMAIL_IMAP_NODE_TYPE],
					title: 'App Trigger Nodes',
					icon: 'satellite-dish',
				},
			},
			{
				key: SCHEDULE_TRIGGER_NODE_TYPE,
				type: 'node',
				category: [CORE_NODES_CATEGORY],
				properties: {
					group: [],
					name: SCHEDULE_TRIGGER_NODE_TYPE,
					displayName: $locale.baseText(
						'nodeCreator.triggerHelperPanel.scheduleTriggerDisplayName',
					),
					description: $locale.baseText(
						'nodeCreator.triggerHelperPanel.scheduleTriggerDescription',
					),
					icon: 'fa:clock',
				},
			},
			{
				key: WEBHOOK_NODE_TYPE,
				type: 'node',
				category: [CORE_NODES_CATEGORY],
				properties: {
					group: [],
					name: WEBHOOK_NODE_TYPE,
					displayName: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDisplayName'),
					description: $locale.baseText('nodeCreator.triggerHelperPanel.webhookTriggerDescription'),
					iconData: {
						type: 'file',
						icon: 'webhook',
						fileBuffer: '/static/webhook-icon.svg',
					},
				},
			},
			{
				key: MANUAL_TRIGGER_NODE_TYPE,
				type: 'node',
				category: [CORE_NODES_CATEGORY],
				properties: {
					group: [],
					name: MANUAL_TRIGGER_NODE_TYPE,
					displayName: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDisplayName'),
					description: $locale.baseText('nodeCreator.triggerHelperPanel.manualTriggerDescription'),
					icon: 'fa:mouse-pointer',
				},
			},
			{
				key: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
				type: 'node',
				category: [CORE_NODES_CATEGORY],
				properties: {
					group: [],
					name: EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
					displayName: $locale.baseText(
						'nodeCreator.triggerHelperPanel.workflowTriggerDisplayName',
					),
					description: $locale.baseText(
						'nodeCreator.triggerHelperPanel.workflowTriggerDescription',
					),
					icon: 'fa:sign-out-alt',
				},
			},
			{
				type: 'subcategory',
				key: OTHER_TRIGGER_NODES_SUBCATEGORY,
				category: CORE_NODES_CATEGORY,
				properties: {
					title: OTHER_TRIGGER_NODES_SUBCATEGORY,
					icon: 'folder-open',
				},
			},
		],
	};
}

export function RegularView($locale: any) {
	return {
		value: REGULAR_NODE_CREATOR_VIEW,
		title: $locale.baseText('nodeCreator.triggerHelperPanel.whatHappensNext'),
		items: [
			{
				key: DEFAULT_SUBCATEGORY,
				type: 'subcategory',
				properties: {
					title: 'App Regular Nodes',
					icon: 'globe',
				},
			},
			{
				type: 'subcategory',
				key: TRANSFORM_DATA_SUBCATEGORY,
				category: CORE_NODES_CATEGORY,
				properties: {
					title: TRANSFORM_DATA_SUBCATEGORY,
					icon: 'pen',
				},
			},
			{
				type: 'subcategory',
				key: HELPERS_SUBCATEGORY,
				category: CORE_NODES_CATEGORY,
				properties: {
					title: HELPERS_SUBCATEGORY,
					icon: 'toolbox',
				},
			},
			{
				type: 'subcategory',
				key: FLOWS_CONTROL_SUBCATEGORY,
				category: CORE_NODES_CATEGORY,
				properties: {
					title: FLOWS_CONTROL_SUBCATEGORY,
					icon: 'code-branch',
				},
			},
			{
				type: 'subcategory',
				key: FILES_SUBCATEGORY,
				category: CORE_NODES_CATEGORY,
				properties: {
					title: FILES_SUBCATEGORY,
					icon: 'file-alt',
				},
			},
			{
				key: TRIGGER_NODE_CREATOR_VIEW,
				type: 'view',
				properties: {
					title: $locale.baseText('nodeCreator.triggerHelperPanel.addAnotherTrigger'),
					icon: 'bolt',
					description: $locale.baseText(
						'nodeCreator.triggerHelperPanel.addAnotherTriggerDescription',
					),
				},
			},
		],
	};
}