diff --git a/app/definitions/IRocketChat.ts b/app/definitions/IRocketChat.ts index 654e2c92c..e9401067b 100644 --- a/app/definitions/IRocketChat.ts +++ b/app/definitions/IRocketChat.ts @@ -3,6 +3,16 @@ import rocketchat from '../lib/rocketchat'; type TRocketChat = typeof rocketchat; export interface IRocketChat extends TRocketChat { + closeListener: any; + usersListener: any; + notifyAllListener: any; + rolesListener: any; + notifyLoggedListener: any; + activeUsers: any; + _setUserTimer: any; + connectedListener: any; + connectingListener: any; + connectTimeout: any; sdk: any; activeUsersSubTimeout: any; roomsSub: any; diff --git a/app/lib/rocketchat/rocketchat.js b/app/lib/rocketchat/rocketchat.js index 8a852a430..10a2399cb 100644 --- a/app/lib/rocketchat/rocketchat.js +++ b/app/lib/rocketchat/rocketchat.js @@ -72,7 +72,11 @@ import { determineAuthType, disconnect, checkAndReopen, - abort + abort, + getServerInfo, + getWebsocketInfo, + stopListener, + connect } from './services/connect'; import * as restAPis from './services/restApi'; @@ -84,8 +88,7 @@ export const THEME_PREFERENCES_KEY = 'RC_THEME_PREFERENCES_KEY'; export const CRASH_REPORT_KEY = 'RC_CRASH_REPORT_KEY'; export const ANALYTICS_EVENTS_KEY = 'RC_ANALYTICS_EVENTS_KEY'; export const MIN_ROCKETCHAT_VERSION = '0.70.0'; - -const STATUSES = ['offline', 'online', 'away', 'busy']; +export const STATUSES = ['offline', 'online', 'away', 'busy']; const RocketChat = { TOKEN_KEY, @@ -110,287 +113,14 @@ const RocketChat = { } }, canOpenRoom, - async getWebsocketInfo({ server }) { - const sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) }); - - try { - await sdk.connect(); - } catch (err) { - if (err.message && err.message.includes('400')) { - return { - success: false, - message: I18n.t('Websocket_disabled', { contact: I18n.t('Contact_your_server_admin') }) - }; - } - } - - sdk.disconnect(); - - return { - success: true - }; - }, - async getServerInfo(server) { - try { - const response = await RNFetchBlob.fetch('GET', `${server}/api/info`, { ...RocketChatSettings.customHeaders }); - try { - // Try to resolve as json - const jsonRes = 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) { - if (e?.message) { - if (e.message === 'Aborted') { - reduxStore.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') }) - }; - }, - stopListener(listener) { - return listener && listener.stop(); - }, + getWebsocketInfo, + getServerInfo, + stopListener, // Abort all requests and create a new AbortController abort, checkAndReopen, disconnect, - connect({ server, user, logoutOnError = false }) { - return new Promise(resolve => { - if (this?.sdk?.client?.host === server) { - return resolve(); - } else { - this.disconnect(); - database.setActiveDB(server); - } - reduxStore.dispatch(connectRequest()); - - if (this.connectTimeout) { - clearTimeout(this.connectTimeout); - } - - if (this.connectingListener) { - this.connectingListener.then(this.stopListener); - } - - if (this.connectedListener) { - this.connectedListener.then(this.stopListener); - } - - if (this.closeListener) { - this.closeListener.then(this.stopListener); - } - - if (this.usersListener) { - this.usersListener.then(this.stopListener); - } - - if (this.notifyAllListener) { - this.notifyAllListener.then(this.stopListener); - } - - if (this.rolesListener) { - this.rolesListener.then(this.stopListener); - } - - if (this.notifyLoggedListener) { - this.notifyLoggedListener.then(this.stopListener); - } - - this.unsubscribeRooms(); - - EventEmitter.emit('INQUIRY_UNSUBSCRIBE'); - - this.sdk = sdk.initialize(server); - this.getSettings(); - - this.sdk - .connect() - .then(() => { - console.log('connected'); - }) - .catch(err => { - console.log('connect error', err); - }); - - this.connectingListener = this.sdk.onStreamData('connecting', () => { - reduxStore.dispatch(connectRequest()); - }); - - this.connectedListener = this.sdk.onStreamData('connected', () => { - const { connected } = reduxStore.getState().meteor; - if (connected) { - return; - } - reduxStore.dispatch(connectSuccess()); - const { server: currentServer } = reduxStore.getState().server; - if (user?.token && server === currentServer) { - reduxStore.dispatch(loginRequest({ resume: user.token }, logoutOnError)); - } - }); - - this.closeListener = this.sdk.onStreamData('close', () => { - reduxStore.dispatch(disconnectAction()); - }); - - this.usersListener = this.sdk.onStreamData( - 'users', - protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)) - ); - - this.notifyAllListener = this.sdk.onStreamData( - 'stream-notify-all', - protectedFunction(async ddpMessage => { - 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); - const { type } = defaultSettings[_id]; - if (type) { - await db.action(async () => { - await settingsRecord.update(u => { - u[type] = value; - }); - }); - } - reduxStore.dispatch(updateSettings(_id, value)); - } catch (e) { - log(e); - } - } - }) - ); - - this.rolesListener = this.sdk.onStreamData( - 'stream-roles', - protectedFunction(ddpMessage => onRolesChanged(ddpMessage)) - ); - - // RC 4.1 - this.sdk.onStreamData('stream-user-presence', ddpMessage => { - const userStatus = ddpMessage.fields.args[0]; - const { uid } = ddpMessage.fields; - const [, status, statusText] = userStatus; - const newStatus = { status: STATUSES[status], statusText }; - reduxStore.dispatch(setActiveUsers({ [uid]: newStatus })); - - const { user: loggedUser } = reduxStore.getState().login; - if (loggedUser && loggedUser.id === uid) { - reduxStore.dispatch(setUser(newStatus)); - } - }); - - this.notifyLoggedListener = this.sdk.onStreamData( - 'stream-notify-logged', - protectedFunction(async ddpMessage => { - 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)) { - this.activeUsers = this.activeUsers || {}; - if (!this._setUserTimer) { - this._setUserTimer = setTimeout(() => { - const activeUsersBatch = this.activeUsers; - InteractionManager.runAfterInteractions(() => { - reduxStore.dispatch(setActiveUsers(activeUsersBatch)); - }); - this._setUserTimer = null; - return (this.activeUsers = {}); - }, 10000); - } - const userStatus = ddpMessage.fields.args[0]; - const [id, , status, statusText] = userStatus; - this.activeUsers[id] = { status: STATUSES[status], statusText }; - - const { user: loggedUser } = reduxStore.getState().login; - if (loggedUser && loggedUser.id === id) { - reduxStore.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.action(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.action(async () => { - await permissionsRecord.update(u => { - u.roles = roles; - }); - }); - reduxStore.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.action(async () => { - await userRecord.update(u => { - Object.assign(u, userNameChanged); - }); - }); - } catch { - // User not found - await db.action(async () => { - await userCollection.create(u => { - u._raw = sanitizedRaw({ id: userNameChanged._id }, userCollection.schema); - Object.assign(u, userNameChanged); - }); - }); - } - } - }) - ); - - resolve(); - }); - }, - + connect, async shareExtensionInit(server) { database.setShareDB(server); diff --git a/app/lib/rocketchat/services/connect.ts b/app/lib/rocketchat/services/connect.ts index a3e9c3523..7eabd29c2 100644 --- a/app/lib/rocketchat/services/connect.ts +++ b/app/lib/rocketchat/services/connect.ts @@ -1,6 +1,15 @@ import RNFetchBlob from 'rn-fetch-blob'; import { settings as RocketChatSettings } from '@rocket.chat/sdk'; +import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; +import { InteractionManager } from 'react-native'; +import { Q } from '@nozbe/watermelondb'; +import log from '../../../utils/log'; +import { onRolesChanged } from '../../methods/getRoles'; +import { UserStatus } from '../../../definitions/UserStatus'; +import { setActiveUsers } from '../../../actions/activeUsers'; +import protectedFunction from '../../methods/helpers/protectedFunction'; +import database from '../../database'; import { selectServerFailure } from '../../../actions/server'; import { twoFactor } from '../../../utils/twoFactor'; import { compareServerVersion } from '../../utils'; @@ -8,9 +17,15 @@ import { store } from '../../auxStore'; import { loginRequest, setLoginServices, setUser } from '../../../actions/login'; import sdk from './sdk'; import I18n from '../../../i18n'; -import { MIN_ROCKETCHAT_VERSION } from '../rocketchat'; -import { ICredentials, ILoggedUser } from '../../../definitions'; +import RocketChat, { MIN_ROCKETCHAT_VERSION, STATUSES } from '../rocketchat'; +import { ICredentials, ILoggedUser, IRocketChat, IUser } from '../../../definitions'; import { isIOS } from '../../../utils/deviceInfo'; +import { connectRequest, connectSuccess, disconnect as disconnectAction } from '../../../actions/connect'; +import { updatePermission } from '../../../actions/permissions'; +import EventEmitter from '../../../utils/events'; +import { updateSettings } from '../../../actions/settings'; +import defaultSettings from '../../../constants/settings'; +import getSettings from '../../methods/getSettings'; interface IServices { [index: string]: string | boolean; @@ -21,10 +36,233 @@ interface IServices { service: string; } +// FIXME: Remove `this` context +function connect( + this: IRocketChat, + { server, user, logoutOnError = false }: { server: string; user: IUser; logoutOnError: boolean } +) { + return new Promise(resolve => { + if (sdk.current?.client?.host === server) { + return resolve(); + } + disconnect(); + database.setActiveDB(server); + + store.dispatch(connectRequest()); + + if (this.connectTimeout) { + clearTimeout(this.connectTimeout); + } + + if (this.connectingListener) { + this.connectingListener.then(stopListener); + } + + if (this.connectedListener) { + this.connectedListener.then(stopListener); + } + + if (this.closeListener) { + this.closeListener.then(stopListener); + } + + if (this.usersListener) { + this.usersListener.then(stopListener); + } + + if (this.notifyAllListener) { + this.notifyAllListener.then(stopListener); + } + + if (this.rolesListener) { + this.rolesListener.then(stopListener); + } + + if (this.notifyLoggedListener) { + this.notifyLoggedListener.then(stopListener); + } + + this.unsubscribeRooms(); + + EventEmitter.emit('INQUIRY_UNSUBSCRIBE'); + + sdk.initialize(server); + getSettings(); + + sdk.current + .connect() + .then(() => { + console.log('connected'); + }) + .catch((err: unknown) => { + console.log('connect error', err); + }); + + this.connectingListener = sdk.current.onStreamData('connecting', () => { + store.dispatch(connectRequest()); + }); + + this.connectedListener = sdk.current.onStreamData('connected', () => { + const { connected } = store.getState().meteor; + if (connected) { + return; + } + store.dispatch(connectSuccess()); + const { server: currentServer } = store.getState().server; + if (user?.token && server === currentServer) { + store.dispatch(loginRequest({ resume: user.token }, logoutOnError)); + } + }); + + this.closeListener = sdk.current.onStreamData('close', () => { + store.dispatch(disconnectAction()); + }); + + this.usersListener = sdk.current.onStreamData( + 'users', + protectedFunction((ddpMessage: any) => RocketChat._setUser(ddpMessage)) + ); + + this.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)); + } catch (e) { + log(e); + } + } + }) + ); + + this.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)); + } + }); + + this.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)) { + this.activeUsers = this.activeUsers || {}; + if (!this._setUserTimer) { + this._setUserTimer = setTimeout(() => { + const activeUsersBatch = this.activeUsers; + InteractionManager.runAfterInteractions(() => { + store.dispatch(setActiveUsers(activeUsersBatch)); + }); + this._setUserTimer = null; + return (this.activeUsers = {}); + }, 10000); + } + const userStatus = ddpMessage.fields.args[0]; + const [id, , status, statusText] = userStatus; + this.activeUsers[id] = { status: STATUSES[status], statusText }; + + const { user: loggedUser } = store.getState().login; + if (loggedUser && loggedUser.id === id) { + store.dispatch(setUser({ status: STATUSES[status] as UserStatus, 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 result = sdk.currentLogin?.result; + const result = sdk.current.currentLogin?.result; if (result) { const user: ILoggedUser = { id: result.userId, @@ -123,14 +361,14 @@ async function loginOAuthOrSso(params: ICredentials, isFromWebView = true) { } function abort() { - if (sdk) { - return sdk.abort(); + if (sdk.current) { + return sdk.current.abort(); } return new AbortController(); } function checkAndReopen() { - return sdk.checkAndReopen(); + return sdk.current.checkAndReopen(); } function disconnect() { @@ -185,7 +423,7 @@ async function getWebsocketInfo({ server }: { server: string }) { sdk.initialize(server); try { - await sdk.connect(); + await sdk.current.connect(); } catch (err: any) { if (err.message && err.message.includes('400')) { return { @@ -195,7 +433,7 @@ async function getWebsocketInfo({ server }: { server: string }) { } } - sdk.disconnect(); + sdk.current.disconnect(); return { success: true @@ -264,9 +502,11 @@ export { loginOAuthOrSso, checkAndReopen, abort, + connect, disconnect, getServerInfo, getWebsocketInfo, + stopListener, getLoginServices, determineAuthType }; diff --git a/app/lib/rocketchat/services/sdk.ts b/app/lib/rocketchat/services/sdk.ts index a23f66217..079a79ecc 100644 --- a/app/lib/rocketchat/services/sdk.ts +++ b/app/lib/rocketchat/services/sdk.ts @@ -6,7 +6,7 @@ import { twoFactor } from '../../../utils/twoFactor'; import { useSsl } from '../../../utils/url'; import reduxStore from '../../createStore'; import { Serialized, MatchPathPattern, OperationParams, PathFor, ResultFor } from '../../../definitions/rest/helpers'; -import { ICredentials, ILoginResultFromServer } from '../../../definitions'; +import { ILoginResultFromServer } from '../../../definitions'; class Sdk { private sdk: typeof Rocketchat; @@ -162,22 +162,6 @@ class Sdk { onStreamData(...args: any[]) { return this.sdk.onStreamData(...args); } - - login(credentials: ICredentials) { - return this.sdk.login(credentials); - } - - checkAndReopen() { - return this.sdk.checkAndReopen(); - } - - abort() { - return this.sdk.abort(); - } - - connect() { - return this.sdk.connect(); - } } const sdk = new Sdk();