keep keep keep keep keep replace keep keep keep

import type ldapjs from 'ldapjs';
import type { ILDAPEntry, IUser, IRoom, ICreatedRoom, IRole, IImportUser } from '@rocket.chat/core-typings';
import { Users as UsersRaw, Roles, Subscriptions as SubscriptionsRaw } from '@rocket.chat/models';

import type { ImporterAfterImportCallback } from '../../../../app/importer/server/definitions/IConversionCallbacks';
import { settings } from '../../../../app/settings/server';
import { Rooms } from '../../../../app/models/server';
import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter';
import { LDAPConnection } from '../../../../server/lib/ldap/Connection';
import { LDAPManager } from '../../../../server/lib/ldap/Manager';
import { logger, searchLogger, mapLogger } from '../../../../server/lib/ldap/Logger';
import { addUserToRoom, removeUserFromRoom, createRoom } from '../../../../app/lib/server/functions';
import { syncUserRoles } from '../syncUserRoles';
import { Team } from '../../../../server/sdk';
import { ensureArray } from '../../../../lib/utils/arrayUtils';
import { copyCustomFieldsLDAP } from './copyCustomFieldsLDAP';

export class LDAPEEManager extends LDAPManager {
	public static async sync(): Promise<void> {
		if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Background_Sync') !== true) {
			return;
		}

		const createNewUsers = settings.get<boolean>('LDAP_Background_Sync_Import_New_Users') ?? true;
		const updateExistingUsers = settings.get<boolean>('LDAP_Background_Sync_Keep_Existant_Users_Updated') ?? true;

		const options = this.getConverterOptions();
		options.skipExistingUsers = !updateExistingUsers;

		const ldap = new LDAPConnection();
		const converter = new LDAPDataConverter(true, options);

		try {
			await ldap.connect();

			if (createNewUsers) {
				await this.importNewUsers(ldap, converter);
			} else if (updateExistingUsers) {
				await this.updateExistingUsers(ldap, converter);
			}

			converter.convertUsers({
				afterImportFn: ((data: IImportUser, _type: string, isNewRecord: boolean): void =>
					Promise.await(this.advancedSync(ldap, data, converter, isNewRecord))) as ImporterAfterImportCallback,
			});
		} catch (error) {
			logger.error(error);
		}

		ldap.disconnect();
	}

	public static async syncAvatars(): Promise<void> {
		if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Background_Sync_Avatars') !== true) {
			return;
		}

		try {
			const ldap = new LDAPConnection();
			await ldap.connect();

			try {
				await this.updateUserAvatars(ldap);
			} finally {
				ldap.disconnect();
			}
		} catch (error) {
			logger.error(error);
		}
	}

	public static validateLDAPTeamsMappingChanges(json: string): void {
		if (!json) {
			return;
		}

		const mustBeAnArrayOfStrings = (array: Array<string>): boolean =>
			Boolean(Array.isArray(array) && array.length && array.every((item) => typeof item === 'string'));
		const mappedTeams = this.parseJson(json);
		if (!mappedTeams) {
			return;
		}

		const mappedRocketChatTeams = Object.values(mappedTeams);
		const validStructureMapping = mappedRocketChatTeams.every(mustBeAnArrayOfStrings);
		if (!validStructureMapping) {
			throw new Error(
				'Please verify your mapping for LDAP X RocketChat Teams. The structure is invalid, the structure should be an object like: {key: LdapTeam, value: [An array of rocket.chat teams]}',
			);
		}
	}

	public static async syncLogout(): Promise<void> {
		if (settings.get('LDAP_Enable') !== true || settings.get('LDAP_Sync_AutoLogout_Enabled') !== true) {
			return;
		}

		try {
			const ldap = new LDAPConnection();
			await ldap.connect();

			try {
				await this.logoutDeactivatedUsers(ldap);
			} finally {
				ldap.disconnect();
			}
		} catch (error) {
			logger.error(error);
		}
	}

	public static async advancedSyncForUser(ldap: LDAPConnection, user: IUser, isNewRecord: boolean, dn: string): Promise<void> {
		try {
			await this.syncUserRoles(ldap, user, dn);
			await this.syncUserChannels(ldap, user, dn);
			await this.syncUserTeams(ldap, user, dn, isNewRecord);
		} catch (e) {
			logger.debug(`Advanced Sync failed for user: ${dn}`);
			logger.error(e);
		}
	}

	private static async advancedSync(
		ldap: LDAPConnection,
		importUser: IImportUser,
		converter: LDAPDataConverter,
		isNewRecord: boolean,
	): Promise<void> {
		const user = converter.findExistingUser(importUser);
		if (!user?.username) {
			return;
		}

		const dn = importUser.importIds[0];
		return this.advancedSyncForUser(ldap, user, isNewRecord, dn);
	}

	private static async isUserInGroup(
		ldap: LDAPConnection,
		baseDN: string,
		filter: string,
		{ dn, username }: { dn: string; username: string },
		groupName: string,
	): Promise<boolean> {
		if (!filter || !baseDN) {
			logger.error('Please setup LDAP Group Filter and LDAP Group BaseDN in LDAP Settings.');
			return false;
		}
		const searchOptions: ldapjs.SearchOptions = {
			filter: filter
				.replace(/#{username}/g, username)
				.replace(/#{groupName}/g, groupName)
				.replace(/#{userdn}/g, dn.replace(/\\/g, '\\5c')),
			scope: 'sub',
		};

		const result = await ldap.searchRaw(baseDN, searchOptions);

		if (!Array.isArray(result) || result.length === 0) {
			logger.debug(`${username} is not in ${groupName} group!!!`);
		} else {
			logger.debug(`${username} is in ${groupName} group.`);
			return true;
		}

		return false;
	}

	private static parseJson(json: string): Record<string, any> | undefined {
		try {
			return JSON.parse(json);
		} catch (err) {
			logger.error({ msg: 'Unexpected error', err });
		}
	}

	private static async syncUserRoles(ldap: LDAPConnection, user: IUser, dn: string): Promise<void> {
		const { username } = user;
		if (!username) {
			logger.debug('User has no username');
			return;
		}

		const shouldSyncUserRoles = settings.get<boolean>('LDAP_Sync_User_Data_Roles') ?? false;
		const syncUserRolesAutoRemove = settings.get<boolean>('LDAP_Sync_User_Data_Roles_AutoRemove') ?? false;
		const syncUserRolesFieldMap = (settings.get<string>('LDAP_Sync_User_Data_RolesMap') ?? '').trim();
		const syncUserRolesFilter = (settings.get<string>('LDAP_Sync_User_Data_Roles_Filter') ?? '').trim();
		const syncUserRolesBaseDN = (settings.get<string>('LDAP_Sync_User_Data_Roles_BaseDN') ?? '').trim();

		if (!shouldSyncUserRoles || !syncUserRolesFieldMap) {
			logger.debug('not syncing user roles');
			return;
		}

		const roles = (await Roles.find(
			{},
			{
				projection: {
					_updatedAt: 0,
				},
			},
		).toArray()) as Array<IRole>;

		if (!roles) {
			return;
		}

		const fieldMap = this.parseJson(syncUserRolesFieldMap);
		if (!fieldMap) {
			logger.debug('missing group role mapping');
			return;
		}

		const ldapFields = Object.keys(fieldMap);
		const roleList: Array<IRole['_id']> = [];
		const allowedRoles: Array<IRole['_id']> = [];

		for await (const ldapField of ldapFields) {
			if (!fieldMap[ldapField]) {
				continue;
			}

			const userFields = ensureArray<string>(fieldMap[ldapField]);

			for await (const userField of userFields) {
				const [roleId] = userField.split(/\.(.+)/);
				allowedRoles.push(roleId);

				if (await this.isUserInGroup(ldap, syncUserRolesBaseDN, syncUserRolesFilter, { dn, username }, ldapField)) {
					roleList.push(roleId);
					continue;
				}
			}
		}

		await syncUserRoles(user._id, roleList, {
			allowedRoles,
			skipRemovingRoles: !syncUserRolesAutoRemove,
		});
	}

	private static createRoomForSync(channel: string): IRoom | undefined {
		logger.debug(`Channel '${channel}' doesn't exist, creating it.`);

		const roomOwner = settings.get<string>('LDAP_Sync_User_Data_Channels_Admin') || '';
		// #ToDo: Remove typecastings when createRoom is converted to ts.
		const room = createRoom('c', channel, roomOwner, [], false, {
			customFields: { ldap: true },
		} as any) as unknown as ICreatedRoom | undefined;
		if (!room?.rid) {
			logger.error(`Unable to auto-create channel '${channel}' during ldap sync.`);
			return;
		}

		room._id = room.rid;
		return room;
	}

	private static async syncUserChannels(ldap: LDAPConnection, user: IUser, dn: string): Promise<void> {
		const syncUserChannels = settings.get<boolean>('LDAP_Sync_User_Data_Channels') ?? false;
		const syncUserChannelsRemove = settings.get<boolean>('LDAP_Sync_User_Data_Channels_Enforce_AutoChannels') ?? false;
		const syncUserChannelsFieldMap = (settings.get<string>('LDAP_Sync_User_Data_ChannelsMap') ?? '').trim();
		const syncUserChannelsFilter = (settings.get<string>('LDAP_Sync_User_Data_Channels_Filter') ?? '').trim();
		const syncUserChannelsBaseDN = (settings.get<string>('LDAP_Sync_User_Data_Channels_BaseDN') ?? '').trim();

		if (!syncUserChannels || !syncUserChannelsFieldMap) {
			logger.debug('not syncing groups to channels');
			return;
		}

		const fieldMap = this.parseJson(syncUserChannelsFieldMap);
		if (!fieldMap) {
			logger.debug('missing group channel mapping');
			return;
		}

		const { username } = user;
		if (!username) {
			return;
		}

		logger.debug('syncing user channels');
		const ldapFields = Object.keys(fieldMap);
		const channelsToAdd = new Set<string>();
		const channelsToRemove = new Set<string>();

		for await (const ldapField of ldapFields) {
			if (!fieldMap[ldapField]) {
				continue;
			}

			const isUserInGroup = await this.isUserInGroup(ldap, syncUserChannelsBaseDN, syncUserChannelsFilter, { dn, username }, ldapField);

			const channels: Array<string> = [].concat(fieldMap[ldapField]);
			for await (const channel of channels) {
				try {
					const room: IRoom | undefined = Rooms.findOneByNonValidatedName(channel) || this.createRoomForSync(channel);
					if (!room) {
						return;
					}

					if (isUserInGroup) {
						if (room.teamMain) {
							logger.error(`Can't add user to channel ${channel} because it is a team.`);
						} else {
							channelsToAdd.add(room._id);
						}
					} else if (syncUserChannelsRemove && !room.teamMain) {
						channelsToRemove.add(room._id);
					}
				} catch (e) {
					logger.debug(`Failed to sync user room, user = ${username}, channel = ${channel}`);
					logger.error(e);
				}
			}
		}

		for (const rid of channelsToAdd) {
			addUserToRoom(rid, user);
			logger.debug(`Synced user channel ${rid} from LDAP for ${username}`);
		}

		for await (const rid of channelsToRemove) {
			if (channelsToAdd.has(rid)) {
				return;
			}

			const subscription = await SubscriptionsRaw.findOneByRoomIdAndUserId(rid, user._id);
			if (subscription) {
				await removeUserFromRoom(rid, user);
				logger.debug(`Removed user ${username} from channel ${rid}`);
			}
		}
	}

	private static async syncUserTeams(ldap: LDAPConnection, user: IUser, dn: string, isNewRecord: boolean): Promise<void> {
		if (!user.username) {
			return;
		}

		const mapTeams =
			settings.get<boolean>('LDAP_Enable_LDAP_Groups_To_RC_Teams') &&
			(isNewRecord || settings.get<boolean>('LDAP_Validate_Teams_For_Each_Login'));
		if (!mapTeams) {
			return;
		}

		const ldapUserTeams = await this.getLdapTeamsByUsername(ldap, user.username, dn);
		const mapJson = settings.get<string>('LDAP_Groups_To_Rocket_Chat_Teams');
		if (!mapJson) {
			return;
		}
		const map = this.parseJson(mapJson) as Record<string, string>;
		if (!map) {
			return;
		}

		const teamNames = this.getRocketChatTeamsByLdapTeams(map, ldapUserTeams);

		const allTeamNames = [...new Set(Object.values(map).flat())];
		const allTeams = await Team.listByNames(allTeamNames, { projection: { _id: 1, name: 1 } });

		const inTeamIds = allTeams.filter(({ name }) => teamNames.includes(name)).map(({ _id }) => _id);
		const notInTeamIds = allTeams.filter(({ name }) => !teamNames.includes(name)).map(({ _id }) => _id);

		const currentTeams = await Team.listTeamsBySubscriberUserId(user._id, {
			projection: { teamId: 1 },
		});
		const currentTeamIds = currentTeams?.map(({ teamId }) => teamId);
		const teamsToRemove = currentTeamIds?.filter((teamId) => notInTeamIds.includes(teamId));
		const teamsToAdd = inTeamIds.filter((teamId) => !currentTeamIds?.includes(teamId));

		await Team.insertMemberOnTeams(user._id, teamsToAdd);
		if (teamsToRemove) {
			await Team.removeMemberFromTeams(user._id, teamsToRemove);
		}
	}

	private static getRocketChatTeamsByLdapTeams(mappedTeams: Record<string, string>, ldapUserTeams: Array<string>): Array<string> {
		const mappedLdapTeams = Object.keys(mappedTeams);
		const filteredTeams = ldapUserTeams.filter((ldapTeam) => mappedLdapTeams.includes(ldapTeam));

		if (filteredTeams.length < ldapUserTeams.length) {
			const unmappedLdapTeams = ldapUserTeams.filter((ldapTeam) => !mappedLdapTeams.includes(ldapTeam));
			logger.error(`The following LDAP teams are not mapped in Rocket.Chat: "${unmappedLdapTeams.join(', ')}".`);
		}

		if (!filteredTeams.length) {
			return [];
		}

		return [...new Set(filteredTeams.map((ldapTeam) => mappedTeams[ldapTeam]).flat())];
	}

	private static async getLdapTeamsByUsername(ldap: LDAPConnection, username: string, dn: string): Promise<Array<string>> {
		const baseDN = (settings.get<string>('LDAP_Teams_BaseDN') ?? '').trim() || ldap.options.baseDN;
		const query = settings.get<string>('LDAP_Query_To_Get_User_Teams');
		if (!query) {
			return [];
		}

		const searchOptions = {
			filter: query.replace(/#{username}/g, username).replace(/#{userdn}/g, dn.replace(/\\/g, '\\5c')),
			scope: ldap.options.userSearchScope || 'sub',
			sizeLimit: ldap.options.searchSizeLimit,
		};

		const attributeNames = (settings.get<string>('LDAP_Teams_Name_Field') ?? '').split(',').map((attributeName) => attributeName.trim());
		if (!attributeNames.length) {
			attributeNames.push('ou');
		}

		const ldapUserGroups = await ldap.searchRaw(baseDN, searchOptions);
		if (!Array.isArray(ldapUserGroups)) {
			return [];
		}

		return ldapUserGroups
			.map((entry) => {
				if (!entry?.raw) {
					return undefined;
				}

				for (const attributeName of attributeNames) {
					if (entry.raw[attributeName]) {
						return ldap.extractLdapAttribute(entry.raw[attributeName]) as string;
					}
				}

				return undefined;
			})
			.filter((entry): entry is string => Boolean(entry))
			.flat();
	}

	private static isUserDeactivated(ldapUser: ILDAPEntry): boolean {
		// Account locked by "Draft-behera-ldap-password-policy"
		if (ldapUser.pwdAccountLockedTime) {
			mapLogger.debug('User account is locked by password policy (attribute pwdAccountLockedTime)');
			return true;
		}

		// EDirectory: Account manually disabled by an admin
		if (ldapUser.loginDisabled) {
			mapLogger.debug('User account was manually disabled by an admin (attribute loginDisabled)');
			return true;
		}

		// Oracle: Account must not be allowed to authenticate
		if (ldapUser.orclIsEnabled && ldapUser.orclIsEnabled !== 'ENABLED') {
			mapLogger.debug('User must not be allowed to authenticate (attribute orclIsEnabled)');
			return true;
		}

		// Active Directory - Account locked automatically by security policies
		if (ldapUser.lockoutTime && ldapUser.lockoutTime !== '0') {
			const lockoutTimeValue = Number(ldapUser.lockoutTime);
			if (lockoutTimeValue && !isNaN(lockoutTimeValue)) {
				// Automatic unlock is disabled
				if (!ldapUser.lockoutDuration) {
					mapLogger.debug('User account locked indefinitely by security policy (attribute lockoutTime)');
					return true;
				}

				const lockoutTime = new Date(lockoutTimeValue);
				lockoutTime.setMinutes(lockoutTime.getMinutes() + Number(ldapUser.lockoutDuration));
				// Account has not unlocked itself yet
				if (lockoutTime.valueOf() > Date.now()) {
					mapLogger.debug('User account locked temporarily by security policy (attribute lockoutTime)');
					return true;
				}
			}
		}

		// Active Directory - Account disabled by an Admin
		if (ldapUser.userAccountControl && (ldapUser.userAccountControl & 2) === 2) {
			mapLogger.debug('User account disabled by an admin (attribute userAccountControl)');
			return true;
		}

		return false;
	}

	public static copyActiveState(ldapUser: ILDAPEntry, userData: IImportUser): void {
		if (!ldapUser) {
			return;
		}

		const syncUserState = settings.get('LDAP_Sync_User_Active_State');
		if (syncUserState === 'none') {
			return;
		}

		const deleted = this.isUserDeactivated(ldapUser);
		if (deleted === userData.deleted) {
			return;
		}

		if (syncUserState === 'disable' && !deleted) {
			return;
		}

		userData.deleted = deleted;
		logger.info(`${deleted ? 'Deactivating' : 'Activating'} user ${userData.name} (${userData.username})`);
	}

	public static copyCustomFields(ldapUser: ILDAPEntry, userData: IImportUser): void {
		return copyCustomFieldsLDAP(
			{
				ldapUser,
				userData,
				customFieldsSettings: settings.get<string>('Accounts_CustomFields'),
				customFieldsMap: settings.get<string>('LDAP_CustomFieldMap'),
				syncCustomFields: settings.get<boolean>('LDAP_Sync_Custom_Fields'),
			},
			logger,
		);
	}

	private static async importNewUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise<void> {
		return new Promise((resolve, reject) => {
			let count = 0;

			ldap.searchAllUsers<IImportUser>({
				entryCallback: (entry: ldapjs.SearchEntry): IImportUser | undefined => {
					const data = ldap.extractLdapEntryData(entry);
					count++;

					const userData = this.mapUserData(data);
					converter.addUser(userData);
					return userData;
				},
				endCallback: (error: any): void => {
					if (error) {
						logger.error(error);
						reject(error);
						return;
					}

					logger.info('LDAP finished loading users. Users added to importer: ', count);
					resolve();
				},
			});
		});
	}

	private static async updateExistingUsers(ldap: LDAPConnection, converter: LDAPDataConverter): Promise<void> {
		const users = await UsersRaw.findLDAPUsers().toArray();
		for await (const user of users) {
			const ldapUser = await this.findLDAPUser(ldap, user);

			if (ldapUser) {
				const userData = this.mapUserData(ldapUser, user.username);
				converter.addUser(userData);
			}
		}
	}

	private static async updateUserAvatars(ldap: LDAPConnection): Promise<void> {
		const users = await UsersRaw.findLDAPUsers().toArray();
		for await (const user of users) {
			const ldapUser = await this.findLDAPUser(ldap, user);
			if (!ldapUser) {
				continue;
			}

			LDAPManager.syncUserAvatar(user, ldapUser);
		}
	}

	private static async findLDAPUser(ldap: LDAPConnection, user: IUser): Promise<ILDAPEntry | undefined> {
		if (user.services?.ldap?.id) {
			return ldap.findOneById(user.services.ldap.id, user.services.ldap.idAttribute);
		}

		if (user.username) {
			return ldap.findOneByUsername(user.username);
		}

		searchLogger.debug({
			msg: 'existing LDAP user not found during Sync',
			ldapId: user.services?.ldap?.id,
			ldapAttribute: user.services?.ldap?.idAttribute,
			username: user.username,
		});
	}

	private static async logoutDeactivatedUsers(ldap: LDAPConnection): Promise<void> {
		const users = await UsersRaw.findConnectedLDAPUsers().toArray();

		for await (const user of users) {
			const ldapUser = await this.findLDAPUser(ldap, user);
			if (!ldapUser) {
				continue;
			}

			if (this.isUserDeactivated(ldapUser)) {
				UsersRaw.unsetLoginTokens(user._id);
			}
		}
	}
}


import type { JSONSchemaType } from 'ajv';
import Ajv from 'ajv';

const ajv = new Ajv();

export type VideoConfInfoProps = { callId: string };

const videoConfInfoPropsSchema: JSONSchemaType<VideoConfInfoProps> = {
	type: 'object',
	properties: {
		callId: {
			type: 'string',
			nullable: false,
		},
	},
	required: ['callId'],
	additionalProperties: false,
};

export const isVideoConfInfoProps = ajv.compile(videoConfInfoPropsSchema);


import { Box, Skeleton } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React, { useMemo } from 'react';

import { AsyncStatePhase } from '../../../../hooks/useAsyncState';
import { useEndpointData } from '../../../../hooks/useEndpointData';
import EditOutgoingWebhook from './EditOutgoingWebhook';

function EditOutgoingWebhookWithData({ integrationId, ...props }) {
	const t = useTranslation();

	const params = useMemo(() => ({ integrationId }), [integrationId]);
	const { value: data, phase: state, error, reload } = useEndpointData('/v1/integrations.get', params);

	const onChange = () => {
		reload();
	};

	if (state === AsyncStatePhase.LOADING) {
		return (
			<Box w='full' pb='x24' {...props}>
				<Skeleton mbe='x4' />
				<Skeleton mbe='x8' />
				<Skeleton mbe='x4' />
				<Skeleton mbe='x8' />
				<Skeleton mbe='x4' />
				<Skeleton mbe='x8' />
			</Box>
		);
	}

	if (error) {
		return (
			<Box mbs='x16' {...props}>
				{t('Oops_page_not_found')}
			</Box>
		);
	}

	return <EditOutgoingWebhook data={data.integration} onChange={onChange} {...props} />;
}

export default EditOutgoingWebhookWithData;


import { Meteor } from 'meteor/meteor';
import { Match } from 'meteor/check';
import { Blaze } from 'meteor/blaze';
import { Deps } from 'meteor/deps';
import _ from 'underscore';
import { getCaretCoordinates } from 'meteor/dandv:caret-position';

import AutoCompleteRecords from './collection';
import { APIClient } from '../../utils/client';

const isServerSearch = function (rule) {
	return _.isString(rule.collection);
};

const validateRule = function (rule) {
	if (rule.subscription != null && !Match.test(rule.collection, String)) {
		throw new Error('Collection name must be specified as string for server-side search');
	}
	// XXX back-compat message, to be removed
	if (rule.callback) {
		console.warn('autocomplete no longer supports callbacks; use event listeners instead.');
	}
};

const isWholeField = function (rule) {
	// either '' or null both count as whole field.
	return !rule.token;
};

const getRegExp = function (rule) {
	if (!isWholeField(rule)) {
		// Expressions for the range from the last word break to the current cursor position
		return new RegExp(`(^|\\b|\\s)${rule.token}([\\w.]*)$`);
	}
	// Whole-field behavior - word characters or spaces
	return new RegExp('(^)(.*)$');
};

const getFindParams = function (rule, filter, limit) {
	// This is a different 'filter' - the selector from the settings
	// We need to extend so that we don't copy over rule.filter
	const selector = _.extend({}, rule.filter || {});
	const options = {
		limit,
	};
	if (!filter) {
		// Match anything, no sort, limit X
		return [selector, options];
	}
	if (rule.sort && rule.field) {
		const sortspec = {};
		// Only sort if there is a filter, for faster performance on a match of anything
		sortspec[rule.field] = 1;
		options.sort = sortspec;
	}
	if (_.isFunction(rule.selector)) {
		// Custom selector
		_.extend(selector, rule.selector(filter));
	} else {
		selector[rule.field] = {
			$regex: rule.matchAll ? filter : `^${filter}`,
			// default is case insensitive search - empty string is not the same as undefined!
			$options: typeof rule.options === 'undefined' ? 'i' : rule.options,
		};
	}
	return [selector, options];
};

const getField = function (obj, str) {
	const string = str.split('.');
	string.forEach((key) => {
		obj = obj[key];
	});
	return obj;
};

export default class AutoComplete {
	constructor(settings) {
		this.KEYS = [40, 38, 13, 27, 9];
		this.limit = settings.limit || 5;
		this.position = settings.position || 'bottom';
		this.rules = settings.rules;
		this.selector = {
			container: '.-autocomplete-container',
			item: '.-autocomplete-item',
			...settings.selector,
		};
		const { rules } = this;

		Object.keys(rules).forEach((key) => {
			const rule = rules[key];
			validateRule(rule);
		});

		this.onSelect = settings.onSelect;

		this.expressions = (() =>
			Object.keys(rules).map((key) => {
				const rule = rules[key];
				return getRegExp(rule);
			}))();
		this.matched = -1;
		this.loaded = true;

		// Reactive dependencies for current matching rule and filter
		this.ruleDep = new Deps.Dependency();
		this.filterDep = new Deps.Dependency();
		this.loadingDep = new Deps.Dependency();

		// Autosubscribe to the record set published by the server based on the filter
		// This will tear down server subscriptions when they are no longer being used.
		this.sub = null;
		this.comp = Deps.autorun(async () => {
			const rule = this.matchedRule();
			const filter = this.getFilter();
			if (this.sub) {
				// Stop any existing sub immediately, don't wait
				this.sub.stop();
			}
			if (!(rule && filter)) {
				return;
			}

			// subscribe only for server-side collections
			if (!isServerSearch(rule)) {
				this.setLoaded(true);
				return;
			}
			const [selector] = getFindParams(rule, filter, this.limit);

			// console.debug 'Subscribing to <%s> in <%s>.<%s>', filter, rule.collection, rule.field
			this.setLoaded(false);
			const endpointName = rule.endpoint || 'users.autocomplete';
			const { items } = await APIClient.get(`/v1/${endpointName}`, { selector: JSON.stringify(selector) });
			AutoCompleteRecords.remove({});
			items.forEach((item) => AutoCompleteRecords.insert(item));
			this.setLoaded(true);
		});
	}

	teardown() {
		// Stop the reactive computation we started for this autocomplete instance
		this.comp.stop();
	}

	matchedRule() {
		// reactive getters and setters for @filter and the currently matched rule
		this.ruleDep.depend();
		if (this.matched >= 0) {
			return this.rules[this.matched];
		}
		return null;
	}

	setMatchedRule(i) {
		this.matched = i;
		this.ruleDep.changed();
	}

	getFilter() {
		this.filterDep.depend();
		return this.filter;
	}

	setFilter(x) {
		this.filter = x;
		this.filterDep.changed();
		return this.filter;
	}

	isLoaded() {
		this.loadingDep.depend();
		return this.loaded;
	}

	setLoaded(val) {
		if (val === this.loaded) {
			return; // Don't cause redraws unnecessarily
		}
		this.loaded = val;
		this.loadingDep.changed();
	}

	onKeyUp() {
		if (!this.$element) {
			return; // Don't try to do this while loading
		}

		if (this._timeoutHandler) {
			clearTimeout(this._timeoutHandler);
		}

		this._timeoutHandler = setTimeout(() => {
			this._timeoutHandler = 0;

			const startpos = this.element.selectionStart;
			const val = this.getText().substring(0, startpos);

			/*
				Matching on multiple expressions.
				We always go from a matched state to an unmatched one
				before going to a different matched one.
			 */
			let i = 0;
			let breakLoop = false;
			while (i < this.expressions.length) {
				const matches = val.match(this.expressions[i]);

				// matching -> not matching
				if (!matches && this.matched === i) {
					this.setMatchedRule(-1);
					breakLoop = true;
				}

				// not matching -> matching
				if (matches && this.matched === -1) {
					this.setMatchedRule(i);
					breakLoop = true;
				}

				// Did filter change?
				if (matches && this.filter !== matches[2]) {
					this.setFilter(matches[2]);
					breakLoop = true;
				}
				if (breakLoop) {
					break;
				}
				i++;
			}
		}, 300);
	}

	onKeyDown(e) {
		if (this.matched === -1 || this.KEYS.indexOf(e.keyCode) < 0) {
			return;
		}
		switch (e.keyCode) {
			case 9: // TAB
			case 13: // ENTER
				if (this.select()) {
					// Don't jump fields or submit if select successful
					e.preventDefault();
					e.stopPropagation();
				}
				break;
			// preventDefault needed below to avoid moving cursor when selecting
			case 40: // DOWN
				e.preventDefault();
				this.next();
				break;
			case 38: // UP
				e.preventDefault();
				this.prev();
				break;
			case 27: // ESCAPE
				this.$element.blur();
				this.hideList();
		}
	}

	onFocus() {
		// We need to run onKeyUp after the focus resolves,
		// or the caret position (selectionStart) will not be correct
		Meteor.defer(() => this.onKeyUp());
	}

	onBlur() {
		// We need to delay this so click events work
		// TODO this is a bit of a hack, see if we can't be smarter
		Meteor.setTimeout(() => {
			this.hideList();
		}, 500);
	}

	onItemClick(doc) {
		this.processSelection(doc, this.rules[this.matched]);
	}

	onItemHover(doc, e) {
		this.tmplInst.$(this.selector.item).removeClass('selected');
		$(e.target).closest(this.selector.item).addClass('selected');
	}

	filteredList() {
		// @ruleDep.depend() # optional as long as we use depend on filter, because list will always get re-rendered
		const filter = this.getFilter(); // Reactively depend on the filter
		if (this.matched === -1) {
			return null;
		}
		const rule = this.rules[this.matched];

		// Don't display list unless we have a token or a filter (or both)
		// Single field: nothing displayed until something is typed
		if (!(rule.token || filter)) {
			return null;
		}
		const params = getFindParams(rule, filter, this.limit);
		const selector = params[0];
		const options = params[1];
		Meteor.defer(() => this.ensureSelection());

		// if server collection, the server has already done the filtering work
		if (isServerSearch(rule)) {
			return AutoCompleteRecords.find({}, options);
		}
		// Otherwise, search on client
		return rule.collection.find(selector, options);
	}

	isShowing() {
		const rule = this.matchedRule();
		// Same rules as above
		const showing = rule && (rule.token || this.getFilter());

		// Do this after the render
		if (showing) {
			Meteor.defer(() => {
				this.positionContainer();
				this.ensureSelection();
			});
		}
		return showing;
	}

	// Replace text with currently selected item
	select() {
		const node = this.tmplInst.find(`${this.selector.item}.selected`);
		if (node == null) {
			return false;
		}
		const doc = Blaze.getData(node);
		if (!doc) {
			return false; // Don't select if nothing matched
		}
		this.processSelection(doc, this.rules[this.matched]);
		return true;
	}

	processSelection(doc, rule) {
		const replacement = getField(doc, rule.field);
		if (!isWholeField(rule)) {
			this.replace(replacement, rule);
			this.hideList();
		} else {
			// Empty string or doesn't exist?
			// Single-field replacement: replace whole field
			this.setText(replacement);

			// Field retains focus, but list is hidden unless another key is pressed
			// Must be deferred or onKeyUp will trigger and match again
			// TODO this is a hack; see above
			this.onBlur();
		}
		this.onSelect && this.onSelect(doc);
		this.$element.trigger('autocompleteselect', doc);
	}

	// Replace the appropriate region
	replace(replacement) {
		const startpos = this.element.selectionStart;
		const fullStuff = this.getText();
		let val = fullStuff.substring(0, startpos);
		val = val.replace(this.expressions[this.matched], `$1${this.rules[this.matched].token}${replacement}`);
		const posfix = fullStuff.substring(startpos, fullStuff.length);
		const separator = posfix.match(/^\s/) ? '' : ' ';
		const finalFight = val + separator + posfix;
		this.setText(finalFight);
		const newPosition = val.length + 1;
		this.element.setSelectionRange(newPosition, newPosition);
	}

	hideList() {
		this.setMatchedRule(-1);
		this.setFilter(null);
	}

	getText() {
		return this.$element.val() || this.$element.text();
	}

	setText(text) {
		if (this.$element.is('input,textarea')) {
			return this.$element.val(text);
		}
		this.$element.html(text);
	}

	/*
		Rendering functions
	 */

	positionContainer() {
		// First render; Pick the first item and set css whenever list gets shown
		let pos = {};
		const element = this.selector.anchor ? this.tmplInst.$(this.selector.anchor) : this.$element;

		if (this.position === 'fixed') {
			const width = element.outerWidth();
			return this.tmplInst.$(this.selector.container).css({ width, position: 'fixed' });
		}

		const position = element.position();
		if (!position) {
			return;
		}

		const rule = this.matchedRule();
		const offset = getCaretCoordinates(this.element, this.element.selectionStart);

		// In whole-field positioning, we don't move the container and make it the
		// full width of the field.
		if (rule && isWholeField(rule)) {
			pos.left = position.left;
			if (rule.doNotChangeWidth !== false) {
				pos.width = element.outerWidth(); // position.offsetWidth
			}
		} else {
			// Normal positioning, at token word
			pos = { left: position.left + offset.left };
		}

		// Position menu from top (above) or from bottom of caret (below, default)
		if (this.position === 'top') {
			pos.bottom = element.offsetParent().height() - position.top - offset.top;
		} else {
			pos.top = position.top + offset.top + parseInt(element.css('font-size'));
		}

		this.tmplInst.$(this.selector.container).css({ ...pos, position: 'absolute' });
	}

	ensureSelection() {
		// Re-render; make sure selected item is something in the list or none if list empty
		const selectedItem = this.tmplInst.$(`${this.selector.item}.selected`);
		if (!selectedItem.length) {
			// Select anything
			this.tmplInst.$(`${this.selector.item}:first-child`).addClass('selected');
		}
	}

	// Select next item in list
	next() {
		const currentItem = this.tmplInst.$(`${this.selector.item}.selected`);
		if (!currentItem.length) {
			return this.tmplInst.$(`${this.selector.item}:first-child`).addClass('selected');
		}
		currentItem.removeClass('selected');
		const next = currentItem.next();
		if (next.length) {
			next.addClass('selected');
		} else {
			// End of list or lost selection; Go back to first item
			this.tmplInst.$(`${this.selector.item}:first-child`).addClass('selected');
		}
	}

	// Select previous item in list
	prev() {
		const currentItem = this.tmplInst.$(`${this.selector.item}.selected`);
		if (!currentItem.length) {
			return; // Don't try to iterate an empty list
		}
		currentItem.removeClass('selected');
		const prev = currentItem.prev();
		if (prev.length) {
			prev.addClass('selected');
		} else {
			// Beginning of list or lost selection; Go to end of list
			this.tmplInst.$(`${this.selector.item}:last-child`).addClass('selected');
		}
	}

	// This doesn't need to be reactive because list already changes reactively
	// and will cause all of the items to re-render anyway
	currentTemplate() {
		return this.rules[this.matched].template;
	}
}


const userFields = ensureArray<string>(fieldMap[ldapField]);

for await (const userField of userFields) {

const [roleId] = userField.split(/\.(.+)/);

if (await this.isUserInGroup(ldap, syncUserRolesBaseDN, syncUserRolesFilter, { dn, username }, ldapField)) {

keep keep keep keep keep replace replace keep keep

const [roleIdOrName] = userField.split(/\.(.+)/);

const role = roles.find((role) => role._id === roleIdOrName) ?? roles.find((role) => role.name === roleIdOrName);

import type { IRole, IRoom, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { IRolesModel } from '@rocket.chat/model-typings';
import { Subscriptions, Users } from '@rocket.chat/models';
import type { Collection, FindCursor, Db, Filter, FindOptions, InsertOneResult, UpdateResult, WithId } from 'mongodb';

import { BaseRaw } from './BaseRaw';

export class RolesRaw extends BaseRaw<IRole> implements IRolesModel {
	constructor(db: Db, trash?: Collection<RocketChatRecordDeleted<IRole>>) {
		super(db, 'roles', trash);
	}

	findByUpdatedDate(updatedAfterDate: Date, options?: FindOptions<IRole>): FindCursor<IRole> {
		const query = {
			_updatedAt: { $gte: new Date(updatedAfterDate) },
		};

		return options ? this.find(query, options) : this.find(query);
	}

	async addUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise<boolean> {
		if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
			throw new Error('Roles.addUserRoles method received a role scope instead of a scope value.');
		}

		if (!Array.isArray(roles)) {
			roles = [roles];
			process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.addUserRoles: roles should be an array');
		}

		for await (const roleId of roles) {
			const role = await this.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });

			if (!role) {
				process.env.NODE_ENV === 'development' && console.warn(`[WARN] RolesRaw.addUserRoles: role: ${roleId} not found`);
				continue;
			}
			switch (role.scope) {
				case 'Subscriptions':
					// TODO remove dependency from other models - this logic should be inside a function/service
					await Subscriptions.addRolesByUserId(userId, [role._id], scope);
					break;
				case 'Users':
				default:
					await Users.addRolesByUserId(userId, [role._id]);
			}
		}
		return true;
	}

	async isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise<boolean> {
		if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
			throw new Error('Roles.isUserInRoles method received a role scope instead of a scope value.');
		}

		for await (const roleId of roles) {
			const role = await this.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });

			if (!role) {
				continue;
			}

			switch (role.scope) {
				case 'Subscriptions':
					if (await Subscriptions.isUserInRole(userId, roleId, scope)) {
						return true;
					}
					break;
				case 'Users':
				default:
					if (await Users.isUserInRole(userId, roleId)) {
						return true;
					}
			}
		}
		return false;
	}

	async removeUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: IRoom['_id']): Promise<boolean> {
		if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
			throw new Error('Roles.removeUserRoles method received a role scope instead of a scope value.');
		}

		for await (const roleId of roles) {
			const role = await this.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });

			if (!role) {
				continue;
			}

			switch (role.scope) {
				case 'Subscriptions':
					scope && (await Subscriptions.removeRolesByUserId(userId, [roleId], scope));
					break;
				case 'Users':
				default:
					await Users.removeRolesByUserId(userId, [roleId]);
			}
		}
		return true;
	}

	async findOneByIdOrName(_idOrName: IRole['_id'] | IRole['name'], options?: undefined): Promise<IRole | null>;

	async findOneByIdOrName(_idOrName: IRole['_id'] | IRole['name'], options: FindOptions<IRole>): Promise<IRole | null>;

	async findOneByIdOrName<P>(_idOrName: IRole['_id'] | IRole['name'], options: FindOptions<P extends IRole ? IRole : P>): Promise<P | null>;

	findOneByIdOrName<P>(_idOrName: IRole['_id'] | IRole['name'], options?: any): Promise<IRole | P | null> {
		const query: Filter<IRole> = {
			$or: [
				{
					_id: _idOrName,
				},
				{
					name: _idOrName,
				},
			],
		};

		return this.findOne(query, options);
	}

	async findOneByName<P = IRole>(name: IRole['name'], options?: any): Promise<IRole | P | null> {
		const query: Filter<IRole> = {
			name,
		};

		return this.findOne(query, options);
	}

	findInIds<P>(ids: IRole['_id'][], options?: FindOptions<IRole>): P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole> {
		const query: Filter<IRole> = {
			name: {
				$in: ids,
			},
		};

		return this.find(query, options || {}) as P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole>;
	}

	findAllExceptIds<P>(ids: IRole['_id'][], options?: FindOptions<IRole>): P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole> {
		const query: Filter<IRole> = {
			_id: {
				$nin: ids,
			},
		};

		return this.find(query, options || {}) as P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole>;
	}

	findByScope(scope: IRole['scope'], options?: FindOptions<IRole>): FindCursor<IRole> {
		const query = {
			scope,
		};

		return this.find(query, options || {});
	}

	updateById(
		_id: IRole['_id'],
		name: IRole['name'],
		scope: IRole['scope'],
		description: IRole['description'] = '',
		mandatory2fa: IRole['mandatory2fa'] = false,
	): Promise<UpdateResult> {
		const queryData = {
			name,
			scope,
			description,
			mandatory2fa,
		};

		return this.updateOne({ _id }, { $set: queryData }, { upsert: true });
	}

	findUsersInRole(roleId: IRole['_id'], scope?: IRoom['_id']): Promise<FindCursor<IUser>>;

	findUsersInRole(roleId: IRole['_id'], scope: IRoom['_id'] | undefined, options: FindOptions<IUser>): Promise<FindCursor<IUser>>;

	findUsersInRole<P>(
		roleId: IRole['_id'],
		scope: IRoom['_id'] | undefined,
		options: FindOptions<P extends IUser ? IUser : P>,
	): Promise<FindCursor<P extends IUser ? IUser : P>>;

	/** @deprecated function getUsersInRole should be used instead */
	async findUsersInRole<P>(
		roleId: IRole['_id'],
		scope: IRoom['_id'] | undefined,
		options?: any | undefined,
	): Promise<FindCursor<IUser | P>> {
		if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
			throw new Error('Roles.findUsersInRole method received a role scope instead of a scope value.');
		}

		const role = await this.findOneById<Pick<IRole, '_id' | 'scope'>>(roleId, { projection: { scope: 1 } });

		if (!role) {
			throw new Error('RolesRaw.findUsersInRole: role not found');
		}

		switch (role.scope) {
			case 'Subscriptions':
				return Subscriptions.findUsersInRoles([role._id], scope, options);
			case 'Users':
			default:
				return Users.findUsersInRoles([role._id], null, options);
		}
	}

	createWithRandomId(
		name: IRole['name'],
		scope: IRole['scope'] = 'Users',
		description = '',
		protectedRole = true,
		mandatory2fa = false,
	): Promise<InsertOneResult<WithId<IRole>>> {
		const role = {
			name,
			scope,
			description,
			protected: protectedRole,
			mandatory2fa,
		};

		return this.insertOne(role);
	}

	async canAddUserToRole(uid: IUser['_id'], roleId: IRole['_id'], scope?: IRoom['_id']): Promise<boolean> {
		if (process.env.NODE_ENV === 'development' && (scope === 'Users' || scope === 'Subscriptions')) {
			throw new Error('Roles.canAddUserToRole method received a role scope instead of a scope value.');
		}

		const role = await this.findOne({ _id: roleId }, { projection: { scope: 1 } });
		if (!role) {
			return false;
		}

		switch (role.scope) {
			case 'Subscriptions':
				return Subscriptions.isUserInRoleScope(uid, scope);
			case 'Users':
			default:
				return Users.isUserInRoleScope(uid);
		}
	}
}


import '../lib/rocketchat';
import './callbacks';


export { default } from './NotificationPreferencesWithData';


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

import GenericModal from '../../../../../components/GenericModal';
import RoomLinkList from '../../RoomLinkList';

export const StepTwo = ({ deletedRooms, keptRooms, onConfirm, onReturn, onCancel }) => {
	const t = useTranslation();

	return (
		<GenericModal
			variant='danger'
			icon='trash'
			title={t('Deleting')}
			onConfirm={() => onConfirm(Object.values(deletedRooms).map(({ _id }) => _id))}
			onCancel={onReturn}
			confirmText={t('Remove')}
			cancelText={t('Back')}
			onClose={onCancel}
		>

{t('Teams_delete_team')}

			{!!Object.values(deletedRooms).length && (
				<>

						{t('Teams_deleted_channels')} <RoomLinkList rooms={deletedRooms} />

				</>
			)}
			{!!Object.values(keptRooms).length && (
				<>

						{t('Teams_kept_channels')} <RoomLinkList rooms={keptRooms} />

				</>
			)}
		</GenericModal>
	);
};

export default StepTwo;


import React from 'react';

export function mapProps(Component) {
	const WrappedComponent = ({ msg, username, tcount, ts, ...props }) => (
		<Component replies={tcount} username={username} msg={msg} ts={ts} {...props} />
	);

	WrappedComponent.displayName = `mapProps(${Component.displayName ?? Component.name ?? 'Component'})`;

	return WrappedComponent;
}


@use 'sass:math';

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

$button-group-margin: math.div($default-gap, 4);

.button-group {
	display: flex;

	margin: -$button-group-margin;
	align-items: center;
	flex-flow: row wrap;

	&__item {
		margin: $button-group-margin;
		flex-grow: 1;
	}
}


findInIds<P>(ids: IRole['_id'][], options?: FindOptions<IRole>): P extends Pick<IRole, '_id'> ? FindCursor<P> : FindCursor<IRole> {


import { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';

import VerticalBar from '../../../../components/VerticalBar';
import AutoTranslate from './AutoTranslate';

export default {
	title: 'Room/Contextual Bar/AutoTranslate',
	component: AutoTranslate,
	parameters: {
		layout: 'fullscreen',
	},
	decorators: [(fn) => <VerticalBar height='100vh'>{fn()}</VerticalBar>],
} as ComponentMeta<typeof AutoTranslate>;

export const Default: ComponentStory<typeof AutoTranslate> = (args) => <AutoTranslate {...args} />;
Default.storyName = 'AutoTranslate';
Default.args = {
	language: 'en',
	languages: [
		['en', 'English'],
		['jp', 'Japanese'],
		['pt', 'Portuguese'],
	],
};


export type FileProp = {
	_id: string;
	name: string;
	type: string;
	format: string;
	size: number;
};


keep keep keep replace replace keep keep keep

// Components
import VLazy from '../VLazy'

// Utilities
import {
  mount,
  MountOptions,
  Wrapper,
} from '@vue/test-utils'

describe('VLazy.ts', () => {
  type Instance = InstanceType<typeof VLazy>
  let mountFunction: (options?: MountOptions<Instance>) => Wrapper<Instance>

  beforeEach(() => {
    mountFunction = (options = {}) => {
      return mount(VLazy, {
        ...options,
      })
    }
  })

  it('should conditionally render content', async () => {
    const wrapper = mountFunction({
      slots: {
        default: '
foobar
',
      },
    })

    expect(wrapper.html()).toMatchSnapshot()

    wrapper.setProps({ value: true })

    await wrapper.vm.$nextTick()

    expect(wrapper.html()).toMatchSnapshot()
  })

  it('should set a minimum height', () => {
    const wrapper = mountFunction({
      propsData: {
        minHeight: 200,
      },
    })

    expect(wrapper.element.style.minHeight).toBe('200px')
  })

  it('should activate the slot when element is intersected', () => {
    const wrapper = mountFunction()

    expect(wrapper.vm.isActive).toBeFalsy()

    wrapper.vm.onObserve([
      { isIntersecting: false },
      { isIntersecting: false },
      { isIntersecting: false },
    ] as IntersectionObserverEntry[])

    expect(wrapper.vm.isActive).toBeFalsy()

    wrapper.vm.onObserve([
      { isIntersecting: false },
      { isIntersecting: false },
      { isIntersecting: true },
    ] as IntersectionObserverEntry[])

    expect(wrapper.vm.isActive).toBeTruthy()

    wrapper.vm.onObserve([
      { isIntersecting: false },
      { isIntersecting: false },
      { isIntersecting: false },
    ] as IntersectionObserverEntry[])

    expect(wrapper.vm.isActive).toBeTruthy()
  })
})


`v-subheader` 컴포넌트는 리스트의 섹션을 분리하는데 사용됩니다.

서브헤더는 사용자가 보는 것에 대한 문맥을 추가할 수 있습니다.

서브헤더는 여러 다른 종류의 액션을 분리하는 것을 도울 수 있습니다..

서브헤더와 소셜 미디어 (Subheaders with social media)

서브헤더를 이용한 소셜미디어 인터렉션


// Mixins
import Toggleable from '../../mixins/toggleable'

// Directives
import intersect from '../../directives/intersect'

// Utilities
import mixins from '../../util/mixins'
import { convertToUnit, getSlot } from '../../util/helpers'

// Types
import { VNode } from 'vue'

export default mixins(Toggleable).extend({
  name: 'VLazy',

  directives: { intersect },

  props: {
    minHeight: [Number, String],
    options: {
      type: Object,
      // For more information on types, navigate to:
      // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
      default: () => ({
        root: undefined,
        rootMargin: undefined,
        threshold: undefined,
      }),
    },
    transition: {
      type: String,
      default: 'fade-transition',
    },
  },

  computed: {
    styles (): object {
      return {
        minHeight: parseInt(this.minHeight) ? convertToUnit(this.minHeight) : this.minHeight,
      }
    },
  },

  methods: {
    genContent () {
      if (!this.isActive) return undefined

      const slot = getSlot(this)

      /* istanbul ignore if */
      if (!this.transition) return slot

      return this.$createElement('transition', {
        props: { name: this.transition },
      }, slot)
    },
    onObserve (entries: IntersectionObserverEntry[]) {
      if (this.isActive) return

      this.isActive = Boolean(entries.find(entry => entry.isIntersecting))
    },
  },

  render (h): VNode {
    return h('div', {
      staticClass: 'v-observe',
      attrs: this.$attrs,
      directives: [{
        name: 'intersect',
        value: {
          handler: this.onObserve,
          options: this.options,
        },
      }] as any,
      on: this.$listeners,
      style: this.styles,
    }, [this.genContent()])
  },
})


<template>
  <v-row justify="center">
    <v-date-picker
      v-model="picker"
      year-icon="mdi-calendar-blank"
      prev-icon="mdi-skip-previous"
      next-icon="mdi-skip-next"
    ></v-date-picker>
  </v-row>
</template>

<script>
  export default {
    data () {
      return {
        picker: new Date().toISOString().substr(0, 10),
      }
    },
  }
</script>


import './_grid.sass'
import './VGrid.sass'

import Grid from './grid'

import mergeData from '../../util/mergeData'

/* @vue/component */
export default Grid('container').extend({
  name: 'v-container',
  functional: true,
  props: {
    id: String,
    tag: {
      type: String,
      default: 'div',
    },
    fluid: {
      type: Boolean,
      default: false,
    },
  },
  render (h, { props, data, children }) {
    let classes
    const { attrs } = data
    if (attrs) {
      // reset attrs to extract utility clases like pa-3
      data.attrs = {}
      classes = Object.keys(attrs).filter(key => {
        // TODO: Remove once resolved
        // https://github.com/vuejs/vue/issues/7841
        if (key === 'slot') return false

        const value = attrs[key]

        // add back data attributes like data-test="foo" but do not
        // add them as classes
        if (key.startsWith('data-')) {
          data.attrs![key] = value
          return false
        }

        return value || typeof value === 'string'
      })
    }

    if (props.id) {
      data.domProps = data.domProps || {}
      data.domProps.id = props.id
    }

    return h(
      props.tag,
      mergeData(data, {
        staticClass: 'container',
        class: Array<any>({
          'container--fluid': props.fluid,
        }).concat(classes || []),
      }),
      children
    )
  },
})


<!-- Rendered `v-list-group` content HTML Output -->

<div
  class="v-list-item-group"
  role="listbox"
>
  <div
    aria-selected="true"
    class="v-list-item"
    role="listitem"
    tabindex="0"
  >
    ...
  </div>
  <div
    aria-disabled="true"
    aria-selected="true"
    class="v-list-item"
    role="listitem"
    tabindex="-1"
  >
    ...
  </div>
  <div
    aria-selected="false"
    class="v-list-item"
    role="listitem"
    tabindex="0"
  >
    ...
  </div>
</div>


import camelCase from 'lodash/camelCase'
import upperFirst from 'lodash/upperFirst'

const requireLang = require.context(
  './',
  true,
  /\.json$/
)

const messages = {}

for (const file of requireLang.keys()) {
  if (file === './index.js') continue

  const path = file.replace(/(\.\/|\.json$)/g, '').split('/')

  path.reduce((o, s, i) => {
    const prop = upperFirst(camelCase(s))

    o[prop] = i + 1 === path.length
      ? requireLang(file)
      : o[prop] || {}

    return o[prop]
  }, messages)
}

export default messages


onObserve (entries: IntersectionObserverEntry[]) {


# Кнопки: плавающая кнопка действия

Плавающие кнопки действия могут быть прикреплены к материалу для указания рекламируемого действия в вашем приложении. Размер по умолчанию будет использоваться в большинстве случаев, тогда как «маленький» вариант может использоваться для поддержания преемственности с аналогичными элементами.

### Малый вариант

Для лучшей визуальной привлекательности мы используем небольшую кнопку, чтобы соответствовать нашим аватарам.

### Отображать анимацию

При первом отображении плавающая кнопка действия должна анимироваться на экране. Здесь мы используем `v-fab-transition` с `v-show`. Вы также можете использовать любой пользовательский переход, предоставляемый Vuetify или вашим собственным.

### Горизонтальный экран

При изменении действия по умолчанию вашей кнопки рекомендуется отображать переход для обозначения изменения. Мы делаем это, связывая `key` с частью данных, которая может правильно сообщать об изменении в системе перехода Vue. Хотя вы можете использовать для этого специальный переход, убедитесь, что вы установили `mode` prop для **out-in**.

### FAB со скоростным набором

Компонент быстрого набора обладает очень надёжным интерфейсом для настройки вашего FAB-интерфейса именно так, как вы хотите.

Направление, в котором будет отображаться содержимое скоростного набора. Возможные значения: `top`, `bottom`, `left`, `right`.

Открывает speed-dial при наведении


this.isActive = Boolean(entries.find(entry => entry.isIntersecting))

keep keep replace keep keep keep keep keep


},
},
render (h): VNode {
return h('div', {
staticClass: 'v-observe',
attrs: this.$attrs,
directives: [{
name: 'intersect',
value: {
handler: this.onObserve,

keep keep keep keep keep replace keep keep keep keep keep


@import './_blockquote'
@import './_code'
@import './_global'
@import './_headings'
@import './_lists'
@import './_typography'


import Vue, { VNode } from 'vue'

export default Vue.extend({
  name: 'row-group',

  functional: true,

  props: {
    value: {
      type: Boolean,
      default: true,
    },
    headerClass: {
      type: String,
      default: 'v-row-group__header',
    },
    contentClass: String,
    summaryClass: {
      type: String,
      default: 'v-row-group__summary',
    },
  },

  render (h, { slots, props }): VNode {
    const computedSlots = slots()
    const children = []

    if (computedSlots['column.header']) {
      children.push(h('tr', {
        staticClass: props.headerClass,
      }, computedSlots['column.header']))
    } else if (computedSlots['row.header']) {
      children.push(...computedSlots['row.header'])
    }

    if (computedSlots['row.content'] && props.value) children.push(...computedSlots['row.content'])

    if (computedSlots['column.summary']) {
      children.push(h('tr', {
        staticClass: props.summaryClass,
      }, computedSlots['column.summary']))
    } else if (computedSlots['row.summary']) {
      children.push(...computedSlots['row.summary'])
    }

    return children as any
  },
})


value: {
handler: this.onObserve,
options: this.options,
},
}] as any,
on: this.$listeners,
style: this.styles,
}, [this.genContent()])

keep keep keep keep replace keep keep keep

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VLazy.ts should conditionally render content 1`] = `

`;

exports[`VLazy.ts should conditionally render content 2`] = `

    foobar

`;


簡単ですが以下のplaygroundでレイアウトに関するpropsを試すことができます。

グリッドの順序を制御できます。 Offsetと同様に、サイズごとに異なる順序を設定できます。あらゆるアプリケーションに対応できるレイアウトを設計します。

Offsetは、まだ表示されていない可能性のある要素を補正したり、コンテンツの位置を制御したりするのに役立ちます。 ブレークポイントと同様に、利用可能なサイズにオフセットを設定できます。 これにより、アプリケーションのレイアウトを必要に合わせて微調整できます。

非常にカスタマイズされたレイアウトを実現するために、他のフレームワークと同様にグリッドをネストすることができます。

Vuetifyグリッドシステムのパワーと柔軟性により、素晴らしいユーザーインターフェースを作成できます。

`v-spacer`コンポーネントは利用可能なスペースを埋めるときや2つのコンポーネントの間にスペースを作りたいときに便利です。

`section`や` li`要素のように、レイアウトアイテムを特定のタグとして指定したいことがあるでしょう。


<template>
  <v-card>
    <v-tabs
      v-model="tab"
      background-color="deep-purple accent-4"
      centered
      dark
      icons-and-text
    >
      <v-tabs-slider></v-tabs-slider>

      <v-tab href="#tab-1">
        Recents
        <v-icon>mdi-phone</v-icon>
      </v-tab>

      <v-tab href="#tab-2">
        Favorites
        <v-icon>mdi-heart</v-icon>
      </v-tab>

      <v-tab href="#tab-3">
        Nearby
        <v-icon>mdi-account-box</v-icon>
      </v-tab>
    </v-tabs>

    <v-tabs-items v-model="tab">
      <v-tab-item
        v-for="i in 3"
        :key="i"
        :value="'tab-' + i"
      >
        <v-card flat>
          <v-card-text>{{ text }}</v-card-text>
        </v-card>
      </v-tab-item>
    </v-tabs-items>
  </v-card>
</template>

<script>
  export default {
    data () {
      return {
        tab: null,
        text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
      }
    },
  }
</script>


keep keep keep replace replace replace replace replace keep keep keep

const entries = [] as IntersectionObserverEntry[]

const observer = {} as IntersectionObserver


<template>
  <div v-mutate.char="onMutate">...</div>
</template>

<script>
  export default {
    methods: {
      onMutate (mutationsList, observer) {}
    }
  }
</script>


import Vue, { ComponentOptions } from 'vue'
import { Wrapper } from '@vue/test-utils'
import toHaveBeenWarnedInit from './util/to-have-been-warned'

export function functionalContext (context: ComponentOptions<Vue> = {}, children = []) {
  if (!Array.isArray(children)) children = [children]
  return {
    context: {
      data: {},
      props: {},
      ...context,
    },
    children,
  }
}

export function touch (element: Wrapper<any>) {
  const createTrigger = (eventName: string) => (clientX: number, clientY: number) => {
    const touches = [{ clientX, clientY }]
    const event = new Event(eventName)

    ;(event as any).touches = touches
    ;(event as any).changedTouches = touches
    element.element.dispatchEvent(event)

    return touch(element)
  }

  return {
    start: createTrigger('touchstart'),
    move: createTrigger('touchmove'),
    end: createTrigger('touchend'),
  }
}

export const resizeWindow = (width = window.innerWidth, height = window.innerHeight) => {
  (window as any).innerWidth = width
  ;(window as any).innerHeight = height
  window.dispatchEvent(new Event('resize'))
  return new Promise(resolve => setTimeout(resolve, 200))
}

export const scrollWindow = (y: number) => {
  (window as any).pageYOffset = y
  window.dispatchEvent(new Event('scroll'))

  return new Promise(resolve => setTimeout(resolve, 200))
}

// Add a global mockup for IntersectionObserver
(global as any).IntersectionObserver = class IntersectionObserver {
  callback: (entries: any, observer: any) => {}

  constructor (callback, options) {
    this.callback = callback
  }

  observe () {
    this.callback([], this)
    return null
  }

  unobserve () {
    this.callback = undefined
    return null
  }
}

toHaveBeenWarnedInit()


<template>
  <div>
    <v-card
      class="d-flex flex-row mb-6"
      color="grey lighten-2"
      flat
      tile
    >
      <v-card
        v-for="n in 3"
        :key="n"
        class="pa-2"
        outlined
        tile
      >
        Flex item {{ n }}
      </v-card>
    </v-card>
    <v-card
      class="d-flex flex-row-reverse"
      color="grey lighten-2"
      flat
      tile
    >
      <v-card
        v-for="n in 3"
        :key="n"
        class="pa-2"
        outlined
        tile
      >
        Flex item {{ n }}
      </v-card>
    </v-card>
  </div>
</template>


<template>

    <v-progress-linear
      indeterminate
      color="yellow darken-2"
    ></v-progress-linear>

    <v-progress-linear
      indeterminate
      color="green"
    ></v-progress-linear>

    <v-progress-linear
      indeterminate
      color="teal"
    ></v-progress-linear>

    <v-progress-linear
      indeterminate
      color="cyan"
    ></v-progress-linear>

</template>


水平方向の位置をリストアイテムと揃えるために、72px 右に寄せて表示できます。

横長のコンポーネント内でも区切り線は問題なく表示できます。

インデントを加えます。 (72px)


header


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VLazy.ts should conditionally render content 1`] = `


Applique un flotteur personnalisé sur n'importe quel point d'arrêt avec des utilitaires flottants réactifs.

Les classes d'utilitaires flottants appliquent float en fonction de la taille actuelle du port de vue en utilisant la [propriété flottante CSS](https://developer.mozilla.org/fr-US/docs/Web/CSS/float).

Activer facilement un float avec une classe :

Les floats peuvent également être appliquées sur une base par point de rupture (point de vue).

Voici une liste de toutes les classes de support disponibles :


<template>
  <v-container>
    <v-layout column>
      <main-header>Item Groups</main-header>

      <core-title>Simple</core-title>
      <core-section center>
        <v-chip-group data-cy-flex="simple">
          <v-chip
            v-for="n in 3"
            :key="n"
          >
            Example Chip
          </v-chip>
        </v-chip-group>
      </core-section>

      <core-title>Multiple</core-title>
      <core-section center>
        <v-chip-group multiple data-cy-flex="multiple">
          <v-chip
            v-for="n in 3"
            :key="n"
          >
            Example Chip
          </v-chip>
        </v-chip-group>
      </core-section>

      <core-title>Active class</core-title>
      <core-section center>
        <v-chip-group active-class="success" data-cy-flex="active">
          <v-chip
            v-for="n in 3"
            :key="n"
            data-cy-flex="activechip"
          >
            Example Chip
          </v-chip>
        </v-chip-group>
      </core-section>

      <core-title>Column</core-title>
      <core-section style="max-width: 300px;" center>
        <v-chip-group column multiple data-cy-flex="column">
          <v-chip
            v-for="n in 3"
            :key="n"
          >
            Example Chip
          </v-chip>
        </v-chip-group>
      </core-section>
    </v-layout>
  </v-container>
</template>

<script>
  export default {
    name: 'ChipGroups',

    data: () => ({}),
  }
</script>


exports[`VLazy.ts should conditionally render content 2`] = `

keep keep keep keep replace keep keep keep keep


Esta página te permitirá configurar las diferentes opciones que tienes disponibles para crear una distribución (layout) en Vuetify. Toma en cuenta que esta **no es** una lista exhaustiva, pero más bien un punto de partida para permitirte entender qué tienes disponible.


<template>
  <v-card flat>
    <v-card-text>
      <v-container fluid>
        <v-row>
          <v-col cols="12" sm="6" md="6">
            <v-radio-group v-model="ex7" column>
              <v-radio
                label="red"
                color="red"
                value="red"
              ></v-radio>
              <v-radio
                label="red darken-3"
                color="red darken-3"
                value="red darken-3"
              ></v-radio>
              <v-radio
                label="indigo"
                color="indigo"
                value="indigo"
              ></v-radio>
              <v-radio
                label="indigo darken-3"
                color="indigo darken-3"
                value="indigo darken-3"
              ></v-radio>
              <v-radio
                label="orange"
                color="orange"
                value="orange"
              ></v-radio>
              <v-radio
                label="orange darken-3"
                color="orange darken-3"
                value="orange darken-3"
              ></v-radio>
            </v-radio-group>
          </v-col>
          <v-col cols="12" sm="6" md="6">
            <v-radio-group v-model="ex8" column>
              <v-radio
                label="primary"
                color="primary"
                value="primary"
              ></v-radio>
              <v-radio
                label="secondary"
                color="secondary"
                value="secondary"
              ></v-radio>
              <v-radio
                label="success"
                color="success"
                value="success"
              ></v-radio>
              <v-radio
                label="info"
                color="info"
                value="info"
              ></v-radio>
              <v-radio
                label="warning"
                color="warning"
                value="warning"
              ></v-radio>
              <v-radio
                label="error"
                color="error"
                value="error"
              ></v-radio>
            </v-radio-group>
          </v-col>
        </v-row>
      </v-container>
    </v-card-text>
  </v-card>
</template>

<script>
  export default {
    data () {
      return {
        ex7: 'red',
        ex8: 'primary',
      }
    },
  }
</script>


import { Container, Service } from 'typedi';
import uniq from 'lodash/uniq';
import { createWriteStream } from 'fs';
import { mkdir } from 'fs/promises';
import path from 'path';

import type {
	ICredentialType,
	IN8nUISettings,
	INodeTypeBaseDescription,
	ITelemetrySettings,
} from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core';

import { LICENSE_FEATURES } from '@/constants';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { License } from '@/License';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import * as WebhookHelpers from '@/WebhookHelpers';
import config from '@/config';
import { getCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { getLdapLoginLabel } from '@/Ldap/helpers';
import { getSamlLoginLabel } from '@/sso/saml/samlHelpers';
import { getVariablesLimit } from '@/environments/variables/enviromentHelpers';
import {
	getWorkflowHistoryLicensePruneTime,
	getWorkflowHistoryPruneTime,
} from '@/workflows/workflowHistory/workflowHistoryHelper.ee';
import { UserManagementMailer } from '@/UserManagement/email';
import type { CommunityPackagesService } from '@/services/communityPackages.service';
import { Logger } from '@/Logger';

@Service()
export class FrontendService {
	settings: IN8nUISettings;

	private communityPackagesService?: CommunityPackagesService;

	constructor(
		private readonly logger: Logger,
		private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
		private readonly credentialTypes: CredentialTypes,
		private readonly credentialsOverwrites: CredentialsOverwrites,
		private readonly license: License,
		private readonly mailer: UserManagementMailer,
		private readonly instanceSettings: InstanceSettings,
	) {
		loadNodesAndCredentials.addPostProcessor(async () => this.generateTypes());
		void this.generateTypes();

		this.initSettings();

		if (config.getEnv('nodes.communityPackages.enabled')) {
			// eslint-disable-next-line @typescript-eslint/naming-convention
			void import('@/services/communityPackages.service').then(({ CommunityPackagesService }) => {
				this.communityPackagesService = Container.get(CommunityPackagesService);
			});
		}
	}

	private initSettings() {
		const instanceBaseUrl = getInstanceBaseUrl();
		const restEndpoint = config.getEnv('endpoints.rest');

		const telemetrySettings: ITelemetrySettings = {
			enabled: config.getEnv('diagnostics.enabled'),
		};

		if (telemetrySettings.enabled) {
			const conf = config.getEnv('diagnostics.config.frontend');
			const [key, url] = conf.split(';');

			if (!key || !url) {
				this.logger.warn('Diagnostics frontend config is invalid');
				telemetrySettings.enabled = false;
			}

			telemetrySettings.config = { key, url };
		}

		this.settings = {
			endpointWebhook: config.getEnv('endpoints.webhook'),
			endpointWebhookTest: config.getEnv('endpoints.webhookTest'),
			saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
			saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
			saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
			executionTimeout: config.getEnv('executions.timeout'),
			maxExecutionTimeout: config.getEnv('executions.maxTimeout'),
			workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'),
			timezone: config.getEnv('generic.timezone'),
			urlBaseWebhook: WebhookHelpers.getWebhookBaseUrl(),
			urlBaseEditor: instanceBaseUrl,
			versionCli: '',
			releaseChannel: config.getEnv('generic.releaseChannel'),
			oauthCallbackUrls: {
				oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,
				oauth2: `${instanceBaseUrl}/${restEndpoint}/oauth2-credential/callback`,
			},
			versionNotifications: {
				enabled: config.getEnv('versionNotifications.enabled'),
				endpoint: config.getEnv('versionNotifications.endpoint'),
				infoUrl: config.getEnv('versionNotifications.infoUrl'),
			},
			instanceId: this.instanceSettings.instanceId,
			telemetry: telemetrySettings,
			posthog: {
				enabled: config.getEnv('diagnostics.enabled'),
				apiHost: config.getEnv('diagnostics.config.posthog.apiHost'),
				apiKey: config.getEnv('diagnostics.config.posthog.apiKey'),
				autocapture: false,
				disableSessionRecording: config.getEnv(
					'diagnostics.config.posthog.disableSessionRecording',
				),
				debug: config.getEnv('logs.level') === 'debug',
			},
			personalizationSurveyEnabled:
				config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
			defaultLocale: config.getEnv('defaultLocale'),
			userManagement: {
				quota: this.license.getUsersLimit(),
				showSetupOnFirstLoad: !config.getEnv('userManagement.isInstanceOwnerSetUp'),
				smtpSetup: this.mailer.isEmailSetUp,
				authenticationMethod: getCurrentAuthenticationMethod(),
			},
			sso: {
				saml: {
					loginEnabled: false,
					loginLabel: '',
				},
				ldap: {
					loginEnabled: false,
					loginLabel: '',
				},
			},
			publicApi: {
				enabled: !config.get('publicApi.disabled') && !this.license.isAPIDisabled(),
				latestVersion: 1,
				path: config.getEnv('publicApi.path'),
				swaggerUi: {
					enabled: !config.getEnv('publicApi.swaggerUi.disabled'),
				},
			},
			workflowTagsDisabled: config.getEnv('workflowTagsDisabled'),
			logLevel: config.getEnv('logs.level'),
			hiringBannerEnabled: config.getEnv('hiringBanner.enabled'),
			templates: {
				enabled: config.getEnv('templates.enabled'),
				host: config.getEnv('templates.host'),
			},
			onboardingCallPromptEnabled: config.getEnv('onboardingCallPrompt.enabled'),
			executionMode: config.getEnv('executions.mode'),
			pushBackend: config.getEnv('push.backend'),
			communityNodesEnabled: config.getEnv('nodes.communityPackages.enabled'),
			deployment: {
				type: config.getEnv('deployment.type'),
			},
			isNpmAvailable: false,
			allowedModules: {
				builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN?.split(',') ?? undefined,
				external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL?.split(',') ?? undefined,
			},
			enterprise: {
				sharing: false,
				ldap: false,
				saml: false,
				logStreaming: false,
				advancedExecutionFilters: false,
				variables: false,
				sourceControl: false,
				auditLogs: false,
				externalSecrets: false,
				showNonProdBanner: false,
				debugInEditor: false,
				binaryDataS3: false,
				workflowHistory: false,
				workerView: false,
			},
			mfa: {
				enabled: false,
			},
			hideUsagePage: config.getEnv('hideUsagePage'),
			license: {
				environment: config.getEnv('license.tenantId') === 1 ? 'production' : 'staging',
			},
			variables: {
				limit: 0,
			},
			expressions: {
				evaluator: config.getEnv('expression.evaluator'),
			},
			banners: {
				dismissed: [],
			},
			ai: {
				enabled: config.getEnv('ai.enabled'),
			},
			workflowHistory: {
				pruneTime: -1,
				licensePruneTime: -1,
			},
		};
	}

	async generateTypes() {
		this.overwriteCredentialsProperties();

		const { staticCacheDir } = this.instanceSettings;
		// pre-render all the node and credential types as static json files
		await mkdir(path.join(staticCacheDir, 'types'), { recursive: true });
		const { credentials, nodes } = this.loadNodesAndCredentials.types;
		this.writeStaticJSON('nodes', nodes);
		this.writeStaticJSON('credentials', credentials);
	}

	getSettings(): IN8nUISettings {
		const restEndpoint = config.getEnv('endpoints.rest');

		// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
		const instanceBaseUrl = getInstanceBaseUrl();
		this.settings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl();
		this.settings.urlBaseEditor = instanceBaseUrl;
		this.settings.oauthCallbackUrls = {
			oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,
			oauth2: `${instanceBaseUrl}/${restEndpoint}/oauth2-credential/callback`,
		};

		// refresh user management status
		Object.assign(this.settings.userManagement, {
			quota: this.license.getUsersLimit(),
			authenticationMethod: getCurrentAuthenticationMethod(),
			showSetupOnFirstLoad:
				!config.getEnv('userManagement.isInstanceOwnerSetUp') &&
				!config.getEnv('deployment.type').startsWith('desktop_'),
		});

		let dismissedBanners: string[] = [];

		try {
			dismissedBanners = config.getEnv('ui.banners.dismissed') ?? [];
		} catch {
			// not yet in DB
		}

		this.settings.banners.dismissed = dismissedBanners;

		const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
		const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
		const isS3Licensed = this.license.isBinaryDataS3Licensed();

		// refresh enterprise status
		Object.assign(this.settings.enterprise, {
			sharing: this.license.isSharingEnabled(),
			logStreaming: this.license.isLogStreamingEnabled(),
			ldap: this.license.isLdapEnabled(),
			saml: this.license.isSamlEnabled(),
			advancedExecutionFilters: this.license.isAdvancedExecutionFiltersEnabled(),
			variables: this.license.isVariablesEnabled(),
			sourceControl: this.license.isSourceControlLicensed(),
			externalSecrets: this.license.isExternalSecretsEnabled(),
			showNonProdBanner: this.license.isFeatureEnabled(LICENSE_FEATURES.SHOW_NON_PROD_BANNER),
			debugInEditor: this.license.isDebugInEditorLicensed(),
			binaryDataS3: isS3Available && isS3Selected && isS3Licensed,
			workflowHistory:
				this.license.isWorkflowHistoryLicensed() && config.getEnv('workflowHistory.enabled'),
			workerView: this.license.isWorkerViewLicensed(),
		});

		if (this.license.isLdapEnabled()) {
			Object.assign(this.settings.sso.ldap, {
				loginLabel: getLdapLoginLabel(),
				loginEnabled: config.getEnv('sso.ldap.loginEnabled'),
			});
		}

		if (this.license.isSamlEnabled()) {
			Object.assign(this.settings.sso.saml, {
				loginLabel: getSamlLoginLabel(),
				loginEnabled: config.getEnv('sso.saml.loginEnabled'),
			});
		}

		if (this.license.isVariablesEnabled()) {
			this.settings.variables.limit = getVariablesLimit();
		}

		if (this.license.isWorkflowHistoryLicensed() && config.getEnv('workflowHistory.enabled')) {
			Object.assign(this.settings.workflowHistory, {
				pruneTime: getWorkflowHistoryPruneTime(),
				licensePruneTime: getWorkflowHistoryLicensePruneTime(),
			});
		}

		if (this.communityPackagesService) {
			this.settings.missingPackages = this.communityPackagesService.hasMissingPackages;
		}

		this.settings.mfa.enabled = config.get('mfa.enabled');

		this.settings.executionMode = config.getEnv('executions.mode');

		return this.settings;
	}

	addToSettings(newSettings: Record<string, unknown>) {
		this.settings = { ...this.settings, ...newSettings };
	}

	private writeStaticJSON(name: string, data: INodeTypeBaseDescription[] | ICredentialType[]) {
		const { staticCacheDir } = this.instanceSettings;
		const filePath = path.join(staticCacheDir, `types/${name}.json`);
		const stream = createWriteStream(filePath, 'utf-8');
		stream.write('[\n');
		data.forEach((entry, index) => {
			stream.write(JSON.stringify(entry));
			if (index !== data.length - 1) stream.write(',');
			stream.write('\n');
		});
		stream.write(']\n');
		stream.end();
	}

	private overwriteCredentialsProperties() {
		const { credentials } = this.loadNodesAndCredentials.types;
		const credentialsOverwrites = this.credentialsOverwrites.getAll();
		for (const credential of credentials) {
			const overwrittenProperties = [];
			this.credentialTypes
				.getParentTypes(credential.name)
				.reverse()
				.map((name) => credentialsOverwrites[name])
				.forEach((overwrite) => {
					if (overwrite) overwrittenProperties.push(...Object.keys(overwrite));
				});

			if (credential.name in credentialsOverwrites) {
				overwrittenProperties.push(...Object.keys(credentialsOverwrites[credential.name]));
			}

			if (overwrittenProperties.length) {
				credential.__overwrittenProperties = uniq(overwrittenProperties);
			}
		}
	}
}


import type { OptionsWithUrl } from 'request';

import type {
	IDataObject,
	IExecuteFunctions,
	IHookFunctions,
	ILoadOptionsFunctions,
	JsonObject,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
function resolveHeaderData(fullResponse: any) {
	if (fullResponse.statusCode === 201) {
		return { urn: fullResponse.headers['x-restli-id'] };
	} else {
		return fullResponse.body;
	}
}

export async function linkedInApiRequest(
	this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
	method: string,
	endpoint: string,

	body: any = {},
	binary?: boolean,
	_headers?: object,
): Promise<any> {
	let options: OptionsWithUrl = {
		headers: {
			Accept: 'application/json',
			'X-Restli-Protocol-Version': '2.0.0',
			'LinkedIn-Version': '202304',
		},
		method,
		body,
		url: binary ? endpoint : `https://api.linkedin.com/rest${endpoint}`,
		json: true,
	};
	options = Object.assign({}, options, {
		resolveWithFullResponse: true,
	});
	// If uploading binary data
	if (binary) {
		delete options.json;
		options.encoding = null;
	}

	if (Object.keys(body as IDataObject).length === 0) {
		delete options.body;
	}

	try {
		return resolveHeaderData(
			await this.helpers.requestOAuth2.call(this, 'linkedInOAuth2Api', options, {
				tokenType: 'Bearer',
			}),
		);
	} catch (error) {
		throw new NodeApiError(this.getNode(), error as JsonObject);
	}
}

export function validateJSON(json: string | undefined): any {
	let result;
	try {
		result = JSON.parse(json!);
	} catch (exception) {
		result = '';
	}
	return result;
}


export type CommentAnalyzeBody = {
	comment: Comment;
	requestedAttributes: RequestedAttributes;
	languages?: Language;
};

export type Language = 'de' | 'en' | 'fr' | 'ar' | 'es' | 'it' | 'pt' | 'ru';

export type Comment = {
	text?: string;
	type?: string;
};

export type RequestedAttributes = {
	[key: string]: {
		scoreType?: string;
		scoreThreshold?: {
			value: number;
		};
	};
};

export type AttributesValuesUi = {
	attributeName: string;
	scoreThreshold: number;
};


Hey founders! Your business doesn't need you to operate

7 no-code workflow automations for Amazon Web Services


binaryDataS3: isS3Available && isS3Selected && isS3Licensed,

keep keep keep keep add keep keep keep keep


import type { MigrationContext, ReversibleMigration } from '@db/types';

export class CreateExecutionMetadataTable1679416281779 implements ReversibleMigration {
	async up({ queryRunner, tablePrefix }: MigrationContext) {
		await queryRunner.query(
			`CREATE TABLE ${tablePrefix}execution_metadata (
				id int(11) auto_increment NOT NULL PRIMARY KEY,
				executionId int(11) NOT NULL,
				\`key\` TEXT NOT NULL,
				value TEXT NOT NULL,
				CONSTRAINT \`${tablePrefix}execution_metadata_FK\` FOREIGN KEY (\`executionId\`) REFERENCES \`${tablePrefix}execution_entity\` (\`id\`) ON DELETE CASCADE,
				INDEX \`IDX_${tablePrefix}6d44376da6c1058b5e81ed8a154e1fee106046eb\` (\`executionId\` ASC)
			)
			ENGINE=InnoDB`,
		);

		// Remove indices that are no longer needed since the addition of the status column
		await queryRunner.query(
			`DROP INDEX \`IDX_${tablePrefix}06da892aaf92a48e7d3e400003\` ON \`${tablePrefix}execution_entity\``,
		);
		await queryRunner.query(
			`DROP INDEX \`IDX_${tablePrefix}78d62b89dc1433192b86dce18a\` ON \`${tablePrefix}execution_entity\``,
		);
		await queryRunner.query(
			`DROP INDEX \`IDX_${tablePrefix}1688846335d274033e15c846a4\` ON \`${tablePrefix}execution_entity\``,
		);
		await queryRunner.query(
			`DROP INDEX \`IDX_${tablePrefix}cefb067df2402f6aed0638a6c1\` ON \`${tablePrefix}execution_entity\``,
		);

		// Add index to the new status column
		await queryRunner.query(
			`CREATE INDEX \`IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584\` ON \`${tablePrefix}execution_entity\` (\`status\`, \`workflowId\`)`,
		);
	}

	async down({ queryRunner, tablePrefix }: MigrationContext) {
		await queryRunner.query(`DROP TABLE "${tablePrefix}execution_metadata"`);
		await queryRunner.query(
			`CREATE INDEX \`IDX_${tablePrefix}06da892aaf92a48e7d3e400003\` ON \`${tablePrefix}execution_entity\` (\`workflowId\`, \`waitTill\`, \`id\`)`,
		);
		await queryRunner.query(
			`CREATE INDEX \`IDX_${tablePrefix}78d62b89dc1433192b86dce18a\` ON \`${tablePrefix}execution_entity\` (\`workflowId\`, \`finished\`, \`id\`)`,
		);
		await queryRunner.query(
			`CREATE INDEX \`IDX_${tablePrefix}1688846335d274033e15c846a4\` ON \`${tablePrefix}execution_entity\` (\`finished\`, \`id\`)`,
		);
		await queryRunner.query(
			'CREATE INDEX `IDX_' +
				tablePrefix +
				'cefb067df2402f6aed0638a6c1` ON `' +
				tablePrefix +
				'execution_entity` (`stoppedAt`)',
		);
		await queryRunner.query(
			`DROP INDEX \`IDX_${tablePrefix}8b6f3f9ae234f137d707b98f3bf43584\` ON \`${tablePrefix}execution_entity\``,
		);
	}
}


/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import { promisify } from 'util';
import type {
	IExecuteFunctions,
	IDataObject,
	INodeExecutionData,
	INodeType,
	INodeTypeDescription,
} from 'n8n-workflow';

import moment from 'moment-timezone';

import * as ics from 'ics';

const createEvent = promisify(ics.createEvent);

export class ICalendar implements INodeType {
	description: INodeTypeDescription = {
		displayName: 'iCalendar',
		name: 'iCal',
		icon: 'fa:calendar',
		group: ['input'],
		version: 1,
		subtitle: '={{$parameter["operation"]}}',
		description: 'Create iCalendar file',
		defaults: {
			name: 'iCalendar',
			color: '#408000',
		},
		inputs: ['main'],
		outputs: ['main'],
		credentials: [],
		properties: [
			{
				displayName: 'Operation',
				name: 'operation',
				type: 'options',
				noDataExpression: true,
				options: [
					{
						name: 'Create Event File',
						value: 'createEventFile',
					},
				],
				default: 'createEventFile',
			},
			{
				displayName: 'Event Title',
				name: 'title',
				type: 'string',
				default: '',
			},
			{
				displayName: 'Start',
				name: 'start',
				type: 'dateTime',
				default: '',
				required: true,
				description:
					'Date and time at which the event begins. (For all-day events, the time will be ignored.).',
			},
			{
				displayName: 'End',
				name: 'end',
				type: 'dateTime',
				default: '',
				required: true,
				description:
					'Date and time at which the event ends. (For all-day events, the time will be ignored.).',
			},
			{
				displayName: 'All Day',
				name: 'allDay',
				type: 'boolean',
				default: false,
				description: 'Whether the event lasts all day or not',
			},
			{
				displayName: 'Binary Property',
				name: 'binaryPropertyName',
				type: 'string',
				default: 'data',
				required: true,
				description: 'The field that your iCalendar file will be available under in the output',
			},
			{
				displayName: 'Additional Fields',
				name: 'additionalFields',
				type: 'collection',
				placeholder: 'Add Field',
				default: {},
				displayOptions: {
					show: {
						operation: ['createEventFile'],
					},
				},
				options: [
					{
						displayName: 'Attendees',
						name: 'attendeesUi',
						type: 'fixedCollection',
						typeOptions: {
							multipleValues: true,
						},
						placeholder: 'Add Attendee',
						default: {},
						options: [
							{
								displayName: 'Attendees',
								name: 'attendeeValues',
								values: [
									{
										displayName: 'Name',
										name: 'name',
										type: 'string',
										required: true,
										default: '',
									},
									{
										displayName: 'Email',
										name: 'email',
										type: 'string',
										placeholder: 'name@email.com',
										required: true,
										default: '',
									},
									{
										displayName: 'RSVP',
										name: 'rsvp',
										type: 'boolean',
										default: false,
										description: 'Whether the attendee has to confirm attendance or not',
									},
								],
							},
						],
					},
					{
						displayName: 'Busy Status',
						name: 'busyStatus',
						type: 'options',
						options: [
							{
								name: 'Busy',
								value: 'BUSY',
							},
							{
								name: 'Tentative',
								value: 'TENTATIVE',
							},
						],
						default: '',
						description: 'Used to specify busy status for Microsoft applications, like Outlook',
					},
					{
						displayName: 'Calendar Name',
						name: 'calName',
						type: 'string',
						default: '',
						description:
							'Specifies the calendar (not event) name. Used by Apple iCal and Microsoft Outlook (spec).',
					},
					{
						displayName: 'Description',
						name: 'description',
						type: 'string',
						default: '',
					},
					{
						displayName: 'File Name',
						name: 'fileName',
						type: 'string',
						default: '',
						description: 'The name of the file to be generated. Default value is event.ics.',
					},
					{
						displayName: 'Geolocation',
						name: 'geolocationUi',
						type: 'fixedCollection',
						typeOptions: {
							multipleValues: false,
						},
						placeholder: 'Add Geolocation',
						default: {},
						options: [
							{
								displayName: 'Geolocation',
								name: 'geolocationValues',
								values: [
									{
										displayName: 'Latitude',
										name: 'lat',
										type: 'string',
										default: '',
									},
									{
										displayName: 'Longitude',
										name: 'lon',
										type: 'string',
										default: '',
									},
								],
							},
						],
					},
					{
						displayName: 'Location',
						name: 'location',
						type: 'string',
						default: '',
						description: 'The intended venue',
					},
					{
						displayName: 'Recurrence Rule',
						name: 'recurrenceRule',
						type: 'string',
						default: '',
						description:
							'A rule to define the repeat pattern of the event (RRULE). (Rule generator).',
					},
					{
						displayName: 'Organizer',
						name: 'organizerUi',
						type: 'fixedCollection',
						typeOptions: {
							multipleValues: false,
						},
						placeholder: 'Add Organizer',
						default: {},
						options: [
							{
								displayName: 'Organizer',
								name: 'organizerValues',
								values: [
									{
										displayName: 'Name',
										name: 'name',
										type: 'string',
										default: '',
										required: true,
									},
									{
										displayName: 'Email',
										name: 'email',
										type: 'string',
										placeholder: 'name@email.com',
										default: '',
										required: true,
									},
								],
							},
						],
					},
					{
						displayName: 'Sequence',
						name: 'sequence',
						type: 'number',
						default: 0,
						description:
							'When sending an update for an event (with the same uid), defines the revision sequence number',
					},
					{
						displayName: 'Status',
						name: 'status',
						type: 'options',
						options: [
							{
								name: 'Confirmed',
								value: 'CONFIRMED',
							},
							{
								name: 'Cancelled',
								value: 'CANCELLED',
							},
							{
								name: 'Tentative',
								value: 'TENTATIVE',
							},
						],
						default: 'CONFIRMED',
					},
					{
						displayName: 'UID',
						name: 'uid',
						type: 'string',
						default: '',
						description:
							'Universally unique ID for the event (will be auto-generated if not specified here). Should be globally unique.',
					},
					{
						displayName: 'URL',
						name: 'url',
						type: 'string',
						default: '',
						description: 'URL associated with event',
					},
				],
			},
		],
	};

	async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
		const items = this.getInputData();
		const length = items.length;
		const returnData: INodeExecutionData[] = [];
		const operation = this.getNodeParameter('operation', 0);
		if (operation === 'createEventFile') {
			for (let i = 0; i < length; i++) {
				const title = this.getNodeParameter('title', i) as string;
				const allDay = this.getNodeParameter('allDay', i) as boolean;
				const start = this.getNodeParameter('start', i) as string;
				let end = this.getNodeParameter('end', i) as string;
				end = allDay ? moment(end).utc().add(1, 'day').format() : end;
				const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i);
				const additionalFields = this.getNodeParameter('additionalFields', i);
				let fileName = 'event.ics';

				const eventStart = moment(start)
					.toArray()
					.splice(0, allDay ? 3 : 6) as ics.DateArray;
				eventStart[1]++;
				const eventEnd = moment(end)
					.toArray()
					.splice(0, allDay ? 3 : 6) as ics.DateArray;
				eventEnd[1]++;

				if (additionalFields.fileName) {
					fileName = additionalFields.fileName as string;
				}

				const data: ics.EventAttributes = {
					title,
					start: eventStart,
					end: eventEnd,
					startInputType: 'utc',
					endInputType: 'utc',
				};

				if (additionalFields.geolocationUi) {
					data.geo = (additionalFields.geolocationUi as IDataObject)
						.geolocationValues as ics.GeoCoordinates;
					delete additionalFields.geolocationUi;
				}

				if (additionalFields.organizerUi) {
					data.organizer = (additionalFields.organizerUi as IDataObject)
						.organizerValues as ics.Person;
					delete additionalFields.organizerUi;
				}

				if (additionalFields.attendeesUi) {
					data.attendees = (additionalFields.attendeesUi as IDataObject)
						.attendeeValues as ics.Attendee[];
					delete additionalFields.attendeesUi;
				}

				Object.assign(data, additionalFields);
				const buffer = Buffer.from((await createEvent(data)) as string);
				const binaryData = await this.helpers.prepareBinaryData(buffer, fileName, 'text/calendar');
				returnData.push({
					json: {},
					binary: {
						[binaryPropertyName]: binaryData,
					},
					pairedItem: {
						item: i,
					},
				});
			}
		}
		return [returnData];
	}
}


environment: 'development' | 'production' | 'staging';

keep keep keep add keep keep keep keep keep