Meteor.startup ->
	UserPresence.awayTime = 300000
	UserPresence.start()
	Meteor.subscribe("activeUsers")

	Session.setDefault('flexOpened', false)
	Session.setDefault('AvatarRandom', 0)

	window.lastMessageWindow = {}
	window.lastMessageWindowHistory = {}

	@defaultUserLanguage = ->
		lng = window.navigator.userLanguage || window.navigator.language || 'en'
		# Fix browsers having all-lowercase language settings eg. pt-br, en-us
		re = /([a-z]{2}-)([a-z]{2})/
		if re.test lng
			lng = lng.replace re, (match, parts...) -> return parts[0] + parts[1].toUpperCase()
		return lng

	loadedLaguages = []

	setLanguage = (language) ->
		if loadedLaguages.indexOf(language) > -1
			return

		loadedLaguages.push language

		language = language.split('-').shift()
		TAPi18n.setLanguage(language)

		language = language.toLowerCase()
		if language isnt 'en'
			Meteor.call 'loadLocale', language, (data) ->
				moment.locale(language)

	Tracker.autorun (c) ->
		if Meteor.user()?.language?
			c.stop()

			localStorage.setItem("userLanguage", Meteor.user().language)
			setLanguage Meteor.user().language

	userLanguage = localStorage.getItem("userLanguage")
	userLanguage ?= defaultUserLanguage()

	setLanguage userLanguage


RocketChat.settings.add 'Allow_Invalid_SelfSigned_Certs', false, { type: 'boolean', group: 'General' }

Meteor.startup ->
	RocketChat.settings.addGroup 'Accounts'
	RocketChat.settings.add 'Accounts_RegistrationRequired', true, { type: 'boolean', group: 'Accounts', public: true, section: 'Registration' }
	RocketChat.settings.add 'Accounts_EmailVerification', false, { type: 'boolean', group: 'Accounts', public: true, section: 'Registration' }
	RocketChat.settings.add 'Accounts_ManuallyApproveNewUsers', false, { type: 'boolean', group: 'Accounts', section: 'Registration' }

	RocketChat.settings.add 'Accounts_AvatarStoreType', 'GridFS', { type: 'string', group: 'Accounts', section: 'Avatar' }
	RocketChat.settings.add 'Accounts_AvatarStorePath', '/var/www/rocket.chat/uploads/avatar/', { type: 'string', group: 'Accounts', section: 'Avatar' }
	RocketChat.settings.add 'Accounts_AvatarResize', false, { type: 'boolean', group: 'Accounts', section: 'Avatar' }
	RocketChat.settings.add 'Accounts_AvatarSize', 200, { type: 'int', group: 'Accounts', section: 'Avatar' }

	RocketChat.settings.add 'Accounts_OAuth_Facebook', false, { type: 'boolean', group: 'Accounts', section: 'Facebook' }
	RocketChat.settings.add 'Accounts_OAuth_Facebook_id', '', { type: 'string', group: 'Accounts', section: 'Facebook' }
	RocketChat.settings.add 'Accounts_OAuth_Facebook_secret', '', { type: 'string', group: 'Accounts', section: 'Facebook' }
	RocketChat.settings.add 'Accounts_OAuth_Google', false, { type: 'boolean', group: 'Accounts', section: 'Google' }
	RocketChat.settings.add 'Accounts_OAuth_Google_id', '', { type: 'string', group: 'Accounts', section: 'Google' }
	RocketChat.settings.add 'Accounts_OAuth_Google_secret', '', { type: 'string', group: 'Accounts', section: 'Google' }
	RocketChat.settings.add 'Accounts_OAuth_Github', false, { type: 'boolean', group: 'Accounts', section: 'Github' }
	RocketChat.settings.add 'Accounts_OAuth_Github_id', '', { type: 'string', group: 'Accounts', section: 'Github' }
	RocketChat.settings.add 'Accounts_OAuth_Github_secret', '', { type: 'string', group: 'Accounts', section: 'Github' }
	RocketChat.settings.add 'Accounts_OAuth_Gitlab', false, { type: 'boolean', group: 'Accounts', section: 'Gitlab' }
	RocketChat.settings.add 'Accounts_OAuth_Gitlab_id', '', { type: 'string', group: 'Accounts', section: 'Gitlab' }
	RocketChat.settings.add 'Accounts_OAuth_Gitlab_secret', '', { type: 'string', group: 'Accounts', section: 'Gitlab' }
	RocketChat.settings.add 'Accounts_OAuth_Linkedin', false, { type: 'boolean', group: 'Accounts', section: 'Linkedin' }
	RocketChat.settings.add 'Accounts_OAuth_Linkedin_id', '', { type: 'string', group: 'Accounts', section: 'Linkedin' }
	RocketChat.settings.add 'Accounts_OAuth_Linkedin_secret', '', { type: 'string', group: 'Accounts', section: 'Linkedin' }
	RocketChat.settings.add 'Accounts_OAuth_Meteor', false, { type: 'boolean', group: 'Accounts', section: 'Meteor' }
	RocketChat.settings.add 'Accounts_OAuth_Meteor_id', '', { type: 'string', group: 'Accounts', section: 'Meteor' }
	RocketChat.settings.add 'Accounts_OAuth_Meteor_secret', '', { type: 'string', group: 'Accounts', section: 'Meteor' }
	RocketChat.settings.add 'Accounts_OAuth_Twitter', false, { type: 'boolean', group: 'Accounts', section: 'Twitter' }
	RocketChat.settings.add 'Accounts_OAuth_Twitter_id', '', { type: 'string', group: 'Accounts', section: 'Twitter' }
	RocketChat.settings.add 'Accounts_OAuth_Twitter_secret', '', { type: 'string', group: 'Accounts', section: 'Twitter' }

	RocketChat.settings.addGroup 'General'
	RocketChat.settings.add 'Site_Name', 'Rocket.Chat', { type: 'string', group: 'General', public: true }

	RocketChat.settings.addGroup 'API'
	RocketChat.settings.add 'API_Analytics', '', { type: 'string', group: 'API', public: true }
	RocketChat.settings.add 'API_Embed', true, { type: 'boolean', group: 'API', public: true }

	RocketChat.settings.addGroup 'SMTP'
	RocketChat.settings.add 'SMTP_Host', '', { type: 'string', group: 'SMTP' }
	RocketChat.settings.add 'SMTP_Port', '', { type: 'string', group: 'SMTP' }
	RocketChat.settings.add 'SMTP_Username', '', { type: 'string', group: 'SMTP' }
	RocketChat.settings.add 'SMTP_Password', '', { type: 'string', group: 'SMTP' }
	RocketChat.settings.add 'From_Email', 'no-reply@rocket.chat', { type: 'string', group: 'SMTP' }

	RocketChat.settings.addGroup 'Message'
	RocketChat.settings.add 'Message_AllowEditing', true, { type: 'boolean', group: 'Message', public: true }
	RocketChat.settings.add 'Message_AllowDeleting', true, { type: 'boolean', group: 'Message', public: true }
	RocketChat.settings.add 'Message_AllowPinning', true, { type: 'boolean', group: 'Message', public: true }
	RocketChat.settings.add 'Message_ShowEditedStatus', true, { type: 'boolean', group: 'Message', public: true }
	RocketChat.settings.add 'Message_ShowDeletedStatus', false, { type: 'boolean', group: 'Message', public: true }
	RocketChat.settings.add 'Message_KeepHistory', false, { type: 'boolean', group: 'Message', public: true }
	RocketChat.settings.add 'Message_MaxAllowedSize', 5000, { type: 'int', group: 'Message', public: true }

	RocketChat.settings.addGroup 'Meta'
	RocketChat.settings.add 'Meta_language', '', { type: 'string', group: 'Meta' }
	RocketChat.settings.add 'Meta_fb_app_id', '', { type: 'string', group: 'Meta' }
	RocketChat.settings.add 'Meta_robots', '', { type: 'string', group: 'Meta' }
	RocketChat.settings.add 'Meta_google-site-verification', '', { type: 'string', group: 'Meta' }
	RocketChat.settings.add 'Meta_msvalidate01', '', { type: 'string', group: 'Meta' }

	RocketChat.settings.addGroup 'Push'
	RocketChat.settings.add 'Push_debug', false, { type: 'boolean', group: 'Push', public: true }
	RocketChat.settings.add 'Push_enable', false, { type: 'boolean', group: 'Push', public: true }
	RocketChat.settings.add 'Push_production', false, { type: 'boolean', group: 'Push', public: true }
	RocketChat.settings.add 'Push_apn_passphrase', '', { type: 'string', group: 'Push' }
	RocketChat.settings.add 'Push_apn_key', '', { type: 'string', multiline: true, group: 'Push' }
	RocketChat.settings.add 'Push_apn_cert', '', { type: 'string', multiline: true, group: 'Push' }
	RocketChat.settings.add 'Push_apn_dev_passphrase', '', { type: 'string', group: 'Push' }
	RocketChat.settings.add 'Push_apn_dev_key', '', { type: 'string', multiline: true, group: 'Push' }
	RocketChat.settings.add 'Push_apn_dev_cert', '', { type: 'string', multiline: true, group: 'Push' }
	RocketChat.settings.add 'Push_gcm_api_key', '', { type: 'string', group: 'Push' }
	RocketChat.settings.add 'Push_gcm_project_number', '', { type: 'string', group: 'Push', public: true }

	RocketChat.settings.addGroup 'Layout'
	RocketChat.settings.add 'Layout_Home_Title', 'Home', { type: 'string', group: 'Layout', public: true, section: 'Content' }
	RocketChat.settings.add 'Layout_Home_Body', 'Welcome to Rocket.Chat
 Go to APP SETTINGS -> Layout to customize this intro.', { type: 'string', multiline: true, group: 'Layout', public: true, section: 'Content' }
	RocketChat.settings.add 'Layout_Terms_of_Service', 'Terms of Service
 Go to APP SETTINGS -> Layout to customize this page.', { type: 'string', multiline: true, group: 'Layout', public: true, section: 'Content' }
	RocketChat.settings.add 'Layout_Privacy_Policy', 'Privacy Policy
 Go to APP SETTINGS -> Layout to customize this page.', { type: 'string', multiline: true, group: 'Layout', public: true, section: 'Content' }
	RocketChat.settings.add 'Layout_Sidenav_Footer', '   Fork it on github ', { type: 'string', group: 'Layout', public: true, i18nDescription: 'Layout_Sidenav_Footer_description' }
	RocketChat.settings.add 'Layout_Login_Header', '', { type: 'string', multiline: true, group: 'Layout', public: true, section: 'Login' }
	RocketChat.settings.add 'Layout_Login_Terms', 'By proceeding to create your account and use Rocket.Chat, you are agreeing to our Terms of Service and Privacy Policy. If you do not agree, you cannot use Rocket.Chat.', { type: 'string', multiline: true, group: 'Layout', public: true, section: 'Login' }

	RocketChat.settings.add 'Statistics_opt_out', false, { type: 'boolean', group: false }

	if process?.env? and not process.env['MAIL_URL']? and RocketChat.settings.get('SMTP_Host') and RocketChat.settings.get('SMTP_Username') and RocketChat.settings.get('SMTP_Password')
		process.env['MAIL_URL'] = "smtp://" + encodeURIComponent(RocketChat.settings.get('SMTP_Username')) + ':' + encodeURIComponent(RocketChat.settings.get('SMTP_Password')) + '@' + encodeURIComponent(RocketChat.settings.get('SMTP_Host'))
		if RocketChat.settings.get('SMTP_Port')
			process.env['MAIL_URL'] += ':' + parseInt(RocketChat.settings.get('SMTP_Port'))


@getAvatarSuggestionForUser = (user) ->
	avatars = []

	if user.services.facebook?.id?
		avatars.push
			service: 'facebook'
			url: "https://graph.facebook.com/#{user.services.facebook.id}/picture?type=large"

	if user.services.google?.picture? and user.services.google.picture isnt "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg"
		avatars.push
			service: 'google'
			url: user.services.google.picture

	if user.services.github?.username?
		avatars.push
			service: 'github'
			url: "https://avatars.githubusercontent.com/#{user.services.github.username}?s=200"

	if user.services.linkedin?.pictureUrl?
		avatars.push
			service: 'linkedin'
			url: user.services.linkedin.pictureUrl

	if user.emails?.length > 0
		for email in user.emails when email.verified is true
			avatars.push
				service: 'gravatar'
				url: Gravatar.imageUrl(email.address, {default: '404', size: 200, secure: true})

		for email in user.emails when email.verified isnt true
			avatars.push
				service: 'gravatar'
				url: Gravatar.imageUrl(email.address, {default: '404', size: 200, secure: true})

	validAvatars = {}
	for avatar in avatars
		try
			result = HTTP.get avatar.url, npmRequestOptions: {encoding: 'binary'}
			if result.statusCode is 200
				blob = "data:#{result.headers['content-type']};base64,"
				blob += Buffer(result.content, 'binary').toString('base64')
				avatar.blob = blob
				avatar.contentType = result.headers['content-type']
				validAvatars[avatar.service] = avatar
		catch e
			# ...

	return validAvatars

Meteor.methods
	getAvatarSuggestion: ->
		if not Meteor.userId()
			throw new Meteor.Error 203, '[methods] getAvatarSuggestion -> Usuário não logado'

		user = Meteor.user()

		getAvatarSuggestionForUser user


# gitlab

An implementation of the GitLab OAuth2 flow. It works with your own private GitLab server instance.

This software is supplied courtesy of the [Rocket.Chat](https://rocket.chat/) open source communications platform.

See the [project page](https://www.meteor.com/accounts) on Meteor Accounts for more details.


Meteor.startup ->
	cdnPrefix = RocketChat.settings.get 'CDN_PREFIX'
	if cdnPrefix?
		WebAppInternals.setBundledJsCssPrefix cdnPrefix


keep keep keep keep replace keep keep keep keep keep

URL = Npm.require('url')
http = Npm.require('http')
https = Npm.require('https')
querystring = Npm.require('querystring')

OEmbed =
	cache: new Meteor.Collection 'rocketchat_oembed_cache'

getUrlContent = (urlObj, redirectCount = 5, callback) ->
	if _.isString(urlObj)
		urlObj = URL.parse urlObj

	opts =
		method: 'GET'
		port: urlObj.port
		hostname: urlObj.hostname
		path: urlObj.path
		rejectUnauthorized: false

	httpOrHttps = if urlObj.protocol is 'https:' then https else http

	parsedUrl = _.pick urlObj, ['host', 'hash', 'pathname', 'protocol', 'port', 'query']

	request = httpOrHttps.request opts, (response) ->
		if response.statusCode is 301 and response.headers.location?
			request.abort()
			console.log response.headers.location

			if redirectCount <= 0
				return callback null, {parsedUrl: parsedUrl}

			getUrlContent response.headers.location, --redirectCount, callback
			return

		if response.statusCode isnt 200
			return callback null, {parsedUrl: parsedUrl}

		str = ''
		response.on 'data', (chunk) ->
			str += chunk
			if str.length > 250000
				request.abort()

		response.on 'end', ->
			callback null, {
				headers: response.headers
				body: str
				parsedUrl: parsedUrl
			}

		response.on 'error', (error) ->
			callback null, {
				error: error
				parsedUrl: parsedUrl
			}

	request.on 'error', (error) ->
		callback null, {
			error: error
			parsedUrl: parsedUrl
		}

	request.end()

OEmbed.getUrlMeta = (url, withFragment) ->
	getUrlContentSync = Meteor.wrapAsync getUrlContent

	urlObj = URL.parse url

	if withFragment?
		queryStringObj = querystring.parse urlObj.query
		queryStringObj._escaped_fragment_ = ''
		urlObj.query = querystring.stringify queryStringObj

		path = urlObj.pathname
		if urlObj.query?
			path += '?' + urlObj.query

		urlObj.path = path

	content = getUrlContentSync urlObj, 5

	metas = undefined

	if content?.body?
		metas = {}
		content.body.replace /<title>(.+)<\/title>/gmi, (meta, title) ->
			metas.pageTitle = title

		content.body.replace /<meta[^>]*(?:name|property)=[']([^']*)['][^>]*content=[']([^']*)['][^>]*>/gmi, (meta, name, value) ->
			metas[changeCase.camelCase(name)] = value

		content.body.replace /<meta[^>]*(?:name|property)=["]([^"]*)["][^>]*content=["]([^"]*)["][^>]*>/gmi, (meta, name, value) ->
			metas[changeCase.camelCase(name)] = value

		content.body.replace /<meta[^>]*content=[']([^']*)['][^>]*(?:name|property)=[']([^']*)['][^>]*>/gmi, (meta, value, name) ->
			metas[changeCase.camelCase(name)] = value

		content.body.replace /<meta[^>]*content=["]([^"]*)["][^>]*(?:name|property)=["]([^"]*)["][^>]*>/gmi, (meta, value, name) ->
			metas[changeCase.camelCase(name)] = value

		if metas.fragment is '!' and not withFragment?
			return OEmbed.getUrlMeta url, true

	headers = undefined

	if content?.headers?
		headers = {}
		for header, value of content.headers
			headers[changeCase.camelCase(header)] = value

	return {
		meta: metas
		headers: headers
		parsedUrl: content.parsedUrl
	}

OEmbed.getUrlMetaWithCache = (url, withFragment) ->
	cache = OEmbed.cache.findOne {_id: url}
	if cache?
		return cache.data

	data = OEmbed.getUrlMeta url, withFragment

	if data?
		OEmbed.cache.insert {_id: url, data: data, updatedAt: new Date}

		return data

	return

getRelevantHeaders = (headersObj) ->
	headers = {}
	for key, value of headersObj
		if key.toLowerCase() in ['contenttype', 'contentlength'] and value?.trim() isnt ''
			headers[key] = value

	if Object.keys(headers).length > 0
		return headers
	return

getRelevantMetaTags = (metaObj) ->
	tags = {}
	for key, value of metaObj
		if /^(og|fb|twitter).+|description|title|pageTitle$/.test(key.toLowerCase()) and value?.trim() isnt ''
			tags[key] = value

	if Object.keys(tags).length > 0
		return tags
	return

OEmbed.RocketUrlParser = (message) ->
	if Array.isArray message.urls
		changed = false
		for item in message.urls
			data = OEmbed.getUrlMetaWithCache item.url

			if data?
				if data.meta?
					item.meta = getRelevantMetaTags data.meta

				if data.headers?
					item.headers = getRelevantHeaders data.headers

				item.parsedUrl = data.parsedUrl
				changed = true

		if changed is true
			ChatMessage.update {_id: message._id}, { $set: { urls: message.urls } }

	return message

if RocketChat.settings.get 'API_Embed'
	RocketChat.callbacks.add 'afterSaveMessage', OEmbed.RocketUrlParser, RocketChat.callbacks.priority.LOW


# Request Gitlab credentials for the user
# @param options {optional}
# @param credentialRequestCompleteCallback {Function} Callback function to call on
#   completion. Takes one argument, credentialToken on success, or Error on
#   error.
class CustomOAuth
	constructor: (@name, options) ->
		if not Match.test @name, String
			return throw new Meteor.Error 'CustomOAuth: Name is required and must be String'

		if not Match.test options, Object
			return throw new Meteor.Error 'CustomOAuth: Options is required and must be Object'

		if not Match.test options.serverURL, String
			return throw new Meteor.Error 'CustomOAuth: Options.serverURL is required and must be String'

		if not Match.test options.authorizePath, String
			options.authorizePath = '/oauth/authorize'

		@serverURL = options.serverURL

		if not /^https?:\/\/.+/.test options.authorizePath
			options.authorizePath = @serverURL + options.authorizePath

		@authorizePath = options.authorizePath

		Accounts.oauth.registerService @name

		@configureLogin()

	configureLogin: ->
		self = @
		loginWithService = "loginWith" + s.capitalize(@name)

		Meteor[loginWithService] = (options, callback) ->
			# support a callback without options
			if not callback and typeof options is "function"
				callback = options
				options = null

			credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback)
			self.requestCredential(options, credentialRequestCompleteCallback)

	requestCredential: (options, credentialRequestCompleteCallback) ->
		# support both (options, callback) and (callback).
		if not credentialRequestCompleteCallback and typeof options is 'function'
			credentialRequestCompleteCallback = options
			options = {}

		config = ServiceConfiguration.configurations.findOne service: @name
		if not config
			credentialRequestCompleteCallback? new ServiceConfiguration.ConfigError()
			return

		credentialToken = Random.secret()
		loginStyle = OAuth._loginStyle @name, config, options

		loginUrl = @authorizePath +
			'?client_id=' + config.clientId +
			'&redirect_uri=' + OAuth._redirectUri(@name, config) +
			'&response_type=code' +
			'&state=' + OAuth._stateParam(loginStyle, credentialToken, options.redirectUrl)

		OAuth.launchLogin
			loginService: @name
			loginStyle: loginStyle
			loginUrl: loginUrl
			credentialRequestCompleteCallback: credentialRequestCompleteCallback
			credentialToken: credentialToken
			popupOptions:
				width: 900
				height: 450


Name

Erstellen Sie einen neuen Direktnachrichten-Raum

Erstelle einen neuen öffentlichen Channel

Beim Löschen eines Benutzers, werden alle seine Nachrichten ebenfalls gelöscht. Dies kann nicht rückgängig gemacht werden.

Ein Kanal mit dem Namen, '%s', existiert

Eine private Gruppe mit dem Namen, '%s', existiert

Folge uns in sozialen Netzen, fork uns auf github und teile deine Gedanken über die rocket.chat app auf unserem trello board.

Sie haben keine öffentliche Email-Adresse in Ihrem Github Account

Erstelle deinen eigenen web chat. Entwickelt mit Meteor.com, der Rocket.Chat ist eine tolle Lösung für Entwickler, die schnell und einfach einen eigene Chat Plattform aufbauen wollen.

%s ist kein zulässiger Raumname.
 Benutze nur Buchstaben, Nummern und Bindestriche.

Oder melden Sie sich direkt mit folgenden Accounts an

Nachricht

Sie sind kein Mitglied einer privaten Gruppe.

Sie haben keine Berechtigung diesen Raum zu betreten

Es wurde keine Kanal mit dem Namen <strong>"%s"</strong> gefunden!

Es wurde keine private Gruppe mit dem Namen <strong>"%s"</strong> gefunden!

Es wurde kein Benutzer mit dem Namen <strong>"%s"</strong> gefunden!

Bitte warten, Statistiken werden generiert.

Raumname geändert zu: __room_name__ von __user_by__

Wählen Sie einen Service um sich einzuloggen, ein Bild auszuwählen oder eines direkt von Ihrem Computer hochzuladen

Zeige <b>__total_online__</b> von __total__ users

Durchschnittliche Benutzer in Privater Gruppe

Benutze das Seitenmenü um deine Chats zu öffnen

Benutzer <em>__user_added__</em> wurde hinzugefügt.

Benutzer __user_added__ hinzugefügt von __user_by__.

Benutzer <em>__user_left__</em> ist gegangen.

Benutzer __user_removed__ entfernt von __user_by__.

Der Benutzername wird dazu benutzt Sie in Nachtichten zu markieren.

%s ist kein zulässiger Username.
 Benutze nur Buchstaben, Nummern, Punkte oder Bindestriche.

<strong>%s</strong> wird schon verwendet :(

Bevor Sie sich einloggen können, muss das Konto von einem Administrator manuell aktiviert werden.

Wir haben Ihnen eine Anleitung zum Zurücksetzen des Passworts an Ihre Email-Adresse gesendet. Wenn Sie keine Email erhalten haben versuchen Sie es bitte noch einmal.

Wir haben Ihnen eine Bestätigungs Email gesendet. Wenn Sie keine Email erhalten haben versuchen Sie es bitte noch einmal.

Sie sind m Vorschau-Modus des Kanals #<strong>__room_name__</strong>

Sie können es nicht wieder rückgängig machen!


	<title>Rocket.Chat</title>
	<meta charset="utf-8" />
	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
	<meta http-equiv="expires" content="-1" />
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="fragment" content="!" />
	<meta name="distribution" content="global" />
	<meta name="rating" content="general" />
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
	<meta name="msapplication-TileColor" content="#04436a">
	<meta name="msapplication-TileImage" content="/images/logo/mstile-144x144.png?v=3">
	<meta name="msapplication-config" content="/images/logo/browserconfig.xml?v=3">
	<meta name="theme-color" content="#04436a">
	<link rel="manifest" href="/images/logo/manifest.json?v=3">
	<link rel="icon" sizes="any" type="image/svg+xml" href="/images/logo/icon.svg?v=3">
	<link rel="icon" sizes="256x256" type="image/png" href="/images/logo/favicon-256x256.png?v=3">
	<link rel="icon" sizes="192x192" type="image/png" href="/images/logo/android-chrome-192x192.png?v=3">
	<link rel="icon" sizes="128x128" type="image/png" href="/images/logo/favicon-128x128.png?v=3">
	<link rel="icon" sizes="96x96" type="image/png" href="/images/logo/favicon-96x96.png?v=3">
	<link rel="icon" sizes="64x64" type="image/png" href="/images/logo/favicon-64x64.png?v=3">

	<link rel="shortcut icon" sizes="16x16 32x32 48x48" type="image/x-icon" href="/favicon.ico?v=3" />
	<link rel="apple-touch-icon" sizes="57x57" href="/images/logo/apple-touch-icon-57x57.png?v=3">
	<link rel="apple-touch-icon" sizes="60x60" href="/images/logo/apple-touch-icon-60x60.png?v=3">
	<link rel="apple-touch-icon" sizes="72x72" href="/images/logo/apple-touch-icon-72x72.png?v=3">
	<link rel="apple-touch-icon" sizes="76x76" href="/images/logo/apple-touch-icon-76x76.png?v=3">
	<link rel="apple-touch-icon" sizes="114x114" href="/images/logo/apple-touch-icon-114x114.png?v=3">
	<link rel="apple-touch-icon" sizes="120x120" href="/images/logo/apple-touch-icon-120x120.png?v=3">
	<link rel="apple-touch-icon" sizes="144x144" href="/images/logo/apple-touch-icon-144x144.png?v=3">
	<link rel="apple-touch-icon" sizes="152x152" href="/images/logo/apple-touch-icon-152x152.png?v=3">
	<link rel="apple-touch-icon" sizes="180x180" href="/images/logo/apple-touch-icon-180x180.png?v=3">

<template name="main">
	{{#if subsReady}}
		{{#unless logged}}
			{{> loginLayout}}
		{{else}}
			{{#unless hasUsername}}
				{{> username}}
			{{else}}
				{{> spotlight}}
				{{> mobileMessageMenu}}

						{{> status}}

						{{> Template.dynamic template=center}}

					{{> sideNav }}

				{{> audioNotification }}
			{{/unless}}
		{{/unless}}
	{{/if}}
</template>


import { MessageTypes } from '/app/ui-utils';

import { MessageTypes } from '../../ui-utils/client';

import dns from 'dns';
import { Meteor } from 'meteor/meteor';
import { FederationDNSCache } from '/app/models';

import { logger } from './logger';
import peerHTTP from './peerHTTP';
import { updateStatus } from './settingsUpdater';

const dnsResolveSRV = Meteor.wrapAsync(dns.resolveSrv);
const dnsResolveTXT = Meteor.wrapAsync(dns.resolveTxt);

class PeerDNS {
	constructor() {
		this.config = {};
	}

	setConfig(config) {
		// General
		this.config = config;

		// Setup HubPeer
		const { hub: { url } } = config;
		this.HubPeer = { url };
	}

	log(message) {
		logger.dns.info(message);
	}

	// ########
	//
	// Register
	//
	// ########
	register(peerConfig) {
		const { uniqueId, domain, url, public_key, cloud_token } = peerConfig;

		this.log(`Registering peer with domain ${ domain }...`);

		let headers;
		if (cloud_token && cloud_token !== '') {
			headers = { Authorization: `Bearer ${ cloud_token }` };
		}

		// Attempt to register peer
		try {
			peerHTTP.request(this.HubPeer, 'POST', '/api/v1/peers', { uniqueId, domain, url, public_key }, { total: 5, stepSize: 1000, tryToUpdateDNS: false }, headers);

			this.log('Peer registered!');

			updateStatus('Running, registered to Hub');

			return true;
		} catch (err) {
			this.log(err);

			this.log('Could not register peer');

			return false;
		}
	}

	// #############
	//
	// Peer Handling
	//
	// #############
	searchPeer(domain) {
		this.log(`searchPeer: ${ domain }`);

		let peer = FederationDNSCache.findOneByDomain(domain);

		// Try to lookup at the DNS Cache
		if (!peer) {
			this.updatePeerDNS(domain);

			peer = FederationDNSCache.findOneByDomain(domain);
		}

		return peer;
	}

	getPeerUsingDNS(domain) {
		this.log(`getPeerUsingDNS: ${ domain }`);

		// Try searching by DNS first
		const srvEntries = dnsResolveSRV(`_rocketchat._tcp.${ domain }`);

		const [srvEntry] = srvEntries;

		// Get the public key from the TXT record
		const txtRecords = dnsResolveTXT(domain);

		let publicKey;

		for (const txtRecord of txtRecords) {
			const joinedTxtRecord = txtRecord.join('');

			if (joinedTxtRecord.indexOf('rocketchat-public-key=') === 0) {
				publicKey = joinedTxtRecord;
				break;
			}
		}

		if (!publicKey) {
			throw new Meteor.Error('ENOTFOUND', 'Could not find public key entry on TXT records');
		}

		publicKey = publicKey.replace('rocketchat-public-key=', '');

		const protocol = srvEntry.name === 'localhost' ? 'http' : 'https';

		return {
			domain,
			url: `${ protocol }://${ srvEntry.name }:${ srvEntry.port }`,
			public_key: publicKey,
		};
	}

	getPeerUsingHub(domain) {
		this.log(`getPeerUsingHub: ${ domain }`);

		// If there is no DNS entry for that, get from the Hub
		const { data: { peer } } = peerHTTP.simpleRequest(this.HubPeer, 'GET', `/api/v1/peers?search=${ domain }`);

		return peer;
	}

	// ##############
	//
	// DNS Management
	//
	// ##############
	updatePeerDNS(domain) {
		this.log(`updatePeerDNS: ${ domain }`);

		let peer;

		try {
			peer = this.getPeerUsingDNS(domain);
		} catch (err) {
			if (err.code !== 'ENOTFOUND') {
				this.log(err);

				throw new Error(`Error trying to fetch SRV DNS entries for ${ domain }`);
			}

			peer = this.getPeerUsingHub(domain);
		}

		this.updateDNSCache.call(this, peer);

		return peer;
	}

	updateDNSEntry(peer) {
		this.log('updateDNSEntry');

		const { domain } = peer;

		delete peer._id;

		// Make sure public_key has no line breaks
		peer.public_key = peer.public_key.replace(/\n|\r/g, '');

		return FederationDNSCache.upsert({ domain }, peer);
	}

	updateDNSCache(peers) {
		this.log('updateDNSCache');

		peers = Array.isArray(peers) ? peers : [peers];

		for (const peer of peers) {
			this.updateDNSEntry.call(this, peer);
		}
	}
}

export default new PeerDNS();


import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { hasPermission } from '/app/authorization';
import { createRoom } from '../functions';

Meteor.methods({
	createChannel(name, members, readOnly = false, customFields = {}, extraData = {}) {
		check(name, String);
		check(members, Match.Optional([String]));

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

		if (!hasPermission(Meteor.userId(), 'create-c')) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createChannel' });
		}
		return createRoom('c', name, Meteor.user() && Meteor.user().username, members, readOnly, { customFields, ...extraData });
	},
});


export class AppListenerBridge {
	constructor(orch) {
		this.orch = orch;
	}

	async messageEvent(inte, message) {
		const msg = this.orch.getConverters().get('messages').convertMessage(message);
		const result = await this.orch.getManager().getListenerManager().executeListener(inte, msg);

		if (typeof result === 'boolean') {
			return result;
		} else {
			return this.orch.getConverters().get('messages').convertAppMessage(result);
		}
		// try {

		// } catch (e) {
		// 	console.log(`${ e.name }: ${ e.message }`);
		// 	console.log(e.stack);
		// }
	}

	async roomEvent(inte, room) {
		const rm = this.orch.getConverters().get('rooms').convertRoom(room);
		const result = await this.orch.getManager().getListenerManager().executeListener(inte, rm);

		if (typeof result === 'boolean') {
			return result;
		} else {
			return this.orch.getConverters().get('rooms').convertAppRoom(result);
		}
		// try {

		// } catch (e) {
		// 	console.log(`${ e.name }: ${ e.message }`);
		// 	console.log(e.stack);
		// }
	}
}


import './v1/config.js';
import './v1/visitor.js';
import './v1/transcript.js';
import './v1/offlineMessage.js';
import './v1/pageVisited.js';
import './v1/agent.js';
import './v1/message.js';
import './v1/customField.js';
import './v1/room.js';
import './v1/videoCall.js';


const srvEntries = dnsResolveSRV(`_rocketchat._tcp.${ domain }`);

// Get the public key from the TXT record

const txtRecords = dnsResolveTXT(domain);

const txtRecords = dnsResolveTXT(`rocketchat-public-key.${ domain }`);


export const isFirefox = navigator.userAgent.match(/Firefox\/(\d+)\.\d/);
export const isChrome = navigator.userAgent.match(/Chrome\/(\d+)\.\d/);


import { Rooms } from '/app/models';

import { authenticated } from '../../helpers/authenticated';
import { roomPublicFields } from './settings';
import schema from '../../schemas/channels/channelByName.graphqls';

const resolver = {
	Query: {
		channelByName: authenticated((root, { name }) => {
			const query = {
				name,
				t: 'c',
			};

			return Rooms.findOne(query, {
				fields: roomPublicFields,
			});
		}),
	},
};

export {
	schema,
	resolver,
};


import { Meteor } from 'meteor/meteor';
import { hasPermission } from '/app/authorization';
import { LivechatDepartmentAgents } from '/app/models';

Meteor.publish('livechat:queue', function() {
	if (!this.userId) {
		return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
	}

	if (!hasPermission(this.userId, 'view-l-room')) {
		return this.error(new Meteor.Error('error-not-authorized', 'Not authorized', { publish: 'livechat:queue' }));
	}

	// let sort = { count: 1, sort: 1, username: 1 };
	// let onlineUsers = {};

	// let handleUsers = RocketChat.models.Users.findOnlineAgents().observeChanges({
	// 	added(id, fields) {
	// 		onlineUsers[fields.username] = 1;
	// 		// this.added('livechatQueueUser', id, fields);
	// 	},
	// 	changed(id, fields) {
	// 		onlineUsers[fields.username] = 1;
	// 		// this.changed('livechatQueueUser', id, fields);
	// 	},
	// 	removed(id) {
	// 		this.removed('livechatQueueUser', id);
	// 	}
	// });

	const self = this;

	const handleDepts = LivechatDepartmentAgents.findUsersInQueue().observeChanges({
		added(id, fields) {
			self.added('livechatQueueUser', id, fields);
		},
		changed(id, fields) {
			self.changed('livechatQueueUser', id, fields);
		},
		removed(id) {
			self.removed('livechatQueueUser', id);
		},
	});

	this.ready();

	this.onStop(() => {
		// handleUsers.stop();
		handleDepts.stop();
	});
});


// Get the first TXT record, this subdomain should have only a single record


import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { settings } from '/app/settings';
import { callbacks } from '/app/callbacks';
import { ChatRoom } from '/app/models';
import { Tracker } from 'meteor/tracker';

function trackEvent(category, action, label) {
	if (window._paq) {
		window._paq.push(['trackEvent', category, action, label]);
	}
	if (window.ga) {
		window.ga('send', 'event', category, action, label);
	}
}

if (!window._paq || window.ga) {
	// Trigger the trackPageView manually as the page views are only loaded when the loadScript.js code is executed
	FlowRouter.triggers.enter([(route) => {
		if (window._paq) {
			const http = location.protocol;
			const slashes = http.concat('//');
			const host = slashes.concat(window.location.hostname);
			window._paq.push(['setCustomUrl', host + route.path]);
			window._paq.push(['trackPageView']);
		}
		if (window.ga) {
			window.ga('send', 'pageview', route.path);
		}
	}]);

	// Login page has manual switches
	callbacks.add('loginPageStateChange', (state) => {
		trackEvent('Navigation', 'Login Page State Change', state);
	}, callbacks.priority.MEDIUM, 'analytics-login-state-change');

	// Messsages
	callbacks.add('afterSaveMessage', (message) => {
		if ((window._paq || window.ga) && settings.get('Analytics_features_messages')) {
			const room = ChatRoom.findOne({ _id: message.rid });
			trackEvent('Message', 'Send', `${ room.name } (${ room._id })`);
		}
	}, 2000, 'trackEvents');

	// Rooms
	callbacks.add('afterCreateChannel', (owner, room) => {
		if (settings.get('Analytics_features_rooms')) {
			trackEvent('Room', 'Create', `${ room.name } (${ room._id })`);
		}
	}, callbacks.priority.MEDIUM, 'analytics-after-create-channel');

	callbacks.add('roomNameChanged', (room) => {
		if (settings.get('Analytics_features_rooms')) {
			trackEvent('Room', 'Changed Name', `${ room.name } (${ room._id })`);
		}
	}, callbacks.priority.MEDIUM, 'analytics-room-name-changed');

	callbacks.add('roomTopicChanged', (room) => {
		if (settings.get('Analytics_features_rooms')) {
			trackEvent('Room', 'Changed Topic', `${ room.name } (${ room._id })`);
		}
	}, callbacks.priority.MEDIUM, 'analytics-room-topic-changed');

	callbacks.add('roomAnnouncementChanged', (room) => {
		if (settings.get('Analytics_features_rooms')) {
			trackEvent('Room', 'Changed Announcement', `${ room.name } (${ room._id })`);
		}
	}, callbacks.priority.MEDIUM, 'analytics-room-announcement-changed');

	callbacks.add('roomTypeChanged', (room) => {
		if (settings.get('Analytics_features_rooms')) {
			trackEvent('Room', 'Changed Room Type', `${ room.name } (${ room._id })`);
		}
	}, callbacks.priority.MEDIUM, 'analytics-room-type-changed');

	callbacks.add('archiveRoom', (room) => {
		if (settings.get('Analytics_features_rooms')) {
			trackEvent('Room', 'Archived', `${ room.name } (${ room._id })`);
		}
	}, callbacks.priority.MEDIUM, 'analytics-archive-room');

	callbacks.add('unarchiveRoom', (room) => {
		if (settings.get('Analytics_features_rooms')) {
			trackEvent('Room', 'Unarchived', `${ room.name } (${ room._id })`);
		}
	}, callbacks.priority.MEDIUM, 'analytics-unarchive-room');

	// Users
	// Track logins and associate user ids with piwik
	(() => {
		let oldUserId = null;

		Tracker.autorun(() => {
			const newUserId = Meteor.userId();
			if (oldUserId === null && newUserId) {
				if (window._paq && settings.get('Analytics_features_users')) {
					trackEvent('User', 'Login', newUserId);
					window._paq.push(['setUserId', newUserId]);
				}
			} else if (newUserId === null && oldUserId) {
				if (window._paq && settings.get('Analytics_features_users')) {
					trackEvent('User', 'Logout', oldUserId);
				}
			}
			oldUserId = Meteor.userId();
		});
	})();

	callbacks.add('userRegistered', () => {
		if (settings.get('Analytics_features_users')) {
			trackEvent('User', 'Registered');
		}
	}, callbacks.priority.MEDIUM, 'piwik-user-resitered');

	callbacks.add('usernameSet', () => {
		if (settings.get('Analytics_features_users')) {
			trackEvent('User', 'Username Set');
		}
	}, callbacks.priority.MEDIUM, 'piweik-username-set');

	callbacks.add('userPasswordReset', () => {
		if (settings.get('Analytics_features_users')) {
			trackEvent('User', 'Reset Password');
		}
	}, callbacks.priority.MEDIUM, 'piwik-user-password-reset');

	callbacks.add('userConfirmationEmailRequested', () => {
		if (settings.get('Analytics_features_users')) {
			trackEvent('User', 'Confirmation Email Requested');
		}
	}, callbacks.priority.MEDIUM, 'piwik-user-confirmation-email-requested');

	callbacks.add('userForgotPasswordEmailRequested', () => {
		if (settings.get('Analytics_features_users')) {
			trackEvent('User', 'Forgot Password Email Requested');
		}
	}, callbacks.priority.MEDIUM, 'piwik-user-forgot-password-email-requested');

	callbacks.add('userStatusManuallySet', (status) => {
		if (settings.get('Analytics_features_users')) {
			trackEvent('User', 'Status Manually Changed', status);
		}
	}, callbacks.priority.MEDIUM, 'analytics-user-status-manually-set');

	callbacks.add('userAvatarSet', (service) => {
		if (settings.get('Analytics_features_users')) {
			trackEvent('User', 'Avatar Changed', service);
		}
	}, callbacks.priority.MEDIUM, 'analytics-user-avatar-set');
}


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

API.helperMethods.set('deprecationWarning', function _deprecationWarning({ endpoint, versionWillBeRemove, response }) {
	const warningMessage = `The endpoint "${ endpoint }" is deprecated and will be removed after version ${ versionWillBeRemove }`;
	console.warn(warningMessage);
	if (process.env.NODE_ENV === 'development') {
		return {
			warning: warningMessage,
			...response,
		};
	}

	return response;
});


import { Meteor } from 'meteor/meteor';

if (Meteor.isClient) {
	module.exports = require('./client/index.js');
}
if (Meteor.isServer) {
	module.exports = require('./server/index.js');
}


const joinedTxtRecord = txtRecord.join('');

if (joinedTxtRecord.indexOf('rocketchat-public-key=') === 0) {

throw new Meteor.Error('ENOTFOUND', 'Could not find public key entry on TXT records');

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

// If there is no record, skip
if (!txtRecord) {


import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { CachedCollectionManager } from '/app/ui-cached-collection';
import _ from 'underscore';

class CustomSoundsClass {
	constructor() {
		this.list = new ReactiveVar({});
		this.add({ _id: 'beep', name: 'Beep', extension: 'mp3', src: 'sounds/beep.mp3' });
		this.add({ _id: 'chelle', name: 'Chelle', extension: 'mp3', src: 'sounds/chelle.mp3' });
		this.add({ _id: 'ding', name: 'Ding', extension: 'mp3', src: 'sounds/ding.mp3' });
		this.add({ _id: 'droplet', name: 'Droplet', extension: 'mp3', src: 'sounds/droplet.mp3' });
		this.add({ _id: 'highbell', name: 'Highbell', extension: 'mp3', src: 'sounds/highbell.mp3' });
		this.add({ _id: 'seasons', name: 'Seasons', extension: 'mp3', src: 'sounds/seasons.mp3' });
	}

	add(sound) {
		if (!sound.src) {
			sound.src = this.getURL(sound);
		}
		const audio = $('<audio />', { id: sound._id, preload: true }).append(
			$('<source />', { src: sound.src })
		);
		const list = this.list.get();
		list[sound._id] = sound;
		this.list.set(list);
		$('body').append(audio);
	}

	remove(sound) {
		const list = this.list.get();
		delete list[sound._id];
		this.list.set(list);
		$(`#${ sound._id }`).remove();
	}

	update(sound) {
		const audio = $(`#${ sound._id }`);
		if (audio && audio[0]) {
			const list = this.list.get();
			list[sound._id] = sound;
			this.list.set(list);
			$('source', audio).attr('src', this.getURL(sound));
			audio[0].load();
		} else {
			this.add(sound);
		}
	}

	getURL(sound) {
		const path = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
		return `${ path }/custom-sounds/${ sound._id }.${ sound.extension }?_dc=${ sound.random || 0 }`;
	}

	getList() {
		const list = Object.values(this.list.get());
		return _.sortBy(list, 'name');
	}
}
export const CustomSounds = new CustomSoundsClass();

Meteor.startup(() =>
	CachedCollectionManager.onLogin(() => {
		Meteor.call('listCustomSounds', (error, result) => {
			for (const sound of result) {
				CustomSounds.add(sound);
			}
		});
	})
);


<template name="fxOsInstallPrompt">

{{_ "Install_FxOs"}}

{{_ "Install_FxOs_follow_instructions"}}

</template>

<template name="fxOsInstallDone">

{{_ "Install_FxOs"}}

{{_ "Install_FxOs_done"}}

</template>

<template name="fxOsInstallError">

{{_ "Install_FxOs"}}

{{_ "Install_FxOs_error"}}

{{installError}}

</template>


import { Mongo } from 'meteor/mongo';

export const ChatMessage = new Mongo.Collection(null);

ChatMessage.setReactions = function(messageId, reactions) {
	return this.update({ _id: messageId }, { $set: { reactions } });
};

ChatMessage.unsetReactions = function(messageId) {
	return this.update({ _id: messageId }, { $unset: { reactions: 1 } });
};


publicKey = publicKey.replace('rocketchat-public-key=', '');

const protocol = srvEntry.name === 'localhost' ? 'http' : 'https';

keep keep keep replace keep keep keep keep keep


# rocketchat:grant-facebook

An implementation of the Facebook OAuth flow.


import { Meteor } from 'meteor/meteor';
import { FlowRouter } from 'meteor/kadira:flow-router';

function updateSandstormMetaData(msg) {
	return window.parent.postMessage(msg, '*');
}

if (Meteor.settings.public.sandstorm) {
	// Set the path of the parent frame when the grain's path changes.
	// See https://docs.sandstorm.io/en/latest/developing/path/

	FlowRouter.triggers.enter([({ path }) => {
		updateSandstormMetaData({ setPath: path });
	}]);
}


throw new Error(`Error trying to fetch SRV DNS entries for ${ domain }`);

keep keep keep keep replace keep keep keep

if (['ENODATA', 'ENOTFOUND'].indexOf(err.code) === -1) {


import { Meteor } from 'meteor/meteor';
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { hasPermission } from '/app/authorization';
import AutoTranslate from '../autotranslate';

Meteor.methods({
	'autoTranslate.getSupportedLanguages'(targetLanguage) {
		if (!hasPermission(Meteor.userId(), 'auto-translate')) {
			throw new Meteor.Error('error-action-not-allowed', 'Auto-Translate is not allowed', { method: 'autoTranslate.saveSettings' });
		}

		return AutoTranslate.getSupportedLanguages(targetLanguage);
	},
});

DDPRateLimiter.addRule({
	type: 'method',
	name: 'autoTranslate.getSupportedLanguages',
	userId(/* userId*/) {
		return true;
	},
}, 5, 60000);


/* eslint-env mocha */
import 'babel-polyfill';
import assert from 'assert';

import MentionsServer from '../server/Mentions';

let mention;

beforeEach(function() {
	mention = new MentionsServer({
		pattern: '[0-9a-zA-Z-_.]+',
		messageMaxAll: () => 4, // || RocketChat.settings.get('Message_MaxAll')
		getUsers: (usernames) =>
			[{
				_id: 1,
				username: 'rocket.cat',
			}, {
				_id: 2,
				username: 'jon',
			}].filter((user) => usernames.includes(user.username)), // Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch();
		getChannels(channels) {
			return [{
				_id: 1,
				name: 'general',
			}].filter((channel) => channels.includes(channel.name));
			// return RocketChat.models.Rooms.find({ name: {$in: _.unique(channels)}, t: 'c'	}, { fields: {_id: 1, name: 1 }}).fetch();
		},
		getUser: (userId) => ({ _id: userId, language: 'en' }),
		getTotalChannelMembers: (/* rid*/) => 2,
	});
});

describe('Mention Server', () => {
	describe('getUsersByMentions', () => {
		describe('for @all but the number of users is greater than messageMaxAll', () => {
			beforeEach(() => {
				mention.getTotalChannelMembers = () => 5;
			});
			it('should return nothing', () => {
				const message = {
					msg: '@all',
				};
				const expected = [];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});
		});
		describe('for one user', () => {
			beforeEach(() => {
				mention.getChannel = () =>
					({
						usernames:[{
							_id: 1,
							username: 'rocket.cat',
						}, {
							_id: 2,
							username: 'jon',
						}],
					})
					// Meteor.users.find({ username: {$in: _.unique(usernames)}}, { fields: {_id: true, username: true }}).fetch();
				;
			});
			it('should return "all"', () => {
				const message = {
					msg: '@all',
				};
				const expected = [{
					_id: 'all',
					username: 'all',
				}];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});
			it('should return "here"', () => {
				const message = {
					msg: '@here',
				};
				const expected = [{
					_id: 'here',
					username: 'here',
				}];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});
			it('should return "rocket.cat"', () => {
				const message = {
					msg: '@rocket.cat',
				};
				const expected = [{
					_id: 1,
					username: 'rocket.cat',
				}];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});
		});
		describe('for two user', () => {
			it('should return "all and here"', () => {
				const message = {
					msg: '@all @here',
				};
				const expected = [{
					_id: 'all',
					username: 'all',
				}, {
					_id: 'here',
					username: 'here',
				}];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});
			it('should return "here and rocket.cat"', () => {
				const message = {
					msg: '@here @rocket.cat',
				};
				const expected = [{
					_id: 'here',
					username: 'here',
				}, {
					_id: 1,
					username: 'rocket.cat',
				}];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});

			it('should return "here, rocket.cat, jon"', () => {
				const message = {
					msg: '@here @rocket.cat @jon',
				};
				const expected = [{
					_id: 'here',
					username: 'here',
				}, {
					_id: 1,
					username: 'rocket.cat',
				}, {
					_id: 2,
					username: 'jon',
				}];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});
		});

		describe('for an unknow user', () => {
			it('should return "nothing"', () => {
				const message = {
					msg: '@unknow',
				};
				const expected = [];
				const result = mention.getUsersByMentions(message);
				assert.deepEqual(expected, result);
			});
		});

	});
	describe('getChannelbyMentions', () => {
		it('should return the channel "general"', () => {
			const message = {
				msg: '#general',
			};
			const expected = [{
				_id: 1,
				name: 'general',
			}];
			const result = mention.getChannelbyMentions(message);
			assert.deepEqual(result, expected);
		});
		it('should return nothing"', () => {
			const message = {
				msg: '#unknow',
			};
			const expected = [];
			const result = mention.getChannelbyMentions(message);
			assert.deepEqual(result, expected);
		});
	});
	describe('execute', () => {
		it('should return the channel "general"', () => {
			const message = {
				msg: '#general',
			};
			const expected = [{
				_id: 1,
				name: 'general',
			}];
			const result = mention.getChannelbyMentions(message);
			assert.deepEqual(result, expected);
		});
		it('should return nothing"', () => {
			const message = {
				msg: '#unknow',
			};
			const expected = {
				msg: '#unknow',
				mentions: [],
				channels: [],
			};
			const result = mention.execute(message);
			assert.deepEqual(result, expected);
		});
	});

	describe('getters and setters', () => {
		describe('messageMaxAll', () => {
			const mention = new MentionsServer({});
			describe('constant', () => {
				it('should return the informed value', () => {
					mention.messageMaxAll = 4;
					assert.deepEqual(mention.messageMaxAll, 4);
				});
			});
			describe('function', () => {
				it('should return the informed value', () => {
					mention.messageMaxAll = () => 4;
					assert.deepEqual(mention.messageMaxAll, 4);
				});
			});
		});
		describe('getUsers', () => {
			const mention = new MentionsServer({});
			describe('constant', () => {
				it('should return the informed value', () => {
					mention.getUsers = 4;
					assert.deepEqual(mention.getUsers(), 4);
				});
			});
			describe('function', () => {
				it('should return the informed value', () => {
					mention.getUsers = () => 4;
					assert.deepEqual(mention.getUsers(), 4);
				});
			});
		});
		describe('getChannels', () => {
			const mention = new MentionsServer({});
			describe('constant', () => {
				it('should return the informed value', () => {
					mention.getChannels = 4;
					assert.deepEqual(mention.getChannels(), 4);
				});
			});
			describe('function', () => {
				it('should return the informed value', () => {
					mention.getChannels = () => 4;
					assert.deepEqual(mention.getChannels(), 4);
				});
			});
		});
		describe('getChannel', () => {
			const mention = new MentionsServer({});
			describe('constant', () => {
				it('should return the informed value', () => {
					mention.getChannel = true;
					assert.deepEqual(mention.getChannel(), true);
				});
			});
			describe('function', () => {
				it('should return the informed value', () => {
					mention.getChannel = () => true;
					assert.deepEqual(mention.getChannel(), true);
				});
			});
		});
	});
});


const ActionsToolbarDropdown = ({ chatContext, isRecording, rid, tmid, actions }: ActionsToolbarDropdownProps) => {

keep keep keep replace keep keep keep keep

const ActionsToolbarDropdown = ({ chatContext, isRecording, rid, tmid, actions, ...props }: ActionsToolbarDropdownProps) => {

import type { IRoom, IMessage } from '@rocket.chat/core-typings';
import React, { memo } from 'react';

import ActionsToolbarDropdown from './ActionsToolbarDropdown';
import AudioMessageAction from './actions/AudioMessageAction';
import FileUploadAction from './actions/FileUploadAction';
import VideoMessageAction from './actions/VideoMessageAction';

type MessageBoxActionsToolbarProps = {
	variant: 'small' | 'large';
	isRecording: boolean;
	typing: boolean;
	canSend: boolean;
	rid: IRoom['_id'];
	tmid?: IMessage['_id'];
	isMicrophoneDenied?: boolean;
};

const MessageBoxActionsToolbar = ({
	variant = 'large',
	isRecording,
	typing,
	canSend,
	rid,
	tmid,
	isMicrophoneDenied,
}: MessageBoxActionsToolbarProps) => {
	const actions = [
		<VideoMessageAction key='video' collapsed={variant === 'small'} disabled={!canSend || typing || isRecording} />,
		<AudioMessageAction
			key='audio'
			disabled={!canSend || typing || isRecording || isMicrophoneDenied}
			isMicrophoneDenied={isMicrophoneDenied}
		/>,
		<FileUploadAction key='file' collapsed={variant === 'small'} disabled={!canSend || isRecording} />,
	];

	let featuredAction;
	if (variant === 'small') {
		featuredAction = actions.splice(1, 1);
	}

	return (
		<>
			{variant !== 'small' && actions}
			{variant === 'small' && featuredAction}
			<ActionsToolbarDropdown {...(variant === 'small' && { actions })} isRecording={isRecording} rid={rid} tmid={tmid} />
		</>
	);
};

export default memo(MessageBoxActionsToolbar);


/* eslint-disable @typescript-eslint/no-var-requires */
import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api';
import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission';
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem';
import type { AppScreenshot, AppRequestFilter, Serialized, AppRequestsStats, PaginatedAppRequests } from '@rocket.chat/core-typings';
import { Meteor } from 'meteor/meteor';

import { hasAtLeastOnePermission } from '../../../app/authorization/client';
import { CachedCollectionManager } from '../../../app/ui-cached-collection/client';
import { APIClient } from '../../../app/utils/client';
import { dispatchToastMessage } from '../../../client/lib/toast';
import type { App } from '../../../client/views/marketplace/types';
import type {
	// IAppFromMarketplace,
	IAppLanguage,
	IAppExternalURL,
	ICategory,
	// IAppSynced,
	// IAppScreenshots,
	// IScreenshot,
} from './@types/IOrchestrator';
import { RealAppsEngineUIHost } from './RealAppsEngineUIHost';
import { AppWebsocketReceiver } from './communication';
import { handleI18nResources } from './i18n';

class AppClientOrchestrator {
	private _appClientUIHost: RealAppsEngineUIHost;

	private _manager: AppClientManager;

	private isLoaded: boolean;

	private ws: AppWebsocketReceiver;

	constructor() {
		this._appClientUIHost = new RealAppsEngineUIHost();
		this._manager = new AppClientManager(this._appClientUIHost);
		this.isLoaded = false;
	}

	public async load(): Promise<void> {
		if (!this.isLoaded) {
			this.ws = new AppWebsocketReceiver();
			this.isLoaded = true;
		}

		await handleI18nResources();
	}

	public getWsListener(): AppWebsocketReceiver {
		return this.ws;
	}

	public getAppClientManager(): AppClientManager {
		return this._manager;
	}

	public handleError(error: unknown): void {
		if (hasAtLeastOnePermission(['manage-apps'])) {
			dispatchToastMessage({
				type: 'error',
				message: error,
			});
		}
	}

	public async screenshots(appId: string): Promise<AppScreenshot[]> {
		const { screenshots } = await APIClient.get(`/apps/${appId}/screenshots`);
		return screenshots;
	}

	public async getInstalledApps(): Promise<App[]> {
		const result = await APIClient.get<'/apps/installed'>('/apps/installed');

		if ('apps' in result) {
			// TODO: chapter day: multiple results are returned, but we only need one
			return result.apps as App[];
		}
		throw new Error('Invalid response from API');
	}

	public async getAppsFromMarketplace(isAdminUser?: string): Promise<App[]> {
		const result = await APIClient.get('/apps/marketplace', { isAdminUser });

		if (!Array.isArray(result)) {
			// TODO: chapter day: multiple results are returned, but we only need one
			throw new Error('Invalid response from API');
		}

		return (result as App[]).map((app: App) => {
			const { latest, appRequestStats, price, pricingPlans, purchaseType, isEnterpriseOnly, modifiedAt, bundledIn, requestedEndUser } = app;
			return {
				...latest,
				appRequestStats,
				price,
				pricingPlans,
				purchaseType,
				isEnterpriseOnly,
				modifiedAt,
				bundledIn,
				requestedEndUser,
			};
		});
	}

	public async getAppsOnBundle(bundleId: string): Promise<App[]> {
		const { apps } = await APIClient.get(`/apps/bundles/${bundleId}/apps`);
		return apps;
	}

	public async getAppsLanguages(): Promise<IAppLanguage> {
		const { apps } = await APIClient.get('/apps/languages');
		return apps;
	}

	public async getApp(appId: string): Promise<App> {
		const { app } = await APIClient.get(`/apps/${appId}` as any);
		return app;
	}

	public async getAppFromMarketplace(appId: string, version: string): Promise<{ app: App; success: boolean }> {
		const result = await APIClient.get(
			`/apps/${appId}` as any,
			{
				marketplace: 'true',
				version,
			} as any,
		);
		return result;
	}

	public async getLatestAppFromMarketplace(appId: string, version: string): Promise<App> {
		const { app } = await APIClient.get(
			`/apps/${appId}` as any,
			{
				marketplace: 'true',
				update: 'true',
				appVersion: version,
			} as any,
		);
		return app;
	}

	public async setAppSettings(appId: string, settings: ISetting[]): Promise<void> {
		await APIClient.post(`/apps/${appId}/settings`, { settings });
	}

	public async getAppApis(appId: string): Promise<IApiEndpointMetadata[]> {
		const { apis } = await APIClient.get(`/apps/${appId}/apis`);
		return apis;
	}

	public async getAppLanguages(appId: string): Promise<IAppStorageItem['languageContent']> {
		const { languages } = await APIClient.get(`/apps/${appId}/languages`);
		return languages;
	}

	public async installApp(appId: string, version: string, permissionsGranted?: IPermission[]): Promise<App> {
		const { app } = await APIClient.post('/apps', {
			appId,
			marketplace: true,
			version,
			permissionsGranted,
		});
		return app;
	}

	public async updateApp(appId: string, version: string, permissionsGranted?: IPermission[]): Promise<App> {
		const result = await APIClient.post<'/apps/:id'>(`/apps/${appId}`, {
			appId,
			marketplace: true,
			version,
			permissionsGranted,
		});

		if ('app' in result) {
			return result.app;
		}
		throw new Error('App not found');
	}

	public async setAppStatus(appId: string, status: AppStatus): Promise<string> {
		const { status: effectiveStatus } = await APIClient.post(`/apps/${appId}/status`, { status });
		return effectiveStatus;
	}

	public disableApp(appId: string): Promise<string> {
		return this.setAppStatus(appId, AppStatus.MANUALLY_ENABLED);
	}

	public async buildExternalUrl(appId: string, purchaseType: 'buy' | 'subscription' = 'buy', details = false): Promise<IAppExternalURL> {
		const result = await APIClient.get('/apps/buildExternalUrl', {
			appId,
			purchaseType,
			details: `${details}`,
		});

		if ('url' in result) {
			return result;
		}

		throw new Error('Failed to build external url');
	}

	public async buildExternalAppRequest(appId: string) {
		const result = await APIClient.get('/apps/buildExternalAppRequest', {
			appId,
		});

		if ('url' in result) {
			return result;
		}
		throw new Error('Failed to build App Request external url');
	}

	public async buildIncompatibleExternalUrl(appId: string, appVersion: string, action: string): Promise<IAppExternalURL> {
		const result = await APIClient.get('/apps/incompatibleModal', {
			appId,
			appVersion,
			action,
		});

		if ('url' in result) {
			return result;
		}

		throw new Error('Failed to build external url');
	}

	public async appRequests(
		appId: string,
		filter?: AppRequestFilter,
		sort?: string,
		limit?: number,
		offset?: number,
	): Promise<PaginatedAppRequests> {
		try {
			const response = await APIClient.get(`/apps/app-request?appId=${appId}&q=${filter}&sort=${sort}&limit=${limit}&offset=${offset}`);

			return response;
		} catch (e: unknown) {
			throw new Error('Could not get the list of app requests');
		}
	}

	public async getAppRequestsStats(): Promise<AppRequestsStats> {
		try {
			const response = await APIClient.get('/apps/app-request/stats');

			return response;
		} catch (e: unknown) {
			throw new Error('Could not get the app requests stats');
		}
	}

	public async getCategories(): Promise<Serialized<ICategory[]>> {
		const result = await APIClient.get('/apps/categories');

		if (Array.isArray(result)) {
			// TODO: chapter day: multiple results are returned, but we only need one
			return result as Serialized<ICategory>[];
		}
		throw new Error('Failed to get categories');
	}

	public getUIHost(): RealAppsEngineUIHost {
		return this._appClientUIHost;
	}
}

export const Apps = new AppClientOrchestrator();

Meteor.startup(() => {
	CachedCollectionManager.onLogin(() => {
		Apps.getAppClientManager().initialize();
		Apps.load();
	});
});


<IconButton
data-qa-id='menu-more-actions'
disabled={isRecording}
small
ref={reference}
icon='plus'
onClick={() => toggle()}
{...props}
/>

import type { IRoom } from '@rocket.chat/core-typings';
import { Dropdown, IconButton, Option, OptionTitle, OptionIcon, OptionContent } from '@rocket.chat/fuselage';
import { useTranslation, useUserRoom } from '@rocket.chat/ui-contexts';
import type { ComponentProps, ReactNode } from 'react';
import React, { useRef, Fragment } from 'react';

import { messageBox } from '../../../../../../../../app/ui-utils/client';
import type { ChatAPI } from '../../../../../../../lib/chats/ChatAPI';
import { useDropdownVisibility } from '../../../../../../../sidebar/header/hooks/useDropdownVisibility';
import CreateDiscussionAction from './actions/CreateDiscussionAction';
import ShareLocationAction from './actions/ShareLocationAction';
import WebdavAction from './actions/WebdavAction';

type ActionsToolbarDropdownProps = {
	chatContext?: ChatAPI;
	rid: IRoom['_id'];
	isRecording?: boolean;
	tmid?: string;
	actions?: ReactNode[];
};

const ActionsToolbarDropdown = ({ chatContext, isRecording, rid, tmid, actions }: ActionsToolbarDropdownProps) => {
	const t = useTranslation();
	const reference = useRef(null);
	const target = useRef(null);

	const room = useUserRoom(rid);

	const { isVisible, toggle } = useDropdownVisibility({ reference, target });

	const groups = messageBox.actions.get();
	const messageBoxActions = Object.entries(groups).map(([name, group]) => {
		const items = group.map((item) => ({
			icon: item.icon,
			name: t(item.label),
			type: 'messagebox-action',
			id: item.id,
			action: item.action,
		}));

		return {
			title: t.has(name) && t(name),
			items,
		};
	});

	return (
		<>
			<IconButton data-qa-id='menu-more-actions' disabled={isRecording} small ref={reference} icon='plus' onClick={() => toggle()} />
			{isVisible && (
				<Dropdown reference={reference} ref={target} placement='bottom-start'>
					<OptionTitle>{t('Create_new')}</OptionTitle>
					{room && <CreateDiscussionAction room={room} />}
					{actions}
					<WebdavAction chatContext={chatContext} />
					{room && <ShareLocationAction room={room} tmid={tmid} />}
					{messageBoxActions?.map((actionGroup, index) => (
						<Fragment key={index}>
							<OptionTitle>{actionGroup.title}</OptionTitle>
							{actionGroup.items.map((item) => (
								<Option key={item.id} onClick={item.action as () => void}>
									{item.icon && <OptionIcon name={item.icon as ComponentProps<typeof OptionIcon>['name']} />}
									<OptionContent>{item.name}</OptionContent>
								</Option>
							))}
						</Fragment>
					))}
				</Dropdown>
			)}
		</>
	);
};

export default ActionsToolbarDropdown;


export const addServerUrlToIndex = (file: string): string => {
	const rootUrl = (global as any).__meteor_runtime_config__.ROOT_URL.replace(/\/$/, '');
	return file.replace('<body>', `<body><script> SERVER_URL = '${rootUrl}'; </script>`);
};


import type { FC } from 'react';
import React, { useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import type { AsyncState } from '../../../../lib/asyncState/AsyncState';
import { AsyncStatePhase } from '../../../../lib/asyncState/AsyncStatePhase';
import { OmnichannelRoomIconContext } from '../context/OmnichannelRoomIconContext';
import OmnichannelRoomIcon from '../lib/OmnichannelRoomIcon';

let icons = Array.from(OmnichannelRoomIcon.icons.values());

export const OmnichannelRoomIconProvider: FC = ({ children }) => {
	const svgIcons = useSyncExternalStore(
		useCallback(
			(callback): (() => void) =>
				OmnichannelRoomIcon.on('change', () => {
					icons = Array.from(OmnichannelRoomIcon.icons.values());
					callback();
				}),
			[],
		),
		(): string[] => icons,
	);

	return (
		<OmnichannelRoomIconContext.Provider
			value={useMemo(() => {
				const extractSnapshot = (app: string, iconName: string): AsyncState<string> => {
					const icon = OmnichannelRoomIcon.get(app, iconName);

					if (icon) {
						return {
							phase: AsyncStatePhase.RESOLVED,
							value: icon,
							error: undefined,
						};
					}

					return {
						phase: AsyncStatePhase.LOADING,
						value: undefined,
						error: undefined,
					};
				};

				// We cache all the icons here, so that we can use them in the OmnichannelRoomIcon component
				const snapshots = new Map<string, AsyncState<string>>();

				return {
					queryIcon: (
						app: string,
						iconName: string,
					): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => AsyncState<string>] => [
						(callback): (() => void) =>
							OmnichannelRoomIcon.on(`${app}-${iconName}`, () => {
								snapshots.set(`${app}-${iconName}`, extractSnapshot(app, iconName));

								// Then we call the callback (onStoreChange), signaling React to re-render
								callback();
							}),

						// No problem here, because it's return value is a cached in the snapshots map on subsequent calls
						(): AsyncState<string> => {
							let snapshot = snapshots.get(`${app}-${iconName}`);

							if (!snapshot) {
								snapshot = extractSnapshot(app, iconName);
								snapshots.set(`${app}-${iconName}`, snapshot);
							}

							return snapshot;
						},
					],
				};
			}, [])}
		>
			{createPortal(
				<svg
					xmlns='http://www.w3.org/2000/svg'
					xmlnsXlink='http://www.w3.org/1999/xlink'
					style={{ display: 'none' }}
					dangerouslySetInnerHTML={{ __html: svgIcons.join('') }}
				/>,
				document.body,
				'custom-icons',
			)}
			{children}
		</OmnichannelRoomIconContext.Provider>
	);
};


rid,
tmid,
isMicrophoneDenied,
}: MessageBoxActionsToolbarProps) => {
const actions = [


import { Mongo } from 'meteor/mongo';

export const LivechatInquiry = new Mongo.Collection(null);


export const filterAppsByText = (name: string, text: string): boolean => name.toLowerCase().indexOf(text.toLowerCase()) > -1;


import { Field, Flex, InputBox, Skeleton } from '@rocket.chat/fuselage';
import type { ReactElement } from 'react';
import React from 'react';

const SettingSkeleton = (): ReactElement => (
	<Field>
		<Flex.Item align='stretch'>
			<Field.Label>
				<Skeleton width='25%' />
			</Field.Label>
		</Flex.Item>
		<Field.Row>
			<InputBox.Skeleton />
		</Field.Row>
	</Field>
);

export default SettingSkeleton;


<VideoMessageAction key='video' collapsed={variant === 'small'} disabled={!canSend || typing || isRecording} />,

disabled={!canSend || typing || isRecording || isMicrophoneDenied}

<VideoMessageAction key='video' collapsed={variant === 'small'} disabled={!canSend || typing || isRecording} {...props} />,


import { Users } from '../../../models/server';

export function userLoggedOut(userId) {
	if (!userId) {
		return false;
	}

	const user = Users.findOneById(userId);

	if (user && user.services && user.services.cloud) {
		Users.update(user._id, {
			$unset: {
				'services.cloud': 1,
			},
		});
	}

	return true;
}


import type { IMessage } from '@rocket.chat/core-typings';
import type { AppServiceOutput, Bridge } from '@rocket.chat/forked-matrix-appservice-bridge';

import type { IExternalUserProfileInformation, IFederationBridge, IFederationBridgeRegistrationFile } from '../../domain/IFederationBridge';
import { federationBridgeLogger } from '../rocket-chat/adapters/logger';
import { toExternalMessageFormat, toExternalQuoteMessageFormat } from './converters/room/to-internal-parser-formatter';
import { convertEmojisFromRCFormatToMatrixFormat } from './converters/room/MessageReceiver';
import type { AbstractMatrixEvent } from './definitions/AbstractMatrixEvent';
import { MatrixEnumRelatesToRelType, MatrixEnumSendMessageType } from './definitions/events/RoomMessageSent';
import { MatrixEventType } from './definitions/MatrixEventType';
import { MatrixRoomType } from './definitions/MatrixRoomType';
import { MatrixRoomVisibility } from './definitions/MatrixRoomVisibility';
import { RoomMembershipChangedEventType } from './definitions/events/RoomMembershipChanged';
import type { MatrixEventRoomNameChanged } from './definitions/events/RoomNameChanged';
import type { MatrixEventRoomTopicChanged } from './definitions/events/RoomTopicChanged';
import type { RocketChatSettingsAdapter } from '../rocket-chat/adapters/Settings';
import { fetch } from '../../../../lib/http/fetch';

let MatrixUserInstance: any;

export class MatrixBridge implements IFederationBridge {
	protected bridgeInstance: Bridge;

	protected isRunning = false;

	protected isUpdatingBridgeStatus = false;

	constructor(protected internalSettings: RocketChatSettingsAdapter, protected eventHandler: (event: AbstractMatrixEvent) => void) {} // eslint-disable-line no-empty-function

	public async start(): Promise<void> {
		if (this.isUpdatingBridgeStatus) {
			return;
		}
		this.isUpdatingBridgeStatus = true;
		try {
			await this.stop();
			await this.createInstance();

			if (!this.isRunning) {
				await this.bridgeInstance.run(this.internalSettings.getBridgePort());
				this.isRunning = true;
			}
		} catch (err) {
			federationBridgeLogger.error({ msg: 'Failed to initialize the matrix-appservice-bridge.', err });
		} finally {
			this.isUpdatingBridgeStatus = false;
		}
	}

	public async stop(): Promise<void> {
		if (!this.isRunning) {
			return;
		}
		return new Promise(async (resolve: () => void): Promise<void> => {
			// the http server might take some minutes to shutdown, and this promise can take some time to be resolved
			await this.bridgeInstance?.close();
			this.isRunning = false;
			resolve();
		});
	}

	public async getUserProfileInformation(externalUserId: string): Promise<IExternalUserProfileInformation | undefined> {
		try {
			const externalInformation = await this.bridgeInstance.getIntent(externalUserId).getProfileInfo(externalUserId, undefined, false);

			return {
				displayName: externalInformation.displayname || '',
				...(externalInformation.avatar_url
					? {
							avatarUrl: externalInformation.avatar_url,
					  }
					: {}),
			};
		} catch (err) {
			// no-op
		}
	}

	public async joinRoom(externalRoomId: string, externalUserId: string, viaServers?: string[]): Promise<void> {
		await this.bridgeInstance.getIntent(externalUserId).join(externalRoomId, viaServers);
	}

	public async getRoomHistoricalJoinEvents(
		externalRoomId: string,
		externalUserId: string,
		excludingUserIds: string[] = [],
	): Promise<any[]> {
		const events = await this.bridgeInstance.getIntent(externalUserId).matrixClient.getRoomState(externalRoomId);
		const roomCreator = events.find((event) => event.type === MatrixEventType.ROOM_CREATED)?.content?.creator;
		if (!roomCreator) {
			return [];
		}
		return events
			.filter(
				(event) =>
					event.type === MatrixEventType.ROOM_MEMBERSHIP_CHANGED &&
					event.content.membership === RoomMembershipChangedEventType.JOIN &&
					!excludingUserIds.includes(event.state_key),
			)
			.map((event) => ({
				...event,
				sender: roomCreator,
			}));
	}

	public async inviteToRoom(externalRoomId: string, externalInviterId: string, externalInviteeId: string): Promise<void> {
		try {
			await this.bridgeInstance.getIntent(externalInviterId).invite(externalRoomId, externalInviteeId);
		} catch (e) {
			// no-op
		}
	}

	public async setUserAvatar(externalUserId: string, avatarUrl: string): Promise<void> {
		try {
			await this.bridgeInstance.getIntent(externalUserId).matrixClient.setAvatarUrl(avatarUrl);
		} catch (e) {
			// no-op
		}
	}

	public async createUser(username: string, name: string, domain: string, avatarUrl?: string): Promise<string> {
		if (!MatrixUserInstance) {
			throw new Error('Error loading the Matrix User instance from the external library');
		}
		const matrixUserId = `@${username?.toLowerCase()}:${domain}`;
		const newUser = new MatrixUserInstance(matrixUserId);
		await this.bridgeInstance.provisionUser(newUser, { name, ...(avatarUrl ? { url: avatarUrl } : {}) });

		return matrixUserId;
	}

	public async setUserDisplayName(externalUserId: string, displayName: string): Promise<void> {
		try {
			await this.bridgeInstance.getIntent(externalUserId).setDisplayName(displayName);
		} catch (e) {
			// no-op
		}
	}

	public async createDirectMessageRoom(
		externalCreatorId: string,
		inviteesExternalIds: string[],
		extraData: Record<string, any> = {},
	): Promise<string> {
		const intent = this.bridgeInstance.getIntent(externalCreatorId);

		const visibility = MatrixRoomVisibility.PRIVATE;
		const preset = MatrixRoomType.PRIVATE;

		const matrixRoom = await intent.createRoom({
			createAsClient: true,
			options: {
				visibility,
				preset,
				is_direct: true,
				invite: inviteesExternalIds,
				creation_content: {
					was_internally_programatically_created: true,
					...extraData,
					inviteesExternalIds,
				},
			},
		});
		return matrixRoom.room_id;
	}

	public async sendMessage(externalRoomId: string, externalSenderId: string, message: IMessage): Promise<string> {
		try {
			const messageId = await this.bridgeInstance
				.getIntent(externalSenderId)
				.matrixClient.sendRawEvent(externalRoomId, MatrixEventType.ROOM_MESSAGE_SENT, {
					msgtype: 'm.text',
					body: this.escapeEmojis(message.msg),
					formatted_body: this.escapeEmojis(
						await toExternalMessageFormat({
							message: message.msg,
							externalRoomId,
							homeServerDomain: this.internalSettings.getHomeServerDomain(),
						}),
					),
					format: 'org.matrix.custom.html',
				});

			return messageId;
		} catch (e) {
			throw new Error('User is not part of the room.');
		}
	}

	public async sendReplyToMessage(
		externalRoomId: string,
		externalUserId: string,
		eventToReplyTo: string,
		originalEventSender: string,
		replyMessage: string,
	): Promise<string> {
		const { formattedMessage, message } = await toExternalQuoteMessageFormat({
			externalRoomId,
			eventToReplyTo,
			originalEventSender,
			message: this.escapeEmojis(replyMessage),
			homeServerDomain: this.internalSettings.getHomeServerDomain(),
		});
		const messageId = await this.bridgeInstance
			.getIntent(externalUserId)
			.matrixClient.sendEvent(externalRoomId, MatrixEventType.ROOM_MESSAGE_SENT, {
				'body': message,
				'format': 'org.matrix.custom.html',
				'formatted_body': formattedMessage,
				'm.relates_to': {
					'm.in_reply_to': { event_id: eventToReplyTo },
				},
				'msgtype': MatrixEnumSendMessageType.TEXT,
			});

		return messageId;
	}

	private escapeEmojis(text: string): string {
		return convertEmojisFromRCFormatToMatrixFormat(text);
	}

	public async getReadStreamForFileFromUrl(externalUserId: string, fileUrl: string): Promise<ReadableStream> {
		const response = await fetch(this.convertMatrixUrlToHttp(externalUserId, fileUrl));
		if (!response.body) {
			throw new Error('Not able to download the file');
		}

		return response.body;
	}

	public isUserIdFromTheSameHomeserver(externalUserId: string, domain: string): boolean {
		const userDomain = this.extractHomeserverOrigin(externalUserId);

		return userDomain === domain;
	}

	public extractHomeserverOrigin(externalUserId: string): string {
		return externalUserId.includes(':') ? externalUserId.split(':').pop() || '' : this.internalSettings.getHomeServerDomain();
	}

	public isRoomFromTheSameHomeserver(externalRoomId: string, domain: string): boolean {
		return this.isUserIdFromTheSameHomeserver(externalRoomId, domain);
	}

	public logFederationStartupInfo(info?: string): void {
		federationBridgeLogger.info(`${info}:
			id: ${this.internalSettings.getApplicationServiceId()}
			bridgeUrl: ${this.internalSettings.getBridgeUrl()}
			homeserverURL: ${this.internalSettings.getHomeServerUrl()}
			homeserverDomain: ${this.internalSettings.getHomeServerDomain()}
		`);
	}

	public async leaveRoom(externalRoomId: string, externalUserId: string): Promise<void> {
		try {
			await this.bridgeInstance.getIntent(externalUserId).leave(externalRoomId);
		} catch (e) {
			// no-op
		}
	}

	public async kickUserFromRoom(externalRoomId: string, externalUserId: string, externalOwnerId: string): Promise<void> {
		await this.bridgeInstance.getIntent(externalOwnerId).kick(externalRoomId, externalUserId);
	}

	public async setRoomPowerLevels(
		externalRoomId: string,
		externalOwnerId: string,
		externalUserId: string,
		powerLevels: number,
	): Promise<void> {
		await this.bridgeInstance.getIntent(externalOwnerId).setPowerLevel(externalRoomId, externalUserId, powerLevels);
	}

	public async redactEvent(externalRoomId: string, externalUserId: string, externalEventId: string): Promise<void> {
		await this.bridgeInstance.getIntent(externalUserId).matrixClient.redactEvent(externalRoomId, externalEventId);
	}

	public async notifyUserTyping(externalRoomId: string, externalUserId: string, isTyping: boolean): Promise<void> {
		await this.bridgeInstance.getIntent(externalUserId).sendTyping(externalRoomId, isTyping);
	}

	public async sendMessageReaction(
		externalRoomId: string,
		externalUserId: string,
		externalEventId: string,
		reaction: string,
	): Promise<string> {
		const eventId = await this.bridgeInstance
			.getIntent(externalUserId)
			.matrixClient.sendEvent(externalRoomId, MatrixEventType.MESSAGE_REACTED, {
				'm.relates_to': {
					event_id: externalEventId,
					key: convertEmojisFromRCFormatToMatrixFormat(reaction),
					rel_type: 'm.annotation',
				},
			});

		return eventId;
	}

	public async updateMessage(
		externalRoomId: string,
		externalUserId: string,
		externalEventId: string,
		newMessageText: string,
	): Promise<void> {
		const messageInExternalFormat = this.escapeEmojis(
			await toExternalMessageFormat({
				message: newMessageText,
				externalRoomId,
				homeServerDomain: this.internalSettings.getHomeServerDomain(),
			}),
		);

		await this.bridgeInstance.getIntent(externalUserId).matrixClient.sendEvent(externalRoomId, MatrixEventType.ROOM_MESSAGE_SENT, {
			'body': ` * ${this.escapeEmojis(newMessageText)}`,
			'format': 'org.matrix.custom.html',
			'formatted_body': messageInExternalFormat,
			'm.new_content': {
				body: this.escapeEmojis(newMessageText),
				format: 'org.matrix.custom.html',
				formatted_body: messageInExternalFormat,
				msgtype: MatrixEnumSendMessageType.TEXT,
			},
			'm.relates_to': {
				rel_type: MatrixEnumRelatesToRelType.REPLACE,
				event_id: externalEventId,
			},
			'msgtype': MatrixEnumSendMessageType.TEXT,
		});
	}

	public async sendMessageFileToRoom(
		externalRoomId: string,
		externaSenderId: string,
		content: Buffer,
		fileDetails: { filename: string; fileSize: number; mimeType: string; metadata?: { width?: number; height?: number; format?: string } },
	): Promise<string> {
		try {
			const mxcUrl = await this.bridgeInstance.getIntent(externaSenderId).uploadContent(content);
			const { event_id: messageId } = await this.bridgeInstance.getIntent(externaSenderId).sendMessage(externalRoomId, {
				body: fileDetails.filename,
				filename: fileDetails.filename,
				info: {
					size: fileDetails.fileSize,
					mimetype: fileDetails.mimeType,
					...(fileDetails.metadata?.height && fileDetails.metadata?.width
						? { h: fileDetails.metadata?.height, w: fileDetails.metadata?.width }
						: {}),
				},
				msgtype: this.getMsgTypeBasedOnMimeType(fileDetails.mimeType),
				url: mxcUrl,
			});

			return messageId;
		} catch (e: any) {
			if (e.body?.includes('413') || e.body?.includes('M_TOO_LARGE')) {
				throw new Error('File is too large');
			}
			return '';
		}
	}

	public async sendReplyMessageFileToRoom(
		externalRoomId: string,
		externaSenderId: string,
		content: Buffer,
		fileDetails: { filename: string; fileSize: number; mimeType: string; metadata?: { width?: number; height?: number; format?: string } },
		eventToReplyTo: string,
	): Promise<string> {
		try {
			const mxcUrl = await this.bridgeInstance.getIntent(externaSenderId).uploadContent(content);
			const { event_id: messageId } = await this.bridgeInstance.getIntent(externaSenderId).sendMessage(externalRoomId, {
				'body': fileDetails.filename,
				'filename': fileDetails.filename,
				'info': {
					size: fileDetails.fileSize,
					mimetype: fileDetails.mimeType,
					...(fileDetails.metadata?.height && fileDetails.metadata?.width
						? { h: fileDetails.metadata?.height, w: fileDetails.metadata?.width }
						: {}),
				},
				'm.relates_to': {
					'm.in_reply_to': { event_id: eventToReplyTo },
				},
				'msgtype': this.getMsgTypeBasedOnMimeType(fileDetails.mimeType),
				'url': mxcUrl,
			});

			return messageId;
		} catch (e: any) {
			if (e.body?.includes('413') || e.body?.includes('M_TOO_LARGE')) {
				throw new Error('File is too large');
			}
			return '';
		}
	}

	private getMsgTypeBasedOnMimeType(mimeType: string): MatrixEnumSendMessageType {
		const knownImageMimeTypes = ['image/jpeg', 'image/png', 'image/gif'];
		const knownAudioMimeTypes = ['audio/mpeg', 'audio/ogg', 'audio/wav'];
		const knownVideoMimeTypes = ['video/mp4', 'video/ogg', 'video/webm'];

		if (knownImageMimeTypes.includes(mimeType)) {
			return MatrixEnumSendMessageType.IMAGE;
		}
		if (knownAudioMimeTypes.includes(mimeType)) {
			return MatrixEnumSendMessageType.AUDIO;
		}
		if (knownVideoMimeTypes.includes(mimeType)) {
			return MatrixEnumSendMessageType.VIDEO;
		}
		return MatrixEnumSendMessageType.FILE;
	}

	public async uploadContent(
		externalSenderId: string,
		content: Buffer,
		options?: { name?: string; type?: string },
	): Promise<string | undefined> {
		try {
			const mxcUrl = await this.bridgeInstance.getIntent(externalSenderId).uploadContent(content, options);

			return mxcUrl;
		} catch (e: any) {
			if (e.body?.includes('413') || e.body?.includes('M_TOO_LARGE')) {
				throw new Error('File is too large');
			}
		}
	}

	public async getRoomName(externalRoomId: string, externalUserId: string): Promise<string | undefined> {
		try {
			const roomState = (await this.bridgeInstance.getIntent(externalUserId).roomState(externalRoomId)) as AbstractMatrixEvent[];

			return ((roomState || []).find((event) => event?.type === MatrixEventType.ROOM_NAME_CHANGED) as MatrixEventRoomNameChanged)?.content
				?.name;
		} catch (error) {
			// no-op
		}
	}

	public async getRoomTopic(externalRoomId: string, externalUserId: string): Promise<string | undefined> {
		try {
			const roomState = (await this.bridgeInstance.getIntent(externalUserId).roomState(externalRoomId)) as AbstractMatrixEvent[];

			return ((roomState || []).find((event) => event?.type === MatrixEventType.ROOM_TOPIC_CHANGED) as MatrixEventRoomTopicChanged)?.content
				?.topic;
		} catch (error) {
			// no-op
		}
	}

	public async setRoomName(externalRoomId: string, externalUserId: string, roomName: string): Promise<void> {
		await this.bridgeInstance.getIntent(externalUserId).setRoomName(externalRoomId, roomName);
	}

	public async setRoomTopic(externalRoomId: string, externalUserId: string, roomTopic: string): Promise<void> {
		await this.bridgeInstance.getIntent(externalUserId).setRoomTopic(externalRoomId, roomTopic);
	}

	public convertMatrixUrlToHttp(externalUserId: string, matrixUrl: string): string {
		return this.bridgeInstance.getIntent(externalUserId).matrixClient.mxcToHttp(matrixUrl);
	}

	protected async createInstance(): Promise<void> {
		federationBridgeLogger.info('Performing Dynamic Import of matrix-appservice-bridge');

		// Dynamic import to prevent Rocket.Chat from loading the module until needed and then handle if that fails
		const { Bridge, AppServiceRegistration, MatrixUser } = await import('@rocket.chat/forked-matrix-appservice-bridge');
		MatrixUserInstance = MatrixUser;
		const registrationFile = this.internalSettings.generateRegistrationFileObject();

		this.bridgeInstance = new Bridge({
			homeserverUrl: this.internalSettings.getHomeServerUrl(),
			domain: this.internalSettings.getHomeServerDomain(),
			registration: AppServiceRegistration.fromObject(this.convertRegistrationFileToMatrixFormat(registrationFile)),
			disableStores: true,
			controller: {
				onEvent: async (request): Promise<void> => {
					const event = request.getData() as unknown as AbstractMatrixEvent;
					this.eventHandler(event);
				},
				onLog: async (line, isError): Promise<void> => {
					console.log(line, isError);
				},
				...(this.internalSettings.generateRegistrationFileObject().enableEphemeralEvents
					? {
							onEphemeralEvent: async (request): Promise<void> => {
								const event = request.getData() as unknown as AbstractMatrixEvent;
								this.eventHandler(event);
							},
					  }
					: {}),
			},
		});
	}

	private convertRegistrationFileToMatrixFormat(registrationFile: IFederationBridgeRegistrationFile): AppServiceOutput {
		return {
			'id': registrationFile.id,
			'hs_token': registrationFile.homeserverToken,
			'as_token': registrationFile.applicationServiceToken,
			'url': registrationFile.bridgeUrl,
			'sender_localpart': registrationFile.botName,
			'namespaces': registrationFile.listenTo,
			'de.sorunome.msc2409.push_ephemeral': registrationFile.enableEphemeralEvents,
		};
	}
}


import { Accounts } from 'meteor/accounts-base';

import { IframeLogin } from '../../ui-utils/client';

const iframeLogin = new IframeLogin();

const { _unstoreLoginToken } = Accounts;
Accounts._unstoreLoginToken = function (...args) {
	iframeLogin.tryLogin();
	_unstoreLoginToken.apply(Accounts, args);
};

window.addEventListener('message', (e) => {
	if (!(typeof e.data === 'function' || (typeof e.data === 'object' && !!e.data))) {
		return;
	}

	switch (e.data.event) {
		case 'try-iframe-login':
			iframeLogin.tryLogin((error) => {
				if (error) {
					e.source.postMessage(
						{
							event: 'login-error',
							response: error.message,
						},
						e.origin,
					);
				}
			});
			break;

		case 'login-with-token':
			iframeLogin.loginWithToken(e.data, (error) => {
				if (error) {
					e.source.postMessage(
						{
							event: 'login-error',
							response: error.message,
						},
						e.origin,
					);
				}
			});
			break;
	}
});


import { Select } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import React, { useMemo, useState } from 'react';

import EngagementDashboardCardFilter from '../EngagementDashboardCardFilter';
import ContentForDays from './ContentForDays';
import ContentForHours from './ContentForHours';

type TimeUnit = 'hours' | 'days';

type BusiestChatTimesSectionProps = {
	timezone: 'utc' | 'local';
};

const BusiestChatTimesSection = ({ timezone }: BusiestChatTimesSectionProps): ReactElement => {
	const t = useTranslation();

	const [timeUnit, setTimeUnit] = useState<TimeUnit>('hours');
	const timeUnitOptions = useMemo<[timeUnit: TimeUnit, label: string][]>(
		() => [
			['hours', t('Hours')],
			['days', t('Days')],
		],
		[t],
	);

	const [displacement, setDisplacement] = useState(0);

	const handleTimeUnitChange = (timeUnit: string): void => {
		setTimeUnit(timeUnit as TimeUnit);
		setDisplacement(0);
	};

	const handlePreviousDateClick = (): void => setDisplacement((displacement) => displacement + 1);
	const handleNextDateClick = (): void => setDisplacement((displacement) => displacement - 1);

	const Content = (
		{
			hours: ContentForHours,
			days: ContentForDays,
		} as const
	)[timeUnit];

	return (
		<>
			<EngagementDashboardCardFilter>
				<Select options={timeUnitOptions} value={timeUnit} onChange={handleTimeUnitChange} />
			</EngagementDashboardCardFilter>
			<Content
				displacement={displacement}
				onPreviousDateClick={handlePreviousDateClick}
				onNextDateClick={handleNextDateClick}
				timezone={timezone}
			/>
		</>
	);
};

export default BusiestChatTimesSection;


import { Meteor } from 'meteor/meteor';

import { hasPermission } from '../../../authorization/server';
import { Livechat } from '../lib/Livechat';
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';

Meteor.methods({
	'livechat:removeAgent'(username) {
		methodDeprecationLogger.warn('livechat:removeAgent will be deprecated in future versions of Rocket.Chat');
		if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'manage-livechat-agents')) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', {
				method: 'livechat:removeAgent',
			});
		}

		return Livechat.removeAgent(username);
	},
});


<FileUploadAction key='file' collapsed={variant === 'small'} disabled={!canSend || isRecording} />,

<FileUploadAction key='file' collapsed={variant === 'small'} disabled={!canSend || isRecording} {...props} />,


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

const SidebarPortal: FC = ({ children }) => {
	const sidebarRoot = document.getElementById('sidebar-region');

	if (!sidebarRoot) {
		return null;
	}

	return createPortal(<Box className='rcx-sidebar flex-nav'>{children}</Box>, sidebarRoot);
};

export default memo<typeof SidebarPortal>(SidebarPortal);


---
to: packages/<%= name %>/package.json
---

{
	"name": "@rocket.chat/<%= name.toLowerCase() %>",
	"version": "0.0.1",
	"private": true,
	"devDependencies": {
		"@types/jest": "^27.4.1",
		"eslint": "^8.12.0",
		"jest": "^27.5.1",
		"ts-jest": "^27.1.4",
		"typescript": "~4.6.4"
	},
	"scripts": {
		"lint": "eslint --ext .js,.jsx,.ts,.tsx .",
		"lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix",
		"jest": "jest",
		"build": "rm -rf dist && tsc -p tsconfig.json",
		"dev": "tsc -p tsconfig.json --watch --preserveWatchOutput"
	},
	"main": "./dist/index.js",
	"typings": "./dist/index.d.ts",
	"files": [
		"/dist"
	]
}


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

API.v1.addRoute(
	'livechat/transfer.history/:rid',
	{ authRequired: true, permissionsRequired: ['view-livechat-rooms'] },
	{
		async get() {
			const { rid } = this.urlParams;

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

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

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

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


<ActionsToolbarDropdown {...(variant === 'small' && { actions })} isRecording={isRecording} rid={rid} tmid={tmid} />

keep keep keep keep replace keep keep keep keep

<ActionsToolbarDropdown {...(variant === 'small' && { actions })} isRecording={isRecording} rid={rid} tmid={tmid} {...props} />


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

import { settings } from '../../../settings/server';
import { checkUsernameAvailability } from '../functions';
import { RateLimiter } from '../lib';
import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';

Meteor.methods({
	checkUsernameAvailability(username) {
		methodDeprecationLogger.warn('checkUsernameAvailability will be deprecated in future versions of Rocket.Chat');

		check(username, String);

		const user = Meteor.user();

		if (!user) {
			throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'setUsername' });
		}

		if (user.username && !settings.get('Accounts_AllowUsernameChange')) {
			throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setUsername' });
		}

		if (user.username === username) {
			return true;
		}
		return checkUsernameAvailability(username);
	},
});

RateLimiter.limitMethod('checkUsernameAvailability', 1, 1000, {
	userId() {
		return true;
	},
});


@rocket.chat/rest-typings

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