import RNFetchBlob from 'rn-fetch-blob'; import { settings as RocketChatSettings, Rocketchat as RocketchatClient } from '@rocket.chat/sdk'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { InteractionManager } from 'react-native'; import { Q } from '@nozbe/watermelondb'; import log from '../methods/helpers/log'; import { setActiveUsers } from '../../actions/activeUsers'; import protectedFunction from '../methods/helpers/protectedFunction'; import database from '../database'; import { selectServerFailure } from '../../actions/server'; import { twoFactor } from './twoFactor'; import { store } from '../store/auxStore'; import { loginRequest, setLoginServices, setUser } from '../../actions/login'; import sdk from './sdk'; import I18n from '../../i18n'; import { ICredentials, ILoggedUser, STATUSES } from '../../definitions'; import { connectRequest, connectSuccess, disconnect as disconnectAction } from '../../actions/connect'; import { updatePermission } from '../../actions/permissions'; import EventEmitter from '../methods/helpers/events'; import { updateSettings } from '../../actions/settings'; import { defaultSettings, MIN_ROCKETCHAT_VERSION } from '../constants'; import { getSettings, IActiveUsers, unsubscribeRooms, _activeUsers, _setUser, _setUserTimer, onRolesChanged, setPresenceCap } from '../methods'; import { compareServerVersion, isIOS, isSsl } from '../methods/helpers'; interface IServices { [index: string]: string | boolean; name: string; custom: boolean; showButton: boolean; buttonLabelText: string; service: string; } let connectingListener: any; let connectedListener: any; let closeListener: any; let usersListener: any; let notifyAllListener: any; let rolesListener: any; let notifyLoggedListener: any; function connect({ server, logoutOnError = false }: { server: string; logoutOnError: boolean }): Promise { return new Promise(resolve => { if (sdk.current?.client?.host === server) { return resolve(); } // Check for running requests and abort them before connecting to the server abort(); disconnect(); database.setActiveDB(server); store.dispatch(connectRequest()); if (connectingListener) { connectingListener.then(stopListener); } if (connectedListener) { connectedListener.then(stopListener); } if (closeListener) { closeListener.then(stopListener); } if (usersListener) { usersListener.then(stopListener); } if (notifyAllListener) { notifyAllListener.then(stopListener); } if (rolesListener) { rolesListener.then(stopListener); } if (notifyLoggedListener) { notifyLoggedListener.then(stopListener); } unsubscribeRooms(); EventEmitter.emit('INQUIRY_UNSUBSCRIBE'); sdk.initialize(server); getSettings(); sdk.current .connect() .then(() => { console.log('connected'); }) .catch((err: unknown) => { console.log('connect error', err); }); connectingListener = sdk.current.onStreamData('connecting', () => { store.dispatch(connectRequest()); }); connectedListener = sdk.current.onStreamData('connected', () => { const { connected } = store.getState().meteor; if (connected) { return; } store.dispatch(connectSuccess()); const { user } = store.getState().login; if (user?.token) { store.dispatch(loginRequest({ resume: user.token }, logoutOnError)); } }); closeListener = sdk.current.onStreamData('close', () => { store.dispatch(disconnectAction()); }); usersListener = sdk.current.onStreamData( 'users', protectedFunction((ddpMessage: any) => _setUser(ddpMessage)) ); notifyAllListener = sdk.current.onStreamData( 'stream-notify-all', protectedFunction(async (ddpMessage: { fields: { args?: any; eventName: string } }) => { const { eventName } = ddpMessage.fields; if (/public-settings-changed/.test(eventName)) { const { _id, value } = ddpMessage.fields.args[1]; const db = database.active; const settingsCollection = db.get('settings'); try { const settingsRecord = await settingsCollection.find(_id); // @ts-ignore const { type } = defaultSettings[_id]; if (type) { await db.write(async () => { await settingsRecord.update(u => { // @ts-ignore u[type] = value; }); }); } store.dispatch(updateSettings(_id, value)); if (_id === 'Presence_broadcast_disabled') { setPresenceCap(value); } } catch (e) { log(e); } } }) ); rolesListener = sdk.current.onStreamData( 'stream-roles', protectedFunction((ddpMessage: any) => onRolesChanged(ddpMessage)) ); // RC 4.1 sdk.current.onStreamData('stream-user-presence', (ddpMessage: { fields: { args?: any; uid?: any } }) => { const userStatus = ddpMessage.fields.args[0]; const { uid } = ddpMessage.fields; const [, status, statusText] = userStatus; const newStatus = { status: STATUSES[status], statusText }; // @ts-ignore store.dispatch(setActiveUsers({ [uid]: newStatus })); const { user: loggedUser } = store.getState().login; if (loggedUser && loggedUser.id === uid) { // @ts-ignore store.dispatch(setUser(newStatus)); } }); notifyLoggedListener = sdk.current.onStreamData( 'stream-notify-logged', protectedFunction(async (ddpMessage: { fields: { args?: any; eventName?: any } }) => { const { eventName } = ddpMessage.fields; // `user-status` event is deprecated after RC 4.1 in favor of `stream-user-presence/${uid}` if (/user-status/.test(eventName)) { _activeUsers.activeUsers = _activeUsers.activeUsers || {}; if (!_setUserTimer.setUserTimer) { _setUserTimer.setUserTimer = setTimeout(() => { const activeUsersBatch = _activeUsers.activeUsers; InteractionManager.runAfterInteractions(() => { // @ts-ignore store.dispatch(setActiveUsers(activeUsersBatch)); }); _setUserTimer.setUserTimer = null; _activeUsers.activeUsers = {} as IActiveUsers; return null; }, 10000); } const userStatus = ddpMessage.fields.args[0]; const [id, , status, statusText] = userStatus; _activeUsers.activeUsers[id] = { status: STATUSES[status], statusText }; const { user: loggedUser } = store.getState().login; if (loggedUser && loggedUser.id === id) { store.dispatch(setUser({ status: STATUSES[status], statusText })); } } else if (/updateAvatar/.test(eventName)) { const { username, etag } = ddpMessage.fields.args[0]; const db = database.active; const userCollection = db.get('users'); try { const [userRecord] = await userCollection.query(Q.where('username', Q.eq(username))).fetch(); await db.write(async () => { await userRecord.update(u => { u.avatarETag = etag; }); }); } catch { // We can't create a new record since we don't receive the user._id } } else if (/permissions-changed/.test(eventName)) { const { _id, roles } = ddpMessage.fields.args[1]; const db = database.active; const permissionsCollection = db.get('permissions'); try { const permissionsRecord = await permissionsCollection.find(_id); await db.write(async () => { await permissionsRecord.update(u => { u.roles = roles; }); }); store.dispatch(updatePermission(_id, roles)); } catch (err) { // } } else if (/Users:NameChanged/.test(eventName)) { const userNameChanged = ddpMessage.fields.args[0]; const db = database.active; const userCollection = db.get('users'); try { const userRecord = await userCollection.find(userNameChanged._id); await db.write(async () => { await userRecord.update(u => { Object.assign(u, userNameChanged); }); }); } catch { // User not found await db.write(async () => { await userCollection.create(u => { u._raw = sanitizedRaw({ id: userNameChanged._id }, userCollection.schema); Object.assign(u, userNameChanged); }); }); } } }) ); resolve(); }); } function stopListener(listener: any): boolean { return listener && listener.stop(); } async function login(credentials: ICredentials, isFromWebView = false): Promise { // RC 0.64.0 await sdk.current.login(credentials); const serverVersion = store.getState().server.version; const result = sdk.current.currentLogin?.result; let enableMessageParserEarlyAdoption = true; let showMessageInMainThread = false; if (compareServerVersion(serverVersion, 'lowerThan', '5.0.0')) { enableMessageParserEarlyAdoption = result.me.settings?.preferences?.enableMessageParserEarlyAdoption ?? true; showMessageInMainThread = result.me.settings?.preferences?.showMessageInMainThread ?? true; } if (result) { const user: ILoggedUser = { id: result.userId, token: result.authToken, username: result.me.username, name: result.me.name, language: result.me.language, status: result.me.status, statusText: result.me.statusText, customFields: result.me.customFields, statusLivechat: result.me.statusLivechat, emails: result.me.emails, roles: result.me.roles, avatarETag: result.me.avatarETag, isFromWebView, showMessageInMainThread, enableMessageParserEarlyAdoption, alsoSendThreadToChannel: result.me.settings?.preferences?.alsoSendThreadToChannel }; return user; } } function loginTOTP(params: ICredentials, loginEmailPassword?: boolean, isFromWebView = false): Promise { return new Promise(async (resolve, reject) => { try { const result = await login(params, isFromWebView); if (result) { return resolve(result); } } catch (e: any) { if (e.data?.error && (e.data.error === 'totp-required' || e.data.error === 'totp-invalid')) { const { details } = e.data; try { const code = await twoFactor({ method: details?.method || 'totp', invalid: details?.error === 'totp-invalid' }); if (loginEmailPassword) { store.dispatch(setUser({ username: params.user || params.username })); // Force normalized params for 2FA starting RC 3.9.0. const serverVersion = store.getState().server.version; if (compareServerVersion(serverVersion as string, 'greaterThanOrEqualTo', '3.9.0')) { const user = params.user ?? params.username; const password = params.password ?? params.ldapPass ?? params.crowdPassword; params = { user, password }; } return resolve(loginTOTP({ ...params, code: code?.twoFactorCode }, loginEmailPassword)); } return resolve( loginTOTP({ totp: { login: { ...params }, code: code?.twoFactorCode } }) ); } catch { // twoFactor was canceled return reject(); } } else { reject(e); } } }); } function loginWithPassword({ user, password }: { user: string; password: string }): Promise { let params: ICredentials = { user, password }; const state = store.getState(); if (state.settings.LDAP_Enable) { params = { username: user, ldapPass: password, ldap: true, ldapOptions: {} }; } else if (state.settings.CROWD_Enable) { params = { username: user, crowdPassword: password, crowd: true }; } return loginTOTP(params, true); } async function loginOAuthOrSso(params: ICredentials, isFromWebView = true) { const result = await loginTOTP(params, false, isFromWebView); store.dispatch(loginRequest({ resume: result.token }, false, isFromWebView)); } function abort() { if (sdk.current) { return sdk.current.abort(); } } function checkAndReopen() { return sdk.current.checkAndReopen(); } function disconnect() { return sdk.disconnect(); } async function getServerInfo(server: string) { try { const response = await RNFetchBlob.fetch('GET', `${server}/api/info`, { ...RocketChatSettings.customHeaders }); try { // Try to resolve as json const jsonRes: { version?: string; success: boolean } = response.json(); if (!jsonRes?.success) { return { success: false, message: I18n.t('Not_RC_Server', { contact: I18n.t('Contact_your_server_admin') }) }; } if (compareServerVersion(jsonRes.version, 'lowerThan', MIN_ROCKETCHAT_VERSION)) { return { success: false, message: I18n.t('Invalid_server_version', { currentVersion: jsonRes.version, minVersion: MIN_ROCKETCHAT_VERSION }) }; } return jsonRes; } catch (error) { // Request is successful, but response isn't a json } } catch (e: any) { if (e?.message) { if (e.message === 'Aborted') { store.dispatch(selectServerFailure()); throw e; } return { success: false, message: e.message }; } } return { success: false, message: I18n.t('Not_RC_Server', { contact: I18n.t('Contact_your_server_admin') }) }; } async function getWebsocketInfo({ server }: { server: string }) { const websocketSdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: isSsl(server) }); try { await websocketSdk.connect(); } catch (err: any) { if (err.message && err.message.includes('400')) { return { success: false, message: I18n.t('Websocket_disabled', { contact: I18n.t('Contact_your_server_admin') }) }; } } websocketSdk.disconnect(); return { success: true }; } async function getLoginServices(server: string) { try { let loginServices = []; const loginServicesResult = await fetch(`${server}/api/v1/settings.oauth`).then(response => response.json()); if (loginServicesResult.success && loginServicesResult.services) { const { services } = loginServicesResult; loginServices = services; const loginServicesReducer = loginServices.reduce((ret: IServices[], item: IServices) => { const name = item.name || item.buttonLabelText || item.service; const authType = determineAuthType(item); if (authType !== 'not_supported') { ret[name as unknown as number] = { ...item, name, authType }; } return ret; }, {}); store.dispatch(setLoginServices(loginServicesReducer)); } else { store.dispatch(setLoginServices({})); } } catch (error) { console.log(error); store.dispatch(setLoginServices({})); } } function determineAuthType(services: IServices) { const { name, custom, showButton = true, service } = services; const authName = name || service; if (custom && showButton) { return 'oauth_custom'; } if (service === 'saml') { return 'saml'; } if (service === 'cas') { return 'cas'; } if (authName === 'apple' && isIOS) { return 'apple'; } // TODO: remove this after other oauth providers are implemented. e.g. Drupal, github_enterprise const availableOAuth = ['facebook', 'github', 'gitlab', 'google', 'linkedin', 'meteor-developer', 'twitter', 'wordpress']; return availableOAuth.includes(authName) ? 'oauth' : 'not_supported'; } export { login, loginTOTP, loginWithPassword, loginOAuthOrSso, checkAndReopen, abort, connect, disconnect, getServerInfo, getWebsocketInfo, stopListener, getLoginServices, determineAuthType };