Chore: Migrate lib/rocketchat.js to TS - structure PoC (#3661)

Co-authored-by: Diego Mello <diegolmello@gmail.com>
Co-authored-by: Gleidson Daniel Silva <gleidson10daniel@hotmail.com>
This commit is contained in:
Gerzon Z 2022-02-10 08:10:42 -04:00 committed by GitHub
parent 977cfd2863
commit a01d281032
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 281 additions and 205 deletions

View File

@ -6,7 +6,7 @@ export interface IServer {
useRealName: boolean; useRealName: boolean;
FileUpload_MediaTypeWhiteList: string; FileUpload_MediaTypeWhiteList: string;
FileUpload_MaxFileSize: number; FileUpload_MaxFileSize: number;
roomsUpdatedAt: Date; roomsUpdatedAt: Date | null;
version: string; version: string;
lastLocalAuthenticatedSession: Date; lastLocalAuthenticatedSession: Date;
autoLock: boolean; autoLock: boolean;

View File

@ -1,5 +1,5 @@
// https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts // https://github.com/RocketChat/Rocket.Chat/blob/develop/definition/ITeam.ts
exports.TEAM_TYPE = { export enum TEAM_TYPE {
PUBLIC: 0, PUBLIC = 0,
PRIVATE: 1 PRIVATE = 1
}; }

View File

@ -0,0 +1,4 @@
import RocketChat, { THEME_PREFERENCES_KEY, CRASH_REPORT_KEY, ANALYTICS_EVENTS_KEY } from './rocketchat';
export { THEME_PREFERENCES_KEY, CRASH_REPORT_KEY, ANALYTICS_EVENTS_KEY };
export default RocketChat;

View File

@ -0,0 +1,23 @@
import database from '../../database';
export default async function clearCache({ server }: { server: string }): Promise<void> {
try {
const serversDB = database.servers;
await serversDB.write(async () => {
const serverCollection = serversDB.get('servers');
const serverRecord = await serverCollection.find(server);
await serverRecord.update(s => {
s.roomsUpdatedAt = null;
});
});
} catch (e) {
// Do nothing
}
try {
const db = database.active;
await db.write(() => db.unsafeResetDatabase());
} catch (e) {
// Do nothing
}
}

View File

@ -0,0 +1,24 @@
import log from '../../../utils/log';
import { TMessageModel, TSubscriptionModel } from '../../../definitions';
import reduxStore from '../../createStore';
import getRoom from './getRoom';
import isGroupChat from './isGroupChat';
type TRoomType = 'p' | 'c' | 'd';
export default async function getPermalinkMessage(message: TMessageModel): Promise<string | null> {
let room: TSubscriptionModel;
try {
room = await getRoom(message.subscription.id);
} catch (e) {
log(e);
return null;
}
const { server } = reduxStore.getState().server;
const roomType = {
p: 'group',
c: 'channel',
d: 'direct'
}[room.t as TRoomType];
return `${server}/${roomType}/${isGroupChat(room) ? room.rid : room.name}?msg=${message.id}`;
}

View File

@ -0,0 +1,12 @@
import { TSubscriptionModel } from '../../../definitions';
import database from '../../database';
export default async function getRoom(rid: string): Promise<TSubscriptionModel> {
try {
const db = database.active;
const room = await db.get('subscriptions').find(rid);
return Promise.resolve(room);
} catch (error) {
return Promise.reject(new Error('Room not found'));
}
}

View File

@ -0,0 +1,5 @@
import { ISubscription, TSubscriptionModel } from '../../../definitions';
export default function isGroupChat(room: ISubscription | TSubscriptionModel): boolean {
return ((room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2)) ?? false;
}

View File

@ -1,64 +1,70 @@
import { InteractionManager } from 'react-native';
import EJSON from 'ejson';
import { settings as RocketChatSettings, Rocketchat as RocketchatClient } from '@rocket.chat/sdk';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import AsyncStorage from '@react-native-community/async-storage';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import AsyncStorage from '@react-native-community/async-storage';
import { Rocketchat as RocketchatClient, settings as RocketChatSettings } from '@rocket.chat/sdk';
import { InteractionManager } from 'react-native';
import RNFetchBlob from 'rn-fetch-blob'; import RNFetchBlob from 'rn-fetch-blob';
import isEmpty from 'lodash/isEmpty'; import { setActiveUsers } from '../../actions/activeUsers';
import { connectRequest, connectSuccess, disconnect } from '../../actions/connect';
import defaultSettings from '../constants/settings'; import { encryptionInit } from '../../actions/encryption';
import log from '../utils/log'; import { loginRequest, setLoginServices, setUser } from '../../actions/login';
import { getBundleId, isIOS } from '../utils/deviceInfo'; import { updatePermission } from '../../actions/permissions';
import fetch from '../utils/fetch'; import { selectServerFailure } from '../../actions/server';
import SSLPinning from '../utils/sslPinning'; import { updateSettings } from '../../actions/settings';
import { encryptionInit } from '../actions/encryption'; import { shareSelectServer, shareSetSettings, shareSetUser } from '../../actions/share';
import { loginRequest, setLoginServices, setUser } from '../actions/login'; import defaultSettings from '../../constants/settings';
import { connectRequest, connectSuccess, disconnect } from '../actions/connect'; import { TEAM_TYPE } from '../../definitions/ITeam';
import { shareSelectServer, shareSetSettings, shareSetUser } from '../actions/share'; import I18n from '../../i18n';
import { getDeviceToken } from '../notifications/push'; import { getDeviceToken } from '../../notifications/push';
import { setActiveUsers } from '../actions/activeUsers'; import { getBundleId, isIOS } from '../../utils/deviceInfo';
import I18n from '../i18n'; import EventEmitter from '../../utils/events';
import { twoFactor } from '../utils/twoFactor'; import fetch from '../../utils/fetch';
import { selectServerFailure } from '../actions/server'; import log from '../../utils/log';
import { useSsl } from '../utils/url'; import SSLPinning from '../../utils/sslPinning';
import EventEmitter from '../utils/events'; import { twoFactor } from '../../utils/twoFactor';
import { updatePermission } from '../actions/permissions'; import { useSsl } from '../../utils/url';
import { TEAM_TYPE } from '../definitions/ITeam'; import database from '../database';
import { updateSettings } from '../actions/settings'; import { sanitizeLikeString } from '../database/utils';
import { compareServerVersion } from './utils'; import { Encryption } from '../encryption';
import { store as reduxStore } from './auxStore'; import triggerBlockAction, { triggerCancel, triggerSubmitView } from '../methods/actions';
import database from './database'; import callJitsi, { callJitsiWithoutServer } from '../methods/callJitsi';
import subscribeRooms from './methods/subscriptions/rooms'; import canOpenRoom from '../methods/canOpenRoom';
import { getUserPresence, subscribeUsersPresence } from './methods/getUsersPresence';
import protectedFunction from './methods/helpers/protectedFunction';
import readMessages from './methods/readMessages';
import getSettings, { getLoginSettings, setSettings, subscribeSettings } from './methods/getSettings';
import getRooms from './methods/getRooms';
import { getPermissions, setPermissions } from './methods/getPermissions';
import { getCustomEmojis, setCustomEmojis } from './methods/getCustomEmojis';
import { import {
getEnterpriseModules, getEnterpriseModules,
hasLicense, hasLicense,
isOmnichannelModuleAvailable, isOmnichannelModuleAvailable,
setEnterpriseModules setEnterpriseModules
} from './methods/enterpriseModules'; } from '../methods/enterpriseModules';
import getSlashCommands from './methods/getSlashCommands'; import { getCustomEmojis, setCustomEmojis } from '../methods/getCustomEmojis';
import { getRoles, onRolesChanged, setRoles } from './methods/getRoles'; import { getPermissions, setPermissions } from '../methods/getPermissions';
import canOpenRoom from './methods/canOpenRoom'; import { getRoles, onRolesChanged, setRoles } from '../methods/getRoles';
import triggerBlockAction, { triggerCancel, triggerSubmitView } from './methods/actions'; import getRooms from '../methods/getRooms';
import loadMessagesForRoom from './methods/loadMessagesForRoom'; import getSettings, { getLoginSettings, setSettings, subscribeSettings } from '../methods/getSettings';
import loadSurroundingMessages from './methods/loadSurroundingMessages'; import getSlashCommands from '../methods/getSlashCommands';
import loadNextMessages from './methods/loadNextMessages'; import protectedFunction from '../methods/helpers/protectedFunction';
import loadMissedMessages from './methods/loadMissedMessages'; import loadMessagesForRoom from '../methods/loadMessagesForRoom';
import loadThreadMessages from './methods/loadThreadMessages'; import loadMissedMessages from '../methods/loadMissedMessages';
import sendMessage, { resendMessage } from './methods/sendMessage'; import loadNextMessages from '../methods/loadNextMessages';
import { cancelUpload, isUploadActive, sendFileMessage } from './methods/sendFileMessage'; import loadSurroundingMessages from '../methods/loadSurroundingMessages';
import callJitsi, { callJitsiWithoutServer } from './methods/callJitsi'; import loadThreadMessages from '../methods/loadThreadMessages';
import logout, { removeServer } from './methods/logout'; import logout, { removeServer } from '../methods/logout';
import UserPreferences from './userPreferences'; import readMessages from '../methods/readMessages';
import { Encryption } from './encryption'; import { cancelUpload, isUploadActive, sendFileMessage } from '../methods/sendFileMessage';
import { sanitizeLikeString } from './database/utils'; import sendMessage, { resendMessage } from '../methods/sendMessage';
import subscribeRooms from '../methods/subscriptions/rooms';
import UserPreferences from '../userPreferences';
import { compareServerVersion } from '../utils';
import { getUserPresence, subscribeUsersPresence } from '../methods/getUsersPresence';
import { store as reduxStore } from '../auxStore';
// Methods
import clearCache from './methods/clearCache';
import getPermalinkMessage from './methods/getPermalinkMessage';
import getRoom from './methods/getRoom';
import isGroupChat from './methods/isGroupChat';
import getUserInfo from './services/getUserInfo';
// Services
import sdk from './services/sdk';
import toggleFavorite from './services/toggleFavorite';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const CURRENT_SERVER = 'currentServer'; const CURRENT_SERVER = 'currentServer';
@ -186,8 +192,7 @@ const RocketChat = {
return this?.sdk?.checkAndReopen(); return this?.sdk?.checkAndReopen();
}, },
disconnect() { disconnect() {
this.sdk?.disconnect?.(); this.sdk = sdk.disconnect();
this.sdk = null;
}, },
connect({ server, user, logoutOnError = false }) { connect({ server, user, logoutOnError = false }) {
return new Promise(resolve => { return new Promise(resolve => {
@ -235,12 +240,7 @@ const RocketChat = {
EventEmitter.emit('INQUIRY_UNSUBSCRIBE'); EventEmitter.emit('INQUIRY_UNSUBSCRIBE');
if (this.code) { this.sdk = sdk.initialize(server);
this.code = null;
}
// The app can't reconnect if reopen interval is 5s while in development
this.sdk = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server), reopen: __DEV__ ? 20000 : 5000 });
this.getSettings(); this.getSettings();
this.sdk this.sdk
@ -415,12 +415,8 @@ const RocketChat = {
// Do nothing // Do nothing
} }
if (this.shareSDK) { this.shareSDK = sdk.disconnect();
this.shareSDK.disconnect(); this.shareSDK = sdk.initialize(server);
this.shareSDK = null;
}
this.shareSDK = new RocketchatClient({ host: server, protocol: 'ddp', useSsl: useSsl(server) });
// set Server // set Server
const currentServer = { server }; const currentServer = { server };
@ -473,10 +469,7 @@ const RocketChat = {
} }
}, },
closeShareExtension() { closeShareExtension() {
if (this.shareSDK) { this.shareSDK = sdk.disconnect();
this.shareSDK.disconnect();
this.shareSDK = null;
}
database.share = null; database.share = null;
reduxStore.dispatch(shareSelectServer({})); reduxStore.dispatch(shareSelectServer({}));
@ -651,27 +644,7 @@ const RocketChat = {
return this.sdk.post('users.removeOtherTokens', { userId }); return this.sdk.post('users.removeOtherTokens', { userId });
}, },
removeServer, removeServer,
async clearCache({ server }) { clearCache,
try {
const serversDB = database.servers;
await serversDB.action(async () => {
const serverCollection = serversDB.get('servers');
const serverRecord = await serverCollection.find(server);
await serverRecord.update(s => {
s.roomsUpdatedAt = null;
});
});
} catch (e) {
// Do nothing
}
try {
const db = database.active;
await db.action(() => db.unsafeResetDatabase());
} catch (e) {
// Do nothing
}
},
registerPushToken() { registerPushToken() {
return new Promise(async resolve => { return new Promise(async resolve => {
const token = getDeviceToken(); const token = getDeviceToken();
@ -991,31 +964,8 @@ const RocketChat = {
reportMessage(messageId) { reportMessage(messageId) {
return this.post('chat.reportMessage', { messageId, description: 'Message reported by user' }); return this.post('chat.reportMessage', { messageId, description: 'Message reported by user' });
}, },
async getRoom(rid) { getRoom,
try { getPermalinkMessage,
const db = database.active;
const room = await db.get('subscriptions').find(rid);
return Promise.resolve(room);
} catch (error) {
return Promise.reject(new Error('Room not found'));
}
},
async getPermalinkMessage(message) {
let room;
try {
room = await RocketChat.getRoom(message.subscription.id);
} catch (e) {
log(e);
return null;
}
const { server } = reduxStore.getState().server;
const roomType = {
p: 'group',
c: 'channel',
d: 'direct'
}[room.t];
return `${server}/${roomType}/${this.isGroupChat(room) ? room.rid : room.name}?msg=${message.id}`;
},
getPermalinkChannel(channel) { getPermalinkChannel(channel) {
const { server } = reduxStore.getState().server; const { server } = reduxStore.getState().server;
const roomType = { const roomType = {
@ -1062,10 +1012,7 @@ const RocketChat = {
// RC 0.62.2 // RC 0.62.2
return this.post('chat.react', { emoji, messageId }); return this.post('chat.react', { emoji, messageId });
}, },
toggleFavorite(roomId, favorite) { toggleFavorite,
// RC 0.64.0
return this.post('rooms.favorite', { roomId, favorite });
},
toggleRead(read, roomId) { toggleRead(read, roomId) {
if (read) { if (read) {
return this.post('subscriptions.unread', { roomId }); return this.post('subscriptions.unread', { roomId });
@ -1091,21 +1038,7 @@ const RocketChat = {
return result?.records; return result?.records;
}, },
methodCallWrapper(method, ...params) { methodCallWrapper(method, ...params) {
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings; return sdk.methodCallWrapper(method, ...params);
const { user } = reduxStore.getState().login;
if (API_Use_REST_For_DDP_Calls) {
const url = isEmpty(user) ? 'method.callAnon' : 'method.call';
return this.post(`${url}/${method}`, {
message: EJSON.stringify({ method, params })
});
}
const parsedParams = params.map(param => {
if (param instanceof Date) {
return { $date: new Date(param).getTime() };
}
return param;
});
return this.methodCall(method, ...parsedParams);
}, },
getUserRoles() { getUserRoles() {
@ -1120,10 +1053,7 @@ const RocketChat = {
// RC 0.48.0 // RC 0.48.0
return this.sdk.get('channels.info', { roomId }); return this.sdk.get('channels.info', { roomId });
}, },
getUserInfo(userId) { getUserInfo,
// RC 0.48.0
return this.sdk.get('users.info', { userId });
},
getUserPreferences(userId) { getUserPreferences(userId) {
// RC 0.62.0 // RC 0.62.0
return this.sdk.get('users.getPreferences', { userId }); return this.sdk.get('users.getPreferences', { userId });
@ -1237,9 +1167,7 @@ const RocketChat = {
return !isUnread; return !isUnread;
}, },
isGroupChat(room) { isGroupChat,
return (room.uids && room.uids.length > 2) || (room.usernames && room.usernames.length > 2);
},
toggleBlockUser(rid, blocked, block) { toggleBlockUser(rid, blocked, block) {
if (block) { if (block) {
@ -1312,62 +1240,10 @@ const RocketChat = {
return this.methodCallWrapper('saveRoomSettings', rid, params); return this.methodCallWrapper('saveRoomSettings', rid, params);
}, },
post(...args) { post(...args) {
return new Promise(async (resolve, reject) => { return sdk.post(...args);
const isMethodCall = args[0]?.startsWith('method.call/');
try {
const result = await this.sdk.post(...args);
/**
* if API_Use_REST_For_DDP_Calls is enabled and it's a method call,
* responses have a different object structure
*/
if (isMethodCall) {
const response = JSON.parse(result.message);
if (response?.error) {
throw response.error;
}
return resolve(response.result);
}
return resolve(result);
} catch (e) {
const errorType = isMethodCall ? e?.error : e?.data?.errorType;
const totpInvalid = 'totp-invalid';
const totpRequired = 'totp-required';
if ([totpInvalid, totpRequired].includes(errorType)) {
const { details } = isMethodCall ? e : e?.data;
try {
await twoFactor({ method: details?.method, invalid: errorType === totpInvalid });
return resolve(this.post(...args));
} catch {
// twoFactor was canceled
return resolve({});
}
} else {
reject(e);
}
}
});
}, },
methodCall(...args) { methodCall(...args) {
return new Promise(async (resolve, reject) => { return sdk.methodCall(...args);
try {
const result = await this.sdk?.methodCall(...args, this.code || '');
return resolve(result);
} catch (e) {
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
const { details } = e;
try {
this.code = await twoFactor({ method: details?.method, invalid: e.error === 'totp-invalid' });
return resolve(this.methodCall(...args));
} catch {
// twoFactor was canceled
return resolve({});
}
} else {
reject(e);
}
}
});
}, },
sendEmailCode() { sendEmailCode() {
const { username } = reduxStore.getState().login.user; const { username } = reduxStore.getState().login.user;

View File

@ -0,0 +1,6 @@
import sdk from './sdk';
export default function getUserInfo(userId: string) {
// RC 0.48.0
return sdk.get('users.info', { userId });
}

View File

@ -0,0 +1,119 @@
import { Rocketchat } from '@rocket.chat/sdk';
import isEmpty from 'lodash/isEmpty';
import EJSON from 'ejson';
import reduxStore from '../../createStore';
import { useSsl } from '../../../utils/url';
import { twoFactor } from '../../../utils/twoFactor';
class Sdk {
private sdk: typeof Rocketchat;
private code: any;
// TODO: We need to stop returning the SDK after all methods are dehydrated
initialize(server: string) {
this.code = null;
// The app can't reconnect if reopen interval is 5s while in development
this.sdk = new Rocketchat({ host: server, protocol: 'ddp', useSsl: useSsl(server), reopen: __DEV__ ? 20000 : 5000 });
return this.sdk;
}
/**
* TODO: evaluate the need for assigning "null" to this.sdk
* I'm returning "null" because we need to remove both instances of this.sdk here and on rocketchat.js
*/
disconnect() {
if (this.sdk) {
this.sdk.disconnect();
this.sdk = null;
}
return null;
}
get(...args: any[]): Promise<any> {
return this.sdk.get(...args);
}
post(...args: any[]): Promise<any> {
return new Promise(async (resolve, reject) => {
const isMethodCall = args[0]?.startsWith('method.call/');
try {
const result = await this.sdk.post(...args);
/**
* if API_Use_REST_For_DDP_Calls is enabled and it's a method call,
* responses have a different object structure
*/
if (isMethodCall) {
const response = JSON.parse(result.message);
if (response?.error) {
throw response.error;
}
return resolve(response.result);
}
return resolve(result);
} catch (e: any) {
const errorType = isMethodCall ? e?.error : e?.data?.errorType;
const totpInvalid = 'totp-invalid';
const totpRequired = 'totp-required';
if ([totpInvalid, totpRequired].includes(errorType)) {
const { details } = isMethodCall ? e : e?.data;
try {
await twoFactor({ method: details?.method, invalid: errorType === totpInvalid });
return resolve(this.post(...args));
} catch {
// twoFactor was canceled
return resolve({});
}
} else {
reject(e);
}
}
});
}
methodCall(...args: any[]): Promise<any> {
return new Promise(async (resolve, reject) => {
try {
const result = await this.sdk?.methodCall(...args, this.code || '');
return resolve(result);
} catch (e: any) {
if (e.error && (e.error === 'totp-required' || e.error === 'totp-invalid')) {
const { details } = e;
try {
this.code = await twoFactor({ method: details?.method, invalid: e.error === 'totp-invalid' });
return resolve(this.methodCall(...args));
} catch {
// twoFactor was canceled
return resolve({});
}
} else {
reject(e);
}
}
});
}
methodCallWrapper(method: string, ...params: any[]): Promise<any> {
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
const { user } = reduxStore.getState().login;
if (API_Use_REST_For_DDP_Calls) {
const url = isEmpty(user) ? 'method.callAnon' : 'method.call';
return this.post(`${url}/${method}`, {
message: EJSON.stringify({ method, params })
});
}
const parsedParams = params.map(param => {
if (param instanceof Date) {
return { $date: new Date(param).getTime() };
}
return param;
});
return this.methodCall(method, ...parsedParams);
}
}
const sdk = new Sdk();
export default sdk;

View File

@ -0,0 +1,6 @@
import sdk from './sdk';
// RC 0.64.0
const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite });
export default toggleFavorite;

View File

@ -13,7 +13,7 @@ import Loading from '../containers/Loading';
import SafeAreaView from '../containers/SafeAreaView'; import SafeAreaView from '../containers/SafeAreaView';
import SearchBox from '../containers/SearchBox'; import SearchBox from '../containers/SearchBox';
import StatusBar from '../containers/StatusBar'; import StatusBar from '../containers/StatusBar';
import { IApplicationState, IBaseScreen } from '../definitions'; import { IApplicationState, IBaseScreen, ISubscription } from '../definitions';
import I18n from '../i18n'; import I18n from '../i18n';
import database from '../lib/database'; import database from '../lib/database';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
@ -265,7 +265,7 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
const data = (search.length > 0 ? search : chats) const data = (search.length > 0 ? search : chats)
// filter DM between multiple users // filter DM between multiple users
.filter(sub => !RocketChat.isGroupChat(sub)); .filter(sub => !RocketChat.isGroupChat(sub as ISubscription));
return ( return (
<FlatList <FlatList

View File

@ -9,6 +9,7 @@ import { useTheme } from '../../theme';
import { isAndroid, isTablet } from '../../utils/deviceInfo'; import { isAndroid, isTablet } from '../../utils/deviceInfo';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { makeThreadName } from '../../utils/room'; import { makeThreadName } from '../../utils/room';
import { ISubscription } from '../../definitions';
const androidMarginLeft = isTablet ? 0 : 4; const androidMarginLeft = isTablet ? 0 : 4;
@ -35,7 +36,7 @@ const styles = StyleSheet.create({
}); });
interface IHeader { interface IHeader {
room: { prid?: string; t?: string }; room: ISubscription;
thread: { id?: string }; thread: { id?: string };
} }