diff --git a/FSV1 TO CLEAN.txt b/FSV1 TO CLEAN.txt new file mode 100644 index 000000000..ec744947f --- /dev/null +++ b/FSV1 TO CLEAN.txt @@ -0,0 +1,69 @@ + +A TESTER +* MENTIONS opengroupv2 + + + + + +lokiPublicChatAPI + + + +dot_net_api +loki_public_chat_api + + + +OpenGroupUtils + +OpenGroup + + + + + +channelAPI.sendMessage + + +getAllOpenGroupV1Conversations => migration to remove opengroupsv1 matching urls + + + + +DELETED +tokenlessFileServerAdnAPI +channelId +publicChat with only a cahnnel of 1 +loki_file_server_api +LokiAppDotNetApi +LokiPublicChatFactoryAPI +lastPublicMessage +getLastRetrievedMessage +setLastRetrievedMessage +attemptConnection +attemptConnectionOneAtATime + +getPublicSendData +setPublicSource +getPublicSource +findOrCreateServer +initAPIs +OpenGroup. +findOrCreateChannel +initSpecialConversations +updateOpenGroupV1 +partChannel +acceptOpenGroupInvitationV1 +pollForChannelOnce +publicConversatio +handleOpenGroupJoinV1 +setChannelName +setChannelAvatar +isOpenGroupV1 +toOpenGroupV1 +setListOfMembers + + +lokiFileServerAPI +lokiFileServerAPIFactory \ No newline at end of file diff --git a/js/background.js b/js/background.js index f27fe2204..7d91e3959 100644 --- a/js/background.js +++ b/js/background.js @@ -6,10 +6,8 @@ storage, textsecure, Whisper, - libsession, libsignal, BlockedNumberController, - libsession, */ // eslint-disable-next-line func-names @@ -91,7 +89,6 @@ window.document.title = window.getTitle(); - let messageReceiver; Whisper.events = _.clone(Backbone.Events); Whisper.events.isListenedTo = eventName => Whisper.events._events ? !!Whisper.events._events[eventName] : false; @@ -100,26 +97,6 @@ window.log.info('Storage fetch'); storage.fetch(); - const initAPIs = () => { - if (window.initialisedAPI) { - return; - } - const ourKey = libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); - // singleton to relay events to libtextsecure/message_receiver - window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); - - // singleton to interface the File server - // If already exists we registered as a secondary device - if (!window.lokiFileServerAPI) { - window.lokiFileServerAPIFactory = new window.LokiFileServerAPI(ourKey); - window.lokiFileServerAPI = window.lokiFileServerAPIFactory.establishHomeConnection( - window.getDefaultFileServer() - ); - } - - window.initialisedAPI = true; - }; - function mapOldThemeToNew(theme) { switch (theme) { case 'dark': @@ -187,10 +164,7 @@ window.libsession.Utils.AttachmentDownloads.stop(); // Stop processing incoming messages - if (messageReceiver) { - await messageReceiver.stopProcessing(); - messageReceiver = null; - } + // FIXME audric stop polling opengroupv2 and swarm nodes // Shut down the data interface cleanly await window.Signal.Data.shutdown(); @@ -489,13 +463,6 @@ window.libsession.Utils.UserUtils.setLastProfileUpdateTimestamp(Date.now()); await window.libsession.Utils.SyncUtils.forceSyncConfigurationNowIfNeeded(true); } - - // inform all your registered public servers - // could put load on all the servers - // if they just keep changing their names without sending messages - // so we could disable this here - // or least it enable for the quickest response - window.lokiPublicChatAPI.setProfileName(newName); }, }); } @@ -513,8 +480,6 @@ window.setSettingValue('link-preview-setting', false); } - // Get memberlist. This function is not accurate >> - // window.getMemberList = window.lokiPublicChatAPI.getListOfMembers(); window.setTheme = newTheme => { $(document.body) .removeClass('dark-theme') @@ -685,9 +650,8 @@ // Clear timer, since we're only called when the timer is expired disconnectTimer = null; - if (messageReceiver) { - await messageReceiver.close(); - } + // FIXME audric stop polling opengroupv2 and swarm nodes + window.libsession.Utils.AttachmentDownloads.stop(); } @@ -717,10 +681,6 @@ return; } - if (messageReceiver) { - await messageReceiver.close(); - } - connectCount += 1; Whisper.Notifications.disable(); // avoid notification flood until empty setTimeout(() => { @@ -728,14 +688,6 @@ }, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000); window.NewReceiver.queueAllCached(); - - initAPIs(); - messageReceiver = new textsecure.MessageReceiver(); - // those handleMessageEvent calls are only used by opengroupv1 - messageReceiver.addEventListener('message', window.DataMessageReceiver.handleMessageEvent); - messageReceiver.addEventListener('sent', window.DataMessageReceiver.handleMessageEvent); - messageReceiver.addEventListener('reconnect', onReconnect); - messageReceiver.addEventListener('configuration', onConfiguration); window.SwarmPolling.addPubkey(window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache()); window.SwarmPolling.start(); @@ -753,32 +705,4 @@ Whisper.Notifications.enable(); } - function onReconnect() { - // We disable notifications on first connect, but the same applies to reconnect. In - // scenarios where we're coming back from sleep, we can get offline/online events - // very fast, and it looks like a network blip. But we need to suppress - // notifications in these scenarios too. So we listen for 'reconnect' events. - Whisper.Notifications.disable(); - - // Enable back notifications once most messages have been fetched - setTimeout(() => { - Whisper.Notifications.enable(); - }, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000); - } - function onConfiguration(ev) { - const { configuration } = ev; - const { readReceipts, typingIndicators, linkPreviews } = configuration; - - storage.put('read-receipt-setting', readReceipts); - - if (typingIndicators === true || typingIndicators === false) { - storage.put('typing-indicators-setting', typingIndicators); - } - - if (linkPreviews === true || linkPreviews === false) { - storage.put('link-preview-setting', linkPreviews); - } - - ev.confirm(); - } })(); diff --git a/js/expire.js b/js/expire.js index f9db04325..e4bb6f48b 100644 --- a/js/expire.js +++ b/js/expire.js @@ -1,4 +1,4 @@ -/* global LokiAppDotNetServerAPI, semver, log */ +/* global semver, log */ // eslint-disable-next-line func-names (function() { 'use strict'; @@ -6,15 +6,6 @@ // hold last result let expiredVersion = null; - window.tokenlessFileServerAdnAPI = new LokiAppDotNetServerAPI( - '', // no pubkey needed - window.getDefaultFileServer() - ); - // use the anonymous access token - window.tokenlessFileServerAdnAPI.token = 'loki'; - // configure for file server comms - window.tokenlessFileServerAdnAPI.getPubKeyForUrl(); - let nextWaitSeconds = 5; const checkForUpgrades = async () => { try { @@ -28,18 +19,13 @@ }, nextWaitSeconds * 1000); // wait a minute return; } - const result = await window.tokenlessFileServerAdnAPI.serverRequest( - 'loki/v1/version/client/desktop' - ); - - if ( - result && - result.response && - result.response.data && - result.response.data.length && - result.response.data[0].length - ) { - const latestVer = semver.clean(result.response.data[0][0]); + let latestVersionWithV; + try { + latestVersionWithV = await window.Fsv2.getLatestDesktopReleaseFileToFsV2(); + if (!latestVersionWithV) { + throw new Error('Invalid latest version. Shceduling retry...'); + } + const latestVer = semver.clean(latestVersionWithV); if (semver.valid(latestVer)) { const ourVersion = window.getVersion(); if (latestVer === ourVersion) { @@ -54,14 +40,18 @@ } } } - } else { - // give it a minute - log.warn('Could not check to see if newer version is available', result); - nextWaitSeconds = 60; - setTimeout(async () => { - await checkForUpgrades(); - }, nextWaitSeconds * 1000); // wait a minute + } catch (e) { + window.log.warn('Failed to fetch latest version'); + log.warn('Could not check to see if newer version is available', latestVersionWithV); } + // wait an hour before retrying + // do this even if we did not get an error before (to be sure to pick up a new release even if + // another request told us we were up to date) + + nextWaitSeconds = 3600; + setTimeout(async () => { + await checkForUpgrades(); + }, nextWaitSeconds * 1000); // no message logged means serverRequest never returned... }; diff --git a/js/modules/loki_app_dot_net_api.d.ts b/js/modules/loki_app_dot_net_api.d.ts index 1026f2f53..94b057854 100644 --- a/js/modules/loki_app_dot_net_api.d.ts +++ b/js/modules/loki_app_dot_net_api.d.ts @@ -1,41 +1,7 @@ -import { Quote, AttachmentPointer, Preview } from '../../ts/session/messages/outgoing'; - -interface UploadResponse { - url: string; - id?: number; -} - export interface LokiAppDotNetServerInterface { - setAvatar(url: any, profileKey: any); - findOrCreateChannel( - api: LokiPublicChatFactoryAPI, - channelId: number, - conversationId: string - ): Promise; - uploadData(data: FormData): Promise; - uploadAvatar(data: FormData): Promise; - putAttachment(data: ArrayBuffer): Promise; - putAvatar(data: ArrayBuffer): Promise; - downloadAttachment(url: String): Promise; serverRequest(endpoint: string): Promise; } -export interface LokiPublicChannelAPI { - banUser(source: string): Promise; - getModerators: () => Promise>; - serverAPI: any; - deleteMessages(arg0: any[]); - sendMessage( - data: { - quote?: Quote; - attachments: Array; - preview: Array; - body?: string; - }, - timestamp: number - ): Promise<{ serverId; serverTimestamp }>; -} - declare class LokiAppDotNetServerAPI implements LokiAppDotNetServerInterface { public baseServerUrl: string; constructor(ourKey: string, url: string); diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index d058f46aa..2108c37de 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -1,43 +1,18 @@ -/* global log, textsecure, libloki, Signal, Whisper, -clearTimeout, getMessageController, libsignal, StringView, window, _, -dcodeIO, Buffer, process */ -const { URL } = require('url'); -const FormData = require('form-data'); -const path = require('path'); -const DataMessage = require('../../ts/receiver/dataMessage'); -const OnionSend = require('../../ts/session/onions/onionSend'); +/* global log, libloki, Signal, StringView, window,, process */ -// Can't be less than 1200 if we have unauth'd requests -const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s -const PUBLICCHAT_CHAN_POLL_EVERY = 20 * 1000; // 20s -const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s -const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s +const OnionSend = require('../../ts/session/onions/onionSend'); -const LOKIFOUNDATION_DEVFILESERVER_PUBKEY = 'BSZiMVxOco/b3sYfaeyiMWv/JnqokxGXkHoclEx8TmZ6'; -const LOKIFOUNDATION_FILESERVER_PUBKEY = 'BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc'; const LOKIFOUNDATION_APNS_PUBKEY = 'BWQqZYWRl0LlotTcUSRJZPvNi8qyt1YSQH3li4EHQNBJ'; const urlPubkeyMap = { - 'https://file-dev.getsession.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY, - 'https://file-dev.lokinet.org': LOKIFOUNDATION_DEVFILESERVER_PUBKEY, - 'https://file.getsession.org': LOKIFOUNDATION_FILESERVER_PUBKEY, - 'https://file.lokinet.org': LOKIFOUNDATION_FILESERVER_PUBKEY, 'https://dev.apns.getsession.org': LOKIFOUNDATION_APNS_PUBKEY, 'https://live.apns.getsession.org': LOKIFOUNDATION_APNS_PUBKEY, }; -const HOMESERVER_USER_ANNOTATION_TYPE = 'network.loki.messenger.homeserver'; -const AVATAR_USER_ANNOTATION_TYPE = 'network.loki.messenger.avatar'; -const SETTINGS_CHANNEL_ANNOTATION_TYPE = 'net.patter-app.settings'; -const MESSAGE_ATTACHMENT_TYPE = 'net.app.core.oembed'; -const LOKI_ATTACHMENT_TYPE = 'attachment'; -const LOKI_PREVIEW_TYPE = 'preview'; - // the core ADN class that handles all communication with a specific server class LokiAppDotNetServerAPI { constructor(ourKey, url) { this.ourKey = ourKey; - this.channels = []; this.tokenPromise = null; this.baseServerUrl = url; log.info(`LokiAppDotNetAPI registered server ${url}`); @@ -47,11 +22,9 @@ class LokiAppDotNetServerAPI { // check token, we're not sure how long we were asleep, token may have expired await this.getOrRefreshServerToken(); // now that we have a working token, start up pollers - this.channels.forEach(channel => channel.open()); } async close() { - this.channels.forEach(channel => channel.stop()); // match sure our pending requests are finished // in case it's still starting up if (this.tokenPromise) { @@ -59,48 +32,6 @@ class LokiAppDotNetServerAPI { } } - // channel getter/factory - async findOrCreateChannel(chatAPI, channelId, conversationId) { - let thisChannel = this.channels.find(channel => channel.channelId === channelId); - if (!thisChannel) { - // make sure we're subscribed - // eventually we'll need to move to account registration/add server - await this.serverRequest(`channels/${channelId}/subscribe`, { - method: 'POST', - }); - thisChannel = new LokiPublicChannelAPI(chatAPI, this, channelId, conversationId); - log.info('LokiPublicChannelAPI started for', channelId, 'on', this.baseServerUrl); - this.channels.push(thisChannel); - } - return thisChannel; - } - - async partChannel(channelId) { - log.info('partChannel', channelId, 'from', this.baseServerUrl); - await this.serverRequest(`channels/${channelId}/subscribe`, { - method: 'DELETE', - }); - this.unregisterChannel(channelId); - } - - // deallocate resources channel uses - unregisterChannel(channelId) { - log.info('unregisterChannel', channelId, 'from', this.baseServerUrl); - let thisChannel; - let i = 0; - for (; i < this.channels.length; i += 1) { - if (this.channels[i].channelId === channelId) { - thisChannel = this.channels[i]; - break; - } - } - if (!thisChannel) { - return; - } - thisChannel.stop(); - this.channels.splice(i, 1); - } - // set up pubKey & pubKeyHex properties // optionally called for mainly file server comms getPubKeyForUrl() { @@ -141,80 +72,6 @@ class LokiAppDotNetServerAPI { return pubKeyAB; } - async setProfileName(profileName) { - // when we add an annotation, may need this - /* - const privKey = await this.getPrivateKey(); - // we might need an annotation that sets the homeserver for media - // better to include this with each attachment... - const objToSign = { - name: profileName, - version: 1, - annotations: [], - }; - const sig = await libsignal.Curve.async.calculateSignature( - privKey, - JSON.stringify(objToSign) - ); - */ - - // You cannot use null to clear the profile name - // the name key has to be set to know what value we want changed - const pName = profileName || ''; - - const res = await this.serverRequest('users/me', { - method: 'PATCH', - objBody: { - name: pName, - }, - }); - // no big deal if it fails... - if (res.err || !res.response || !res.response.data) { - if (res.err) { - log.error(`setProfileName Error ${res.err} ${res.statusCode}`, this.baseServerUrl); - } - return []; - } - - // expecting a user object - return res.response.data.annotations || []; - - // if no profileName should we update the local from the server? - // no because there will be multiple public chat servers - } - - async setHomeServer(homeServer) { - const res = await this.serverRequest('users/me', { - method: 'PATCH', - objBody: { - annotations: [ - { - type: HOMESERVER_USER_ANNOTATION_TYPE, - value: homeServer, - }, - ], - }, - }); - - if (res.err || !res.response || !res.response.data) { - if (res.err) { - log.error(`setHomeServer Error ${res.err}`); - } - return []; - } - - // expecting a user object - return res.response.data.annotations || []; - } - - async setAvatar(url, profileKey) { - let value; // undefined will save bandwidth on the annotation if we don't need it (no avatar) - if (url && profileKey) { - value = { url, profileKey }; - } - return this.setSelfAnnotation(AVATAR_USER_ANNOTATION_TYPE, value); - } - // get active token for this server async getOrRefreshServerToken(forceRefresh = false) { let token; @@ -384,1241 +241,6 @@ class LokiAppDotNetServerAPI { srvPubKey: this.pubKeyHex, }); } - - async getUserAnnotations(pubKey) { - if (!pubKey) { - log.warn('No pubkey provided to getUserAnnotations!'); - return []; - } - const res = await this.serverRequest(`users/@${pubKey}`, { - method: 'GET', - params: { - include_user_annotations: 1, - }, - }); - - if (res.err || !res.response || !res.response.data) { - if (res.err) { - log.error(`getUserAnnotations Error ${res.err}`); - } - return []; - } - - return res.response.data.annotations || []; - } - - async getModerators(channelId) { - if (!channelId) { - log.warn('No channelId provided to getModerators!'); - return []; - } - const res = await this.serverRequest(`loki/v1/channels/${channelId}/moderators`); - - return (!res.err && res.response && res.response.moderators) || []; - } - - async addModerator(pubKeyStr) { - const pubkey = `@${pubKeyStr}`; - const users = await this.getUsers([pubkey]); - const validUsers = users.filter(user => !!user.id); - if (!validUsers || validUsers.length === 0) { - return false; - } - const results = await Promise.all( - validUsers.map(async user => { - log.info(`POSTing loki/v1/moderators/${user.id}`); - const res = await this.serverRequest(`loki/v1/moderators/${user.id}`, { - method: 'POST', - }); - return !!(!res.err && res.response && res.response.data); - }) - ); - - const anyFailures = results.some(test => !test); - if (anyFailures) { - window.log.info('failed to add moderator:', results); - } - return !anyFailures; - } - - async removeModerators(pubKeysParam) { - let pubKeys = pubKeysParam; - if (!Array.isArray(pubKeys)) { - pubKeys = [pubKeys]; - } - pubKeys = pubKeys.map(key => `@${key}`); - const users = await this.getUsers(pubKeys); - const validUsers = users.filter(user => !!user.id); - - const results = await Promise.all( - validUsers.map(async user => { - const res = await this.serverRequest(`loki/v1/moderators/${user.id}`, { - method: 'DELETE', - }); - return !!(!res.err && res.response && res.response.data); - }) - ); - const anyFailures = results.some(test => !test); - if (anyFailures) { - window.log.info('failed to remove moderator:', results); - } - return !anyFailures; - } - - async getSubscribers(channelId, wantObjects) { - if (!channelId) { - log.warn('No channelId provided to getSubscribers!'); - return []; - } - - let res = {}; - if (!Array.isArray(channelId) && wantObjects) { - res = await this.serverRequest(`channels/${channelId}/subscribers`, { - method: 'GET', - params: { - include_user_annotations: 1, - }, - }); - } else { - // not deployed on all backends yet - res.err = 'array subscribers endpoint not yet implemented'; - /* - var list = channelId; - if (!Array.isArray(list)) { - list = [channelId]; - } - const idres = await this.serverRequest(`channels/subscribers/ids`, { - method: 'GET', - params: { - ids: list.join(','), - include_user_annotations: 1, - }, - }); - if (wantObjects) { - if (idres.err || !idres.response || !idres.response.data) { - if (idres.err) { - log.error(`Error ${idres.err}`); - } - return []; - } - const userList = []; - await Promise.all(idres.response.data.map(async channelId => { - const channelUserObjs = await this.getUsers(idres.response.data[channelId]); - userList.push(...channelUserObjs); - })); - res = { - response: { - meta: { - code: 200, - }, - data: userList - } - } - } else { - res = idres; - } - */ - } - - if (res.err || !res.response || !res.response.data) { - if (res.err) { - log.error(`getSubscribers Error ${res.err}`); - } - return []; - } - - return res.response.data || []; - } - - async getUsers(pubKeys) { - if (!pubKeys) { - log.warn('No pubKeys provided to getUsers!'); - return []; - } - // ok to call without - if (!pubKeys.length) { - return []; - } - if (pubKeys.length > 200) { - log.warn('Too many pubKeys given to getUsers!'); - } - const res = await this.serverRequest('users', { - method: 'GET', - params: { - ids: pubKeys.join(','), - include_user_annotations: 1, - }, - }); - - if (res.err || !res.response || !res.response.data) { - if (res.err) { - log.error(`loki_app_dot_net:::getUsers - Error: ${res.err} for ${pubKeys.join(',')}`); - } - return []; - } - - return res.response.data || []; - } - - // Only one annotation at a time - async setSelfAnnotation(type, value) { - const annotation = { type }; - - // to delete annotation, omit the "value" field - if (value) { - annotation.value = value; - } - - const res = await this.serverRequest('users/me', { - method: 'PATCH', - objBody: { - annotations: [annotation], - }, - }); - - if (!res.err && res.response) { - return res.response; - } - - return false; - } - - async uploadAvatar(data) { - const endpoint = 'users/me/avatar'; - - const options = { - method: 'POST', - rawBody: data, - }; - - const { response, ok } = await this.serverRequest(endpoint, options); - - if (!ok) { - throw new Error(`Failed to upload avatar to ${this.baseServerUrl}`); - } - - const url = response.data && response.data.avatar_image && response.data.avatar_image.url; - - if (!url) { - throw new Error(`Failed to upload data: Invalid url.`); - } - - // We don't use the server id for avatars - return { - url, - id: undefined, - }; - } - - // for avatar - async uploadData(data) { - const endpoint = 'files'; - const options = { - method: 'POST', - rawBody: data, - }; - - const { ok, response } = await this.serverRequest(endpoint, options); - if (!ok) { - throw new Error(`Failed to upload data to server: ${this.baseServerUrl}`); - } - - const url = response.data && response.data.url; - const id = response.data && response.data.id; - - if (!url || !id) { - throw new Error(`Failed to upload data: Invalid url or id returned.`); - } - - return { - url, - id, - }; - } - - // for files - putAttachment(attachmentBin) { - const formData = new FormData(); - const buffer = Buffer.from(attachmentBin); - formData.append('type', 'network.loki'); - formData.append('content', buffer, { - contentType: 'application/octet-stream', - name: 'content', - filename: 'attachment', - knownLength: buffer.byteLength, - }); - - return this.uploadData(formData); - } - - putAvatar(buf) { - const formData = new FormData(); - const buffer = Buffer.from(buf); - formData.append('avatar', buffer, { - contentType: 'application/octet-stream', - name: 'avatar', - filename: 'attachment', - }); - return this.uploadAvatar(formData); - } - - // This should return Uint8Array in response.data - async downloadAttachment(url) { - const endpoint = new URL(url).pathname; - - // With the new protocol, there is no json in body, we shouldn't try to parse it - const noJson = window.lokiFeatureFlags.useFileOnionRequestsV2; - - const res = await this.serverRequest(`loki/v1${endpoint}`, { - method: 'GET', - noJson, - }); - - if (window.lokiFeatureFlags.useFileOnionRequestsV2) { - const buffer = dcodeIO.ByteBuffer.fromBase64(res.response).toArrayBuffer(); - return buffer; - } - return new Uint8Array(res.response.data).buffer; - } -} - -// functions to a specific ADN channel on an ADN server -class LokiPublicChannelAPI { - constructor(_, serverAPI, channelId, conversationId) { - // properties - this.serverAPI = serverAPI; - this.channelId = channelId; - this.baseChannelUrl = `channels/${this.channelId}`; - this.conversationId = conversationId; - this.conversation = window.getConversationController().getOrThrow(conversationId); - this.lastMessageServerID = null; - this.modStatus = false; - this.deleteLastId = 1; - this.timers = {}; - this.myPrivateKey = false; - this.messagesPollLock = false; - - // can escalated to SQL if it start uses too much memory - this.logMop = {}; - - // Cache for duplicate checking - this.lastMessagesCache = []; - - // end properties - - log.info(`registered LokiPublicChannel ${channelId} on ${this.serverAPI.baseServerUrl}`); - // start polling - this.open(); - } - - async getPrivateKey() { - if (!this.myPrivateKey) { - const item = await window.Signal.Data.getItemById('identityKey'); - const keyPair = (item && item.value) || undefined; - if (!keyPair) { - window.log.error('Could not get our Keypair from getItemById'); - } - this.myPrivateKey = keyPair.privKey; - } - return this.myPrivateKey; - } - - async banUser(pubkey) { - const res = await this.serverRequest(`loki/v1/moderation/blacklist/@${pubkey}`, { - method: 'POST', - }); - - if (res.err || !res.response || !res.response.data) { - if (res.err) { - log.error(`banUser Error ${res.err}`); - } - return false; - } - - return true; - } - - open() { - log.info(`LokiPublicChannel open ${this.channelId} on ${this.serverAPI.baseServerUrl}`); - if (this.running) { - log.warn( - `LokiPublicChannel already open ${this.channelId} on ${this.serverAPI.baseServerUrl}` - ); - } - this.running = true; - if (!this.timers.channel) { - this.pollForChannel(); - } - if (!this.timers.moderator) { - this.pollForModerators(); - } - if (!this.timers.delete) { - this.pollForDeletions(); - } - if (!this.timers.message) { - this.pollForMessages(); - } - // TODO: poll for group members here? - } - - stop() { - log.info(`LokiPublicChannel close ${this.channelId} on ${this.serverAPI.baseServerUrl}`); - if (!this.running) { - log.warn( - `LokiPublicChannel already open ${this.channelId} on ${this.serverAPI.baseServerUrl}` - ); - } - this.running = false; - if (this.timers.channel) { - clearTimeout(this.timers.channel); - this.timers.channel = false; - } - if (this.timers.moderator) { - clearTimeout(this.timers.moderator); - this.timers.moderator = false; - } - if (this.timers.delete) { - clearTimeout(this.timers.delete); - this.timers.delete = false; - } - if (this.timers.message) { - clearTimeout(this.timers.message); - this.timers.message = false; - } - } - - serverRequest(endpoint, options = {}) { - return this.serverAPI.serverRequest(endpoint, options); - } - - getSubscribers() { - return this.serverAPI.getSubscribers(this.channelId, true); - } - - getModerators() { - return this.serverAPI.getModerators(this.channelId); - } - - // get moderation actions - async pollForModerators() { - try { - await this.pollOnceForModerators(); - } catch (e) { - log.warn('Error while polling for public chat moderators:', e.code, e.message); - } - if (this.running) { - this.timers.moderator = setTimeout(() => { - this.pollForModerators(); - }, PUBLICCHAT_MOD_POLL_EVERY); - } - } - - // get moderator status - async pollOnceForModerators() { - // get moderator status - const res = await this.serverRequest(`loki/v1/channels/${this.channelId}/moderators`); - const ourNumberDevice = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); - - // Get the list of moderators if no errors occurred - const moderators = !res.err && res.response && res.response.moderators; - - // if we encountered problems then we'll keep the old mod status - if (moderators) { - this.modStatus = moderators.includes(ourNumberDevice); - } - - if (this.running) { - await this.conversation.updateGroupAdmins(moderators || []); - } - } - - async setChannelSettings(settings) { - if (!this.modStatus) { - // need moderator access to set this - log.warn('Need moderator access to setChannelName'); - return false; - } - // racy! - const res = await this.serverRequest(this.baseChannelUrl, { - params: { include_annotations: 1 }, - }); - if (res.err) { - // state unknown - log.warn(`public chat channel state unknown, skipping set: ${res.err}`); - return false; - } - let notes = res.response && res.response.data && res.response.data.annotations; - if (!notes) { - // ok if nothing is set yet - notes = []; - } - let settingNotes = notes.filter(note => note.type === SETTINGS_CHANNEL_ANNOTATION_TYPE); - if (!settingNotes) { - // default name, description, avatar - settingNotes = [ - { - type: SETTINGS_CHANNEL_ANNOTATION_TYPE, - value: { - name: 'Your Public Chat', - description: 'Your public chat room', - avatar: null, - }, - }, - ]; - } - // update settings - settingNotes[0].value = Object.assign(settingNotes[0].value, settings); - // commit settings - const updateRes = await this.serverRequest(`loki/v1/${this.baseChannelUrl}`, { - method: 'PUT', - objBody: { annotations: settingNotes }, - }); - if (updateRes.err || !updateRes.response || !updateRes.response.data) { - if (updateRes.err) { - log.error(`setChannelSettings Error ${updateRes.err}`); - } - return false; - } - return true; - } - - // Do we need this? They definitely make it more clear... - setChannelName(name) { - return this.setChannelSettings({ name }); - } - setChannelDescription(description) { - return this.setChannelSettings({ description }); - } - setChannelAvatar(avatar) { - return this.setChannelSettings({ avatar }); - } - - // delete messages on the server - async deleteMessages(serverIds, canThrow = false) { - const res = await this.serverRequest( - this.modStatus ? `loki/v1/moderation/messages` : `loki/v1/messages`, - { method: 'DELETE', params: { ids: serverIds } } - ); - if (!res.err) { - const deletedIds = res.response.data.filter(d => d.is_deleted).map(d => d.id); - - if (deletedIds.length > 0) { - log.info(`deleted ${serverIds} on ${this.baseChannelUrl}`); - } - - const failedIds = res.response.data.filter(d => !d.is_deleted).map(d => d.id); - - if (failedIds.length > 0) { - log.warn(`failed to delete ${failedIds} on ${this.baseChannelUrl}`); - } - - // Note: if there is no entry for message, we assume it wasn't found - // on the server, so it is not treated as explicitly failed - const ignoredIds = _.difference(serverIds, _.union(failedIds, deletedIds)); - - if (ignoredIds.length > 0) { - log.warn(`No response for ${ignoredIds} on ${this.baseChannelUrl}`); - } - - return { deletedIds, ignoredIds }; - } - if (canThrow) { - throw new textsecure.PublicChatError('Failed to delete public chat message'); - } - return { deletedIds: [], ignoredIds: [] }; - } - - // used for sending messages - getEndpoint() { - const endpoint = `${this.serverAPI.baseServerUrl}/${this.baseChannelUrl}/messages`; - return endpoint; - } - - // get moderation actions - async pollForChannel() { - try { - await this.pollForChannelOnce(); - } catch (e) { - log.warn('Error while polling for public chat room details', e.code, e.message); - } - if (this.running) { - this.timers.channel = setTimeout(() => { - this.pollForChannel(); - }, PUBLICCHAT_CHAN_POLL_EVERY); - } - } - - // update room details - async pollForChannelOnce() { - const res = await this.serverRequest(`${this.baseChannelUrl}`, { - params: { - include_annotations: 1, - }, - }); - - if (res.err || !res.response || !res.response.data) { - if (res.statusCode === 403) { - // token is now invalid - this.serverAPI.getOrRefreshServerToken(true); - } - return; - } - if (!this.running) { - return; - } - - const { data } = res.response; - - if (data.annotations && data.annotations.length) { - // get our setting note - const settingNotes = data.annotations.filter( - note => note.type === SETTINGS_CHANNEL_ANNOTATION_TYPE - ); - const note = settingNotes && settingNotes.length ? settingNotes[0] : {}; - // setting_note.value.description only needed for directory - if (note.value && note.value.name) { - this.conversation.setGroupName(note.value.name); - } - if (note.value && note.value.avatar) { - if (note.value.avatar.match(/^images\//)) { - // local file avatar - const resolvedAvatar = path.normalize(note.value.avatar); - const base = path.normalize('images/'); - const re = new RegExp(`^${base}`); - // do we at least ends up inside images/ somewhere? - if (re.test(resolvedAvatar)) { - this.conversation.set('avatar', resolvedAvatar); - } - } else { - // relative URL avatar - const avatarAbsUrl = this.serverAPI.baseServerUrl + note.value.avatar; - const { writeNewAttachmentData, deleteAttachmentData } = window.Signal.Migrations; - // do we already have this image? no, then - - // download a copy and save it - const imageData = await this.serverAPI.downloadAttachment(avatarAbsUrl); - - const newAttributes = await window.Signal.Types.Conversation.maybeUpdateAvatar( - this.conversation.attributes, - imageData, - { - writeNewAttachmentData, - deleteAttachmentData, - } - ); - // update group - this.conversation.set('avatar', newAttributes.avatar); - } - } - // is it mutable? - // who are the moderators? - // else could set a default in case of server problems... - } - - if (data.counts && Number.isInteger(data.counts.subscribers)) { - this.conversation.setSubscriberCount(data.counts.subscribers); - } - await this.conversation.commit(); - } - - // get moderation actions - async pollForDeletions() { - try { - await this.pollOnceForDeletions(); - } catch (e) { - log.warn('Error while polling for public chat deletions:', e.code, e.message); - } - if (this.running) { - this.timers.delete = setTimeout(() => { - this.pollForDeletions(); - }, PUBLICCHAT_DELETION_POLL_EVERY); - } - } - - async pollOnceForDeletions() { - // grab the last 200 deletions - const params = { - count: 200, - }; - - // start loop - let more = true; - while (more) { - // set params to from where we last checked - params.since_id = this.deleteLastId; - - // grab the next 200 deletions from where we last checked - // eslint-disable-next-line no-await-in-loop - const res = await this.serverRequest(`loki/v1/channel/${this.channelId}/deletes`, { params }); - - // if any problems, abort out - if (res.err || !res.response || !res.response.data || !res.response.meta) { - if (res.statusCode === 403) { - // token is now invalid - this.serverAPI.getOrRefreshServerToken(true); - } - if (res.err) { - log.error(`pollOnceForDeletions Error ${res.err}`); - } else { - log.error(`pollOnceForDeletions Error: Received incorrect response ${res.response}`); - } - break; - } - - // Process results - const entries = res.response.data || []; - if (entries.length > 0) { - Whisper.events.trigger('deleteLocalPublicMessages', { - messageServerIds: entries.reverse().map(e => e.message_id), - conversationId: this.conversationId, - }); - } - - // update where we last checked - this.deleteLastId = res.response.meta.max_id; - more = res.response.meta.more && res.response.data.length >= params.count && this.running; - } - } - - static getSigData(sigVer, noteValue, attachmentAnnotations, previewAnnotations, adnMessage) { - let sigString = ''; - sigString += adnMessage.text.trim(); - sigString += noteValue.timestamp; - if (noteValue.quote) { - sigString += noteValue.quote.id; - sigString += noteValue.quote.author; - sigString += noteValue.quote.text.trim(); - if (adnMessage.reply_to) { - sigString += adnMessage.reply_to; - } - } - sigString += [...attachmentAnnotations, ...previewAnnotations] - .map(data => data.id || (data.image && data.image.id)) - .sort() - .join(''); - sigString += sigVer; - - return dcodeIO.ByteBuffer.wrap(sigString, 'utf8').toArrayBuffer(); - } - - async getMessengerData(adnMessage) { - if (!Array.isArray(adnMessage.annotations) || adnMessage.annotations.length === 0) { - return false; - } - const noteValue = adnMessage.annotations[0].value; - - // signatures now required - if (!noteValue.sig || typeof noteValue.sig !== 'string') { - return false; - } - - // timestamp is the only required field we've had since the first deployed version - const { timestamp, quote } = noteValue; - - let profileKey = null; - let avatar = null; - const avatarNote = adnMessage.user.annotations.find( - note => note.type === AVATAR_USER_ANNOTATION_TYPE - ); - if (avatarNote) { - ({ profileKey, url: avatar } = avatarNote.value); - } - - if (quote) { - // Disable quote attachments - quote.attachments = []; - } - - // try to verify signature - const { sig, sigver } = noteValue; - const annoCopy = [...adnMessage.annotations]; - const attachments = annoCopy - .filter(anno => anno.value.lokiType === LOKI_ATTACHMENT_TYPE) - .map(attachment => ({ isRaw: true, ...attachment.value })); - const preview = annoCopy - .filter(anno => anno.value.lokiType === LOKI_PREVIEW_TYPE) - .map(LokiPublicChannelAPI.getPreviewFromAnnotation); - // strip out sig and sigver - annoCopy[0] = _.omit(annoCopy[0], ['value.sig', 'value.sigver']); - const sigData = LokiPublicChannelAPI.getSigData( - sigver, - noteValue, - attachments, - preview, - adnMessage - ); - - const pubKeyBin = StringView.hexToArrayBuffer(adnMessage.user.username); - const sigBin = StringView.hexToArrayBuffer(sig); - try { - await libsignal.Curve.async.verifySignature(pubKeyBin, sigData, sigBin); - } catch (e) { - if (e.message === 'Invalid signature') { - // keep noise out of the logs, once per start up is enough - if (this.logMop[adnMessage.id] === undefined) { - log.warn( - 'Invalid or missing signature on ', - this.serverAPI.baseServerUrl, - this.channelId, - adnMessage.id, - 'says', - adnMessage.text, - 'from', - adnMessage.user.username, - 'signature', - sig, - 'signature version', - sigver - ); - this.logMop[adnMessage.id] = true; - } - // we now only accept valid messages into the public chat - return false; - } - // any error should cause problem - log.error(`Unhandled message signature validation error ${e.message}`); - return false; - } - - return { - timestamp, - serverTimestamp: new Date(`${adnMessage.created_at}`).getTime() || timestamp, - attachments, - preview, - quote, - avatar, - text: adnMessage.text, - profileKey, - }; - } - - // get channel messages - async pollForMessages() { - try { - await this.pollOnceForMessages(); - } catch (e) { - log.warn('Error while polling for public chat messages:', e.code, e.message); - } - if (this.running) { - this.timers.message = setTimeout(() => { - this.pollForMessages(); - }, PUBLICCHAT_MSG_POLL_EVERY); - } - } - - async pollOnceForMessages() { - if (this.messagesPollLock) { - // TODO: check if lock is stale - log.warn( - 'pollOnceForModerators locked', - 'on', - this.channelId, - 'at', - this.serverAPI.baseServerUrl - ); - return; - } - // disable locking system for now as it's not quite perfect yet - // this.messagesPollLock = Date.now(); - - const params = { - include_annotations: 1, - include_user_annotations: 1, // to get the home server - include_deleted: false, - }; - if (!this.conversation) { - log.warn('Trying to poll for non-existing public conversation'); - this.lastMessageServerID = 0; - } else if (!this.lastMessageServerID) { - this.lastMessageServerID = this.conversation.getLastRetrievedMessage(); - } - // If lastMessageServerID is not set, it's the first pull of messages for this open group. - // We just pull 100 messages (server sends the most recent ones) - if (!this.lastMessageServerID || this.lastMessageServerID === 0) { - params.count = 100; - } else { - // if lastMessageServerID is set, we pull 200 messages per 200 messages, giving the since_id parameter set to our last received message id. - params.count = 200; - params.since_id = this.lastMessageServerID; - } - // log.info(`Getting ${params.count} from ${this.lastMessageServerID} on ${this.baseChannelUrl}`); - const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, { - params, - }); - - if (res.err || !res.response) { - if (res.statusCode === 403) { - // token is now invalid - this.serverAPI.getOrRefreshServerToken(true); - } - log.error( - `app_dot_net:::pollOnceForMessages - Could not get messages from`, - this.serverAPI.baseServerUrl, - this.baseChannelUrl - ); - if (res.err) { - log.error(`app_dot_net:::pollOnceForMessages - receive error`, res.err); - } - this.messagesPollLock = false; - return; - } - - let receivedAt = new Date().getTime(); - const homeServerPubKeys = {}; - let pendingMessages = []; - - // get our profile name - const ourNumberDevice = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); - // if no primaryDevicePubKey fall back to ourNumberDevice - const ourNumberProfile = window.storage.get('primaryDevicePubKey') || ourNumberDevice; - let lastProfileName = false; - - // the signature forces this to be async - pendingMessages = await Promise.all( - // process these in chronological order - res.response.data.reverse().map(async adnMessage => { - // still update our last received if deleted, not signed or not valid - this.lastMessageServerID = !this.lastMessageServerID - ? adnMessage.id - : Math.max(this.lastMessageServerID, adnMessage.id); - - if ( - !adnMessage.id || - !adnMessage.user || - !adnMessage.user.username || // pubKey lives in the username field - !adnMessage.text || - adnMessage.is_deleted - ) { - return false; // Invalid or delete message - } - - const pubKey = adnMessage.user.username; - try { - const messengerData = await this.getMessengerData(adnMessage); - - if (messengerData === false) { - return false; - } - // eslint-disable-next-line no-param-reassign - adnMessage.timestamp = messengerData.timestamp; - // eslint-disable-next-line no-param-reassign - adnMessage.body = messengerData.text; - const { - timestamp, - serverTimestamp, - quote, - attachments, - preview, - avatar, - profileKey, - } = messengerData; - if (!timestamp) { - return false; // Invalid message - } - - // Duplicate check - // message is one of the object of this.lastMessagesCache - // testedMessage is the adnMessage object - const isDuplicate = (message, testedMessage) => - DataMessage.isDuplicate(message, testedMessage, testedMessage.user.username); - const isThisMessageDuplicate = this.lastMessagesCache.some(m => - isDuplicate(m, adnMessage) - ); - - // Filter out any messages that we got previously - if (isThisMessageDuplicate) { - return false; // Duplicate message - } - - // Add the message to the lastMessage cache and keep the last 5 recent messages - this.lastMessagesCache = [ - ...this.lastMessagesCache, - { - attributes: { - source: pubKey, - body: adnMessage.text, - sent_at: timestamp, - serverId: adnMessage.id, - }, - }, - ].splice(-5); - const from = adnMessage.user.name || 'Anonymous'; // profileName - - // if us - if (pubKey === ourNumberProfile || pubKey === ourNumberDevice) { - // update the last name we saw from ourself - lastProfileName = from; - } - - // track sources for multidevice support - // sort it by home server - let homeServer = window.getDefaultFileServer(); - if (adnMessage.user && adnMessage.user.annotations.length) { - const homeNotes = adnMessage.user.annotations.filter( - note => note.type === HOMESERVER_USER_ANNOTATION_TYPE - ); - // FIXME: this annotation should probably be signed and verified... - homeServer = homeNotes.reduce( - (curVal, note) => (note.value ? note.value : curVal), - homeServer - ); - } - if (homeServerPubKeys[homeServer] === undefined) { - homeServerPubKeys[homeServer] = []; - } - if (homeServerPubKeys[homeServer].indexOf(`@${pubKey}`) === -1) { - homeServerPubKeys[homeServer].push(`@${pubKey}`); - } - - // generate signal message object - const messageData = { - serverId: adnMessage.id, - clientVerified: true, - source: pubKey, - sourceDevice: 1, - timestamp, // sender timestamp - - serverTimestamp, // server created_at, used to order messages - receivedAt, - isPublic: true, - message: { - body: adnMessage.text === timestamp.toString() ? '' : adnMessage.text, - attachments, - group: { - id: this.conversationId, - type: textsecure.protobuf.GroupContext.Type.DELIVER, - }, - flags: 0, - expireTimer: 0, - profileKey, - timestamp, - received_at: receivedAt, - sent_at: timestamp, // sender timestamp inner - quote, - contact: [], - preview, - profile: { - displayName: from, - avatar, - }, - }, - }; - receivedAt += 1; // Ensure different arrival times - - // now process any user meta data updates - // - update their conversation with a potentially new avatar - return messageData; - } catch (e) { - window.log.error('pollOnceForMessages: caught error:', e); - return false; - } - }) - ); - // return early if we should stop processing - if (!pendingMessages.length || !this.running) { - this.conversation.setLastRetrievedMessage(this.lastMessageServerID); - this.messagesPollLock = false; - return; - } - - // filter out invalid messages - pendingMessages = pendingMessages.filter(messageData => !!messageData); - - // process all messages in the order received - - // trigger the handling of those messages sequentially - - // eslint-disable-next-line no-plusplus - for (let index = 0; index < pendingMessages.length; index++) { - if (this.running) { - // log.info( - // 'emitting pending public message', - // pendingMessages[index].serverId, - // 'on', - // this.channelId, - // 'at', - // this.serverAPI.baseServerUrl - // ); - // eslint-disable-next-line no-await-in-loop - window.NewReceiver.handlePublicMessage(pendingMessages[index]); - } - } - - /* eslint-enable no-param-reassign */ - - // if we received one of our own messages - if (lastProfileName !== false) { - // get current profileName - const profileConvo = window.getConversationController().get(ourNumberProfile); - const profileName = profileConvo.getProfileName(); - // check to see if it out of sync - if (profileName !== lastProfileName) { - // out of sync, update this server - this.serverAPI.setProfileName(profileName); - } - } - - // finally update our position - this.conversation.setLastRetrievedMessage(this.lastMessageServerID); - this.messagesPollLock = false; - } - - static getPreviewFromAnnotation(annotation) { - const preview = { - title: annotation.value.linkPreviewTitle, - url: annotation.value.linkPreviewUrl, - image: { - isRaw: true, - caption: annotation.value.caption, - contentType: annotation.value.contentType, - digest: annotation.value.digest, - fileName: annotation.value.fileName, - flags: annotation.value.flags, - height: annotation.value.height, - id: annotation.value.id, - key: annotation.value.key, - size: annotation.value.size, - thumbnail: annotation.value.thumbnail, - url: annotation.value.url, - width: annotation.value.width, - }, - }; - return preview; - } - - static getAnnotationFromPreview(preview) { - const annotation = { - type: MESSAGE_ATTACHMENT_TYPE, - value: { - // Mandatory ADN fields - version: '1.0', - lokiType: LOKI_PREVIEW_TYPE, - - // Signal stuff we actually care about - linkPreviewTitle: preview.title, - linkPreviewUrl: preview.url, - caption: (preview.image && preview.image.caption) || undefined, - contentType: (preview.image && preview.image.contentType) || undefined, - digest: (preview.image && preview.image.digest) || undefined, - fileName: (preview.image && preview.image.fileName) || undefined, - flags: (preview.image && preview.image.flags) || undefined, - height: (preview.image && preview.image.height) || undefined, - id: (preview.image && preview.image.id) || undefined, - key: (preview.image && preview.image.key) || undefined, - size: (preview.image && preview.image.size) || undefined, - thumbnail: (preview.image && preview.image.thumbnail) || undefined, - url: (preview.image && preview.image.url) || undefined, - width: (preview.image && preview.image.width) || undefined, - }, - }; - return annotation; - } - - static getAnnotationFromAttachment(attachment) { - let type; - if (attachment.contentType.match(/^image/)) { - type = 'photo'; - } else if (attachment.contentType.match(/^video/)) { - type = 'video'; - } else if (attachment.contentType.match(/^audio/)) { - type = 'audio'; - } else { - type = 'other'; - } - const annotation = { - type: MESSAGE_ATTACHMENT_TYPE, - value: { - // Mandatory ADN fields - version: '1.0', - type, - lokiType: LOKI_ATTACHMENT_TYPE, - - // Signal stuff we actually care about - ...attachment, - }, - }; - return annotation; - } - - // create a message in the channel - async sendMessage(data, messageTimeStamp) { - const { quote, attachments, preview } = data; - const text = data.body || messageTimeStamp.toString(); - const attachmentAnnotations = attachments.map(LokiPublicChannelAPI.getAnnotationFromAttachment); - const previewAnnotations = preview.map(LokiPublicChannelAPI.getAnnotationFromPreview); - - const payload = { - text, - annotations: [ - { - type: 'network.loki.messenger.publicChat', - value: { - timestamp: messageTimeStamp, - }, - }, - ...attachmentAnnotations, - ...previewAnnotations, - ], - }; - - if (quote && quote.id) { - payload.annotations[0].value.quote = quote; - - // copied from model/message.js copyFromQuotedMessage - const collection = await Signal.Data.getMessagesBySentAt(quote.id); - const found = collection.find(item => { - const messageAuthor = item.getContact(); - - return messageAuthor && quote.author === messageAuthor.id; - }); - - if (found) { - const queryMessage = getMessageController().register(found.id, found); - const replyTo = queryMessage.get('serverId'); - if (replyTo) { - payload.reply_to = replyTo; - } - } - } - const privKey = await this.getPrivateKey(); - const sigVer = 1; - const mockAdnMessage = { text }; - if (payload.reply_to) { - mockAdnMessage.reply_to = payload.reply_to; - } - const sigData = LokiPublicChannelAPI.getSigData( - sigVer, - payload.annotations[0].value, - attachmentAnnotations.map(anno => anno.value), - previewAnnotations.map(anno => anno.value), - mockAdnMessage - ); - const sig = await libsignal.Curve.async.calculateSignature(privKey, sigData); - payload.annotations[0].value.sig = StringView.arrayBufferToHex(sig); - payload.annotations[0].value.sigver = sigVer; - const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, { - method: 'POST', - objBody: payload, - }); - if (!res.err && res.response) { - return { - serverId: res.response.data.id, - serverTimestamp: new Date(`${res.response.data.created_at}`).getTime(), - }; - } - if (res.err) { - log.error(`POST ${this.baseChannelUrl}/messages failed`); - if (res.response && res.response.meta && res.response.meta.code === 401) { - log.error(`Got invalid token for ${this.serverAPI.token}`); - } - log.error(res.err); - log.error(res.response); - } else { - log.warn(res.response); - } - - return { serverId: -1, serverTimestamp: -1 }; - } } LokiAppDotNetServerAPI.serverRequest = OnionSend.serverRequest; diff --git a/js/modules/loki_file_server_api.d.ts b/js/modules/loki_file_server_api.d.ts deleted file mode 100644 index 52b3863b3..000000000 --- a/js/modules/loki_file_server_api.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface LokiFileServerInstance { - downloadAttachment(url: string): Promise; -} diff --git a/js/modules/loki_file_server_api.js b/js/modules/loki_file_server_api.js deleted file mode 100644 index 60935bfce..000000000 --- a/js/modules/loki_file_server_api.js +++ /dev/null @@ -1,89 +0,0 @@ -/* global log, window */ -/* global log: false */ - -const LokiAppDotNetAPI = require('./loki_app_dot_net_api'); - -// can have multiple of these instances as each user can have a -// different home server -class LokiFileServerInstance { - constructor(ourKey) { - this.ourKey = ourKey; - } - - // FIXME: this is not file-server specific - // and is currently called by LokiAppDotNetAPI. - // LokiAppDotNetAPI (base) should not know about LokiFileServer. - async establishConnection(serverUrl, options) { - // why don't we extend this? - this._server = new LokiAppDotNetAPI(this.ourKey, serverUrl); - - // make sure pubKey & pubKeyHex are set in _server - this.pubKey = this._server.getPubKeyForUrl(); - - if (options !== undefined && options.skipToken) { - return; - } - - // get a token for multidevice - const gotToken = await this._server.getOrRefreshServerToken(); - // TODO: Handle this failure gracefully - if (!gotToken) { - log.error('You are blacklisted form this home server'); - } - } - - // for files - async downloadAttachment(url) { - return this._server.downloadAttachment(url); - } -} - -// extends LokiFileServerInstance with functions we'd only perform on our own home server -// so we don't accidentally send info to the wrong file server -class LokiHomeServerInstance extends LokiFileServerInstance { - // you only upload to your own home server - // you can download from any server... - uploadAvatar(data) { - if (!this._server.token) { - log.warn('uploadAvatar no token yet'); - } - return this._server.uploadAvatar(data); - } - - static uploadPrivateAttachment(data) { - return window.tokenlessFileServerAdnAPI.uploadData(data); - } -} - -// this will be our instance factory -class LokiFileServerFactoryAPI { - constructor(ourKey) { - this.ourKey = ourKey; - this.servers = []; - } - - establishHomeConnection(serverUrl) { - let thisServer = this.servers.find(server => server._server.baseServerUrl === serverUrl); - if (!thisServer) { - thisServer = new LokiHomeServerInstance(this.ourKey); - log.info(`Registering HomeServer ${serverUrl}`); - // not await, so a failure or slow connection doesn't hinder loading of the app - thisServer.establishConnection(serverUrl); - this.servers.push(thisServer); - } - return thisServer; - } - - async establishConnection(serverUrl) { - let thisServer = this.servers.find(server => server._server.baseServerUrl === serverUrl); - if (!thisServer) { - thisServer = new LokiFileServerInstance(this.ourKey); - log.info(`Registering FileServer ${serverUrl}`); - await thisServer.establishConnection(serverUrl, { skipToken: true }); - this.servers.push(thisServer); - } - return thisServer; - } -} - -module.exports = LokiFileServerFactoryAPI; diff --git a/js/modules/loki_public_chat_api.d.ts b/js/modules/loki_public_chat_api.d.ts deleted file mode 100644 index 0c8524c5e..000000000 --- a/js/modules/loki_public_chat_api.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { LokiAppDotNetServerInterface, LokiPublicChannelAPI } from './loki_app_dot_net_api'; - -export interface LokiPublicChatFactoryInterface { - ourKey: string; - openGroupPubKeys: { [key: string]: string }; - findOrCreateServer(url: string): Promise; - findOrCreateChannel( - url: string, - channelId: number, - conversationId: string - ): Promise; - getListOfMembers(): Promise>; - setListOfMembers(members: Array<{ authorPhoneNumber: string; authorProfileName?: string }>); -} - -declare class LokiPublicChatFactoryAPI implements LokiPublicChatFactoryInterface { - constructor(ourKey: string); -} - -export default LokiPublicChatFactoryAPI; diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js deleted file mode 100644 index fc361a1d6..000000000 --- a/js/modules/loki_public_chat_api.js +++ /dev/null @@ -1,149 +0,0 @@ -/* global log, process */ -const EventEmitter = require('events'); -const LokiAppDotNetAPI = require('./loki_app_dot_net_api'); -const OpenGroupUtils = require('../../ts/opengroup/utils/OpenGroupUtils'); - -class LokiPublicChatFactoryAPI extends EventEmitter { - constructor(ourKey) { - super(); - this.ourKey = ourKey; - this.servers = []; - this.allMembers = []; - this.openGroupPubKeys = {}; - // Multidevice states - } - - // MessageReceiver.connect calls this - // start polling in all existing registered channels - async open() { - await Promise.all(this.servers.map(server => server.open())); - } - - // MessageReceiver.close - async close() { - await Promise.all(this.servers.map(server => server.close())); - } - - // server getter/factory - async findOrCreateServer(serverUrl) { - let thisServer = this.servers.find(server => server.baseServerUrl === serverUrl); - if (!thisServer) { - log.info(`loki_public_chat::findOrCreateServer - creating ${serverUrl}`); - - const serverIsValid = await OpenGroupUtils.validOpenGroupServer(serverUrl); - if (!serverIsValid) { - // FIXME: add toast? - log.error(`loki_public_chat::findOrCreateServer - error: ${serverUrl} is not valid`); - return null; - } - - // after verification then we can start up all the pollers - if (process.env.USE_STUBBED_NETWORK) { - // eslint-disable-next-line global-require - const StubAppDotNetAPI = require('../.././ts/test/session/integration/stubs/stub_app_dot_net_api'); - thisServer = new StubAppDotNetAPI(this.ourKey, serverUrl); - } else { - thisServer = new LokiAppDotNetAPI(this.ourKey, serverUrl); - if (this.openGroupPubKeys[serverUrl]) { - thisServer.getPubKeyForUrl(); - if (!thisServer.pubKeyHex) { - log.warn(`loki_public_chat::findOrCreateServer - failed to set public key`); - } - } - } - - const gotToken = await thisServer.getOrRefreshServerToken(); - if (!gotToken) { - log.warn(`loki_public_chat::findOrCreateServer - Invalid server ${serverUrl}`); - return null; - } - - this.servers.push(thisServer); - } - return thisServer; - } - - // channel getter/factory - async findOrCreateChannel(serverUrl, channelId, conversationId) { - const server = await this.findOrCreateServer(serverUrl); - if (!server) { - log.error(`Failed to create server for: ${serverUrl}`); - return null; - } - return server.findOrCreateChannel(this, channelId, conversationId); - } - - // deallocate resources server uses - unregisterChannel(serverUrl, channelId) { - const i = this.servers.findIndex(server => server.baseServerUrl === serverUrl); - if (i === -1) { - log.warn(`Tried to unregister from nonexistent server ${serverUrl}`); - return; - } - const thisServer = this.servers[i]; - if (!thisServer) { - log.warn(`Tried to unregister from nonexistent server ${i}`); - return; - } - thisServer.unregisterChannel(channelId); - this.servers.splice(i, 1); - } - - // shouldn't this be scoped per conversation? - async getListOfMembers() { - // enable in the next release - /* - let members = []; - await Promise.all(this.servers.map(async server => { - await Promise.all(server.channels.map(async channel => { - const newMembers = await channel.getSubscribers(); - members = [...members, ...newMembers]; - })); - })); - const results = members.map(member => { - return { authorPhoneNumber: member.username }; - }); - */ - return this.allMembers; - } - - // TODO: make this private (or remove altogether) when - // we switch to polling the server for group members - setListOfMembers(members) { - this.allMembers = members; - } - - async setProfileName(profileName) { - await Promise.all( - this.servers.map(async server => { - await server.setProfileName(profileName); - }) - ); - } - - async setHomeServer(homeServer) { - await Promise.all( - this.servers.map(async server => { - // this may fail - // but we can't create a sql table to remember to retry forever - // I think we just silently fail for now - await server.setHomeServer(homeServer); - }) - ); - } - - async setAvatar(url, profileKey) { - await Promise.all( - this.servers.map(async server => { - // this may fail - // but we can't create a sql table to remember to retry forever - // I think we just silently fail for now - await server.setAvatar(url, profileKey); - }) - ); - } -} - -// These files are expected to be in commonjs so we can't use es6 syntax :( -// If we move these to TS then we should be able to use es6 -module.exports = LokiPublicChatFactoryAPI; diff --git a/js/views/invite_contacts_dialog_view.js b/js/views/invite_contacts_dialog_view.js index e00173507..1930f66fb 100644 --- a/js/views/invite_contacts_dialog_view.js +++ b/js/views/invite_contacts_dialog_view.js @@ -28,7 +28,6 @@ this.chatName = convo.get('name'); this.chatServer = convo.get('server'); - this.channelId = convo.get('channelId'); this.isPublic = !!convo.isPublic(); this.convo = convo; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js deleted file mode 100644 index 4b58ea348..000000000 --- a/libtextsecure/message_receiver.js +++ /dev/null @@ -1,98 +0,0 @@ -/* global window: false */ -/* global callWorker: false */ -/* global textsecure: false */ -/* global Event: false */ -/* global dcodeIO: false */ -/* global lokiPublicChatAPI: false */ - -/* eslint-disable more/no-then */ -/* eslint-disable no-unreachable */ - -let openGroupBound = false; - -function MessageReceiver() { - this.pending = Promise.resolve(); - - // only do this once to prevent duplicates - if (lokiPublicChatAPI) { - window.log.info('Binding open group events handler', openGroupBound); - if (!openGroupBound) { - openGroupBound = true; - } - } else { - window.log.warn('Can not handle open group data, API is not available'); - } -} - -MessageReceiver.stringToArrayBuffer = string => - Promise.resolve(dcodeIO.ByteBuffer.wrap(string, 'binary').toArrayBuffer()); -MessageReceiver.arrayBufferToString = arrayBuffer => - Promise.resolve(dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('binary')); -MessageReceiver.arrayBufferToStringBase64 = arrayBuffer => - callWorker('arrayBufferToStringBase64', arrayBuffer); - -MessageReceiver.prototype = new textsecure.EventTarget(); -MessageReceiver.prototype.extend({ - constructor: MessageReceiver, - connect() { - if (this.calledClose) { - return; - } - - if (this.hasConnected) { - const ev = new Event('reconnect'); - this.dispatchEvent(ev); - } - - this.hasConnected = true; - - // start polling all open group rooms you have registered - // if not registered yet, they'll get started when they're created - if (lokiPublicChatAPI) { - lokiPublicChatAPI.open(); - } - - // Ensures that an immediate 'empty' event from the websocket will fire only after - // all cached envelopes are processed. - this.incoming = [this.pending]; - }, - stopProcessing() { - window.log.info('MessageReceiver: stopProcessing requested'); - this.stoppingProcessing = true; - return this.close(); - }, - shutdown() {}, - async close() { - window.log.info('MessageReceiver.close()'); - this.calledClose = true; - - // stop polling all open group rooms - if (lokiPublicChatAPI) { - await lokiPublicChatAPI.close(); - } - }, - onopen() {}, - onerror() {}, - onclose() {}, -}); - -window.textsecure = window.textsecure || {}; - -textsecure.MessageReceiver = function MessageReceiverWrapper() { - const messageReceiver = new MessageReceiver(); - this.addEventListener = messageReceiver.addEventListener.bind(messageReceiver); - this.removeEventListener = messageReceiver.removeEventListener.bind(messageReceiver); - this.close = messageReceiver.close.bind(messageReceiver); - - this.stopProcessing = messageReceiver.stopProcessing.bind(messageReceiver); - - messageReceiver.connect(); -}; - -textsecure.MessageReceiver.prototype = { - constructor: textsecure.MessageReceiver, -}; - -textsecure.MessageReceiver.stringToArrayBuffer = MessageReceiver.stringToArrayBuffer; -textsecure.MessageReceiver.arrayBufferToString = MessageReceiver.arrayBufferToString; -textsecure.MessageReceiver.arrayBufferToStringBase64 = MessageReceiver.arrayBufferToStringBase64; diff --git a/preload.js b/preload.js index bf1ce9ebb..f89978937 100644 --- a/preload.js +++ b/preload.js @@ -313,15 +313,6 @@ window.Signal = Signal.setup({ logger: window.log, }); -if (process.env.USE_STUBBED_NETWORK) { - const StubAppDotNetAPI = require('./ts/test/session/integration/stubs/stub_app_dot_net_api'); - window.LokiAppDotNetServerAPI = StubAppDotNetAPI; -} else { - window.LokiAppDotNetServerAPI = require('./js/modules/loki_app_dot_net_api'); -} -window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api'); - -window.LokiFileServerAPI = require('./js/modules/loki_file_server_api'); window.LokiPushNotificationServerApi = require('./js/modules/loki_push_notification_server_api'); window.SwarmPolling = require('./ts/session/snode_api/swarmPolling').SwarmPolling.getInstance(); diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 60a265648..fa9ec184f 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -40,7 +40,6 @@ import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool'; import { SwarmPolling } from '../../session/snode_api/swarmPolling'; import { IMAGE_JPEG } from '../../types/MIME'; import { FSv2 } from '../../fileserver'; -import { stringToArrayBuffer } from '../../session/utils/String'; import { debounce } from 'underscore'; import { DURATION } from '../../session/constants'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index fbd0aafa9..fe7cd779a 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -23,7 +23,6 @@ import { ToastUtils, UserUtils } from '../../session/utils'; import { DefaultTheme } from 'styled-components'; import { LeftPaneSectionHeader } from './LeftPaneSectionHeader'; import { ConversationController } from '../../session/conversations'; -import { OpenGroup } from '../../opengroup/opengroupV1/OpenGroup'; import { ConversationTypeEnum } from '../../models/conversation'; import { openGroupV2CompleteURLRegex } from '../../opengroup/utils/OpenGroupUtils'; import { joinOpenGroupV2WithUIEvents } from '../../opengroup/opengroupV2/JoinOpenGroupV2'; @@ -360,51 +359,16 @@ export class LeftPaneMessageSection extends React.Component { } } - private async handleOpenGroupJoinV1(serverUrlV1: string) { - // Server URL valid? - if (serverUrlV1.length === 0 || !OpenGroup.validate(serverUrlV1)) { - ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl')); - return; - } - - // Already connected? - if (OpenGroup.getConversation(serverUrlV1)) { - ToastUtils.pushToastError('publicChatExists', window.i18n('publicChatExists')); - return; - } - // Connect to server - try { - ToastUtils.pushToastInfo('connectingToServer', window.i18n('connectingToServer')); - - this.setState({ loading: true }); - await OpenGroup.join(serverUrlV1); - if (await OpenGroup.serverExists(serverUrlV1)) { - ToastUtils.pushToastSuccess( - 'connectToServerSuccess', - window.i18n('connectToServerSuccess') - ); - } else { - throw new Error('Open group joined but the corresponding server does not exist'); - } - this.setState({ loading: false }); - const openGroupConversation = OpenGroup.getConversation(serverUrlV1); - - if (!openGroupConversation) { - window?.log?.error('Joined an opengroup but did not find ther corresponding conversation'); - } - this.handleToggleOverlay(undefined); - } catch (e) { - window?.log?.error('Failed to connect to server:', e); - ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail')); - this.setState({ loading: false }); - } - } - private async handleOpenGroupJoinV2(serverUrlV2: string) { const loadingCallback = (loading: boolean) => { this.setState({ loading }); }; - const joinSuccess = await joinOpenGroupV2WithUIEvents(serverUrlV2, true, loadingCallback); + const joinSuccess = await joinOpenGroupV2WithUIEvents( + serverUrlV2, + true, + false, + loadingCallback + ); return joinSuccess; } @@ -423,8 +387,7 @@ export class LeftPaneMessageSection extends React.Component { this.handleToggleOverlay(undefined); } } else { - // this is an open group v1 - await this.handleOpenGroupJoinV1(serverUrl); + window.log.warn('Invalid opengroupv2 url'); } } diff --git a/ts/components/session/SessionJoinableDefaultRooms.tsx b/ts/components/session/SessionJoinableDefaultRooms.tsx index f8b43fc89..250b8aaf9 100644 --- a/ts/components/session/SessionJoinableDefaultRooms.tsx +++ b/ts/components/session/SessionJoinableDefaultRooms.tsx @@ -101,7 +101,7 @@ export const SessionJoinableRooms = () => { roomId={r.id} base64Data={r.base64Data} onClick={completeUrl => { - void joinOpenGroupV2WithUIEvents(completeUrl, true); + void joinOpenGroupV2WithUIEvents(completeUrl, true, false); }} /> ); diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index a5aca0a6f..967b3599d 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -399,7 +399,7 @@ export class SessionCompositionBox extends React.Component { overridenQuery = ''; } if (this.props.isPublic) { - this.fetchUsersForOpenGroup(overridenQuery, callback); + window.log.warn('mentions users for opengroup v2 todo'); return; } if (!this.props.isPrivate) { @@ -408,26 +408,6 @@ export class SessionCompositionBox extends React.Component { } } - private fetchUsersForOpenGroup(query: any, callback: any) { - void window.lokiPublicChatAPI - .getListOfMembers() - .then(members => - members - .filter(d => !!d) - .filter(d => d.authorProfileName !== 'Anonymous') - .filter(d => d.authorProfileName?.toLowerCase()?.includes(query.toLowerCase())) - ) - // Transform the users to what react-mentions expects - .then(members => { - const toRet = members.map(user => ({ - display: user.authorProfileName, - id: user.authorPhoneNumber, - })); - return toRet; - }) - .then(callback); - } - private fetchUsersForClosedGroup(query: any, callback: any) { const { selectedConversation } = this.props; if (!selectedConversation) { diff --git a/ts/fileserver/FileServerApiV2.ts b/ts/fileserver/FileServerApiV2.ts index 1a161590b..326f72a1e 100644 --- a/ts/fileserver/FileServerApiV2.ts +++ b/ts/fileserver/FileServerApiV2.ts @@ -23,6 +23,7 @@ export type FileServerV2Request = { }; const FILES_ENDPOINT = 'files'; +const RELEASE_VERSION_ENDPOINT = 'session_version'; // Disable this if you don't want to use the file server v2 for sending // Receiving is always enabled if the attachments url matches a fsv2 url @@ -154,3 +155,33 @@ export const buildUrl = (request: FileServerV2Request | OpenGroupV2Request): URL return null; } }; + +/** + * Upload a file to the file server v2 + * @param fileContent the data to send + * @returns null or the fileID and complete URL to share this file + */ +export const getLatestDesktopReleaseFileToFsV2 = async (): Promise => { + const queryParams = { + platform: 'desktop', + }; + + const request: FileServerV2Request = { + method: 'GET', + endpoint: RELEASE_VERSION_ENDPOINT, + queryParams, + }; + + const result = await sendApiV2Request(request); + const statusCode = parseStatusCodeFromOnionRequest(result); + if (statusCode !== 200) { + return null; + } + + // we should probably change the logic of sendOnionRequest to not have all those levels + const latestVersionWithV = (result as any)?.result?.result as string | undefined; + if (!latestVersionWithV) { + return null; + } + return latestVersionWithV; +}; diff --git a/ts/interactions/message.ts b/ts/interactions/message.ts index bc95e0eda..96d31e07f 100644 --- a/ts/interactions/message.ts +++ b/ts/interactions/message.ts @@ -1,14 +1,12 @@ import _ from 'lodash'; import { getV2OpenGroupRoom } from '../data/opengroups'; -import { ConversationModel, ConversationTypeEnum } from '../models/conversation'; -import { OpenGroup } from '../opengroup/opengroupV1/OpenGroup'; +import { ConversationModel } from '../models/conversation'; import { ApiV2 } from '../opengroup/opengroupV2'; import { joinOpenGroupV2WithUIEvents } from '../opengroup/opengroupV2/JoinOpenGroupV2'; import { isOpenGroupV2, openGroupV2CompleteURLRegex } from '../opengroup/utils/OpenGroupUtils'; import { ConversationController } from '../session/conversations'; import { PubKey } from '../session/types'; import { ToastUtils } from '../session/utils'; -import { openConversationExternal } from '../state/ducks/conversations'; export function banUser(userToBan: string, conversation?: ConversationModel) { let pubKeyToBan: PubKey; @@ -36,12 +34,8 @@ export function banUser(userToBan: string, conversation?: ConversationModel) { success = await ApiV2.banUser(pubKeyToBan, _.pick(roomInfos, 'serverUrl', 'roomId')); } } else { - const channelAPI = await conversation.getPublicSendData(); - if (!channelAPI) { - window?.log?.info('cannot ban user, the corresponding channelAPI was not found.'); - return; - } - success = await channelAPI.banUser(userToBan); + window?.log?.info('cannot ban user, the not an opengroupv2.'); + return; } if (success) { ToastUtils.pushUserBanSuccess(); @@ -152,69 +146,22 @@ export async function addSenderAsModerator(sender: string, convoId: string) { } } -async function acceptOpenGroupInvitationV1(serverAddress: string) { - try { - if (serverAddress.length === 0 || !OpenGroup.validate(serverAddress)) { - ToastUtils.pushToastError('connectToServer', window.i18n('invalidOpenGroupUrl')); - return; - } - - // Already connected? - if (OpenGroup.getConversation(serverAddress)) { - ToastUtils.pushToastError('publicChatExists', window.i18n('publicChatExists')); - return; - } - // To some degree this has been copy-pasted from LeftPaneMessageSection - const rawServerUrl = serverAddress.replace(/^https?:\/\//i, '').replace(/[/\\]+$/i, ''); - const sslServerUrl = `https://${rawServerUrl}`; - const conversationId = `publicChat:1@${rawServerUrl}`; - - const conversationExists = ConversationController.getInstance().get(conversationId); - if (conversationExists) { - window?.log?.warn('We are already a member of this public chat'); - ToastUtils.pushAlreadyMemberOpenGroup(); - - return; - } - - const conversation = await ConversationController.getInstance().getOrCreateAndWait( - conversationId, - ConversationTypeEnum.GROUP - ); - await conversation.setPublicSource(sslServerUrl, 1); - - const channelAPI = await window.lokiPublicChatAPI.findOrCreateChannel( - sslServerUrl, - 1, - conversationId - ); - if (!channelAPI) { - window?.log?.warn(`Could not connect to ${serverAddress}`); - return; - } - openConversationExternal(conversationId); - } catch (e) { - window?.log?.warn('failed to join opengroupv1 from invitation', e); - ToastUtils.pushToastError('connectToServerFail', window.i18n('connectToServerFail')); - } -} - const acceptOpenGroupInvitationV2 = (completeUrl: string, roomName?: string) => { window.confirmationDialog({ title: window.i18n('joinOpenGroupAfterInvitationConfirmationTitle', roomName), message: window.i18n('joinOpenGroupAfterInvitationConfirmationDesc', roomName), - resolve: () => joinOpenGroupV2WithUIEvents(completeUrl, true), + resolve: () => joinOpenGroupV2WithUIEvents(completeUrl, true, false), }); // this function does not throw, and will showToasts if anything happens }; /** - * Accepts a v1 (channelid defaults to 1) url or a v2 url (with pubkey) + * Accepts a v2 url open group invitation (with pubkey) or just log an error */ -export const acceptOpenGroupInvitation = async (completeUrl: string, roomName?: string) => { +export const acceptOpenGroupInvitation = (completeUrl: string, roomName?: string) => { if (completeUrl.match(openGroupV2CompleteURLRegex)) { acceptOpenGroupInvitationV2(completeUrl, roomName); } else { - await acceptOpenGroupInvitationV1(completeUrl); + window?.log?.warn('Invalid opengroup url:', completeUrl); } }; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 5171d999f..c4aa02639 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -35,7 +35,6 @@ import { } from '../session/messages/outgoing/visibleMessage/VisibleMessage'; import { GroupInvitationMessage } from '../session/messages/outgoing/visibleMessage/GroupInvitationMessage'; import { ReadReceiptMessage } from '../session/messages/outgoing/controlMessage/receipt/ReadReceiptMessage'; -import { OpenGroup } from '../opengroup/opengroupV1/OpenGroup'; import { OpenGroupUtils } from '../opengroup/utils'; import { ConversationInteraction } from '../interactions'; import { OpenGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/OpenGroupVisibleMessage'; @@ -77,10 +76,8 @@ export interface ConversationAttributes { /* Avatar hash is currently used for opengroupv2. it's sha256 hash of the base64 avatar data. */ avatarHash?: string; server?: any; - channelId?: any; nickname?: string; profile?: any; - lastPublicMessage?: any; profileAvatar?: any; profileKey?: string; accessKey?: any; @@ -114,10 +111,8 @@ export interface ConversationAttributesOptionals { avatar?: any; avatarHash?: string; server?: any; - channelId?: any; nickname?: string; profile?: any; - lastPublicMessage?: any; profileAvatar?: any; profileKey?: string; accessKey?: any; @@ -1073,57 +1068,7 @@ export class ConversationModel extends Backbone.Model { public getNickname() { return this.get('nickname'); } - // maybe "Backend" instead of "Source"? - public async setPublicSource(newServer: any, newChannelId: any) { - if (!this.isPublic()) { - window?.log?.warn(`trying to setPublicSource on non public chat conversation ${this.id}`); - return; - } - if (this.get('server') !== newServer || this.get('channelId') !== newChannelId) { - // mark active so it's not in the contacts list but in the conversation list - this.set({ - server: newServer, - channelId: newChannelId, - active_at: Date.now(), - }); - await this.commit(); - } - } - public getPublicSource() { - if (!this.isPublic()) { - window?.log?.warn(`trying to getPublicSource on non public chat conversation ${this.id}`); - return null; - } - return { - server: this.get('server'), - channelId: this.get('channelId'), - conversationId: this.get('id'), - }; - } - public async getPublicSendData() { - const channelAPI = await window.lokiPublicChatAPI.findOrCreateChannel( - this.get('server'), - this.get('channelId'), - this.id - ); - return channelAPI; - } - public getLastRetrievedMessage() { - if (!this.isPublic()) { - return null; - } - const lastMessageId = this.get('lastPublicMessage') || 0; - return lastMessageId; - } - public async setLastRetrievedMessage(newLastMessageId: any) { - if (!this.isPublic()) { - return; - } - if (this.get('lastPublicMessage') !== newLastMessageId) { - this.set({ lastPublicMessage: newLastMessageId }); - await this.commit(); - } - } + public isAdmin(pubKey?: string) { if (!this.isPublic()) { return false; diff --git a/ts/models/message.ts b/ts/models/message.ts index 9485abbc2..4fa0382ff 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -301,7 +301,7 @@ export class MessageModel extends Backbone.Model { serverAddress, direction, onJoinClick: () => { - void acceptOpenGroupInvitation(invitation.serverAddress, invitation.serverName); + acceptOpenGroupInvitation(invitation.serverAddress, invitation.serverName); }, }; } diff --git a/ts/opengroup/opengroupV1/OpenGroup.ts b/ts/opengroup/opengroupV1/OpenGroup.ts deleted file mode 100644 index 001cbf018..000000000 --- a/ts/opengroup/opengroupV1/OpenGroup.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { ConversationModel, ConversationTypeEnum } from '../../models/conversation'; -import { ConversationController } from '../../session/conversations'; -import { PromiseUtils } from '../../session/utils'; -import { allowOnlyOneAtATime } from '../../session/utils/Promise'; -import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; -import { arrayBufferFromFile } from '../../types/Attachment'; -import { openGroupPrefix, prefixify } from '../utils/OpenGroupUtils'; - -interface OpenGroupParams { - server: string; - channel: number; - conversationId: string; -} - -export async function updateOpenGroupV1(convo: any, groupName: string, avatar: any) { - const API = await convo.getPublicSendData(); - - if (avatar) { - // I hate duplicating this... - const readFile = async (attachment: any) => - new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = (e: any) => { - const data = e.target.result; - resolve({ - ...attachment, - data, - size: data.byteLength, - }); - }; - fileReader.onerror = reject; - fileReader.onabort = reject; - fileReader.readAsArrayBuffer(attachment.file); - }); - const avatarAttachment: any = await readFile({ file: avatar }); - - // We want a square for iOS - const withBlob = await window.Signal.Util.AttachmentUtil.autoScale( - { - contentType: avatar.type, - file: new Blob([avatarAttachment.data], { - type: avatar.contentType, - }), - }, - { - maxSide: 640, - maxSize: 1000 * 1024, - } - ); - const dataResized = await arrayBufferFromFile(withBlob.file); - // const tempUrl = window.URL.createObjectURL(avatar); - - // Get file onto public chat server - const fileObj = await API.serverAPI.putAttachment(dataResized); - if (fileObj === null) { - // problem - window?.log?.warn('File upload failed'); - return; - } - - // lets not allow ANY URLs, lets force it to be local to public chat server - const url = new URL(fileObj.url); - - // write it to the channel - await API.setChannelAvatar(url.pathname); - } - - if (await API.setChannelName(groupName)) { - // queue update from server - // and let that set the conversation - API.pollForChannelOnce(); - // or we could just directly call - // convo.setGroupName(groupName); - // but gut is saying let the server be the definitive storage of the state - // and trickle down from there - } -} - -export class OpenGroup { - private static readonly serverRegex = new RegExp( - '^((https?:\\/\\/){0,1})([\\w-]{2,}\\.){1,2}[\\w-]{2,}$' - ); - private static readonly groupIdRegex = new RegExp( - `^${openGroupPrefix}:[0-9]*@([\\w-]{2,}.){1,2}[\\w-]{2,}$` - ); - - public readonly server: string; - public readonly channel: number; - public readonly groupId?: string; - public readonly conversationId: string; - - /** - * An OpenGroup object. - * If `params.server` is not valid, this will throw an `Error`. - * - * @param params.server The server URL. `https` will be prepended if `http` or `https` is not explicitly set - * @param params.channel The server channel - * @param params.groupId The string corresponding to the server. Eg. `${openGroupPrefix}1@chat.getsession.org` - * @param params.conversationId The conversation ID for the backbone model - */ - constructor(params: OpenGroupParams) { - this.server = prefixify(params.server.toLowerCase()); - - // Validate server format - const isValid = OpenGroup.serverRegex.test(this.server); - if (!isValid) { - throw Error('an invalid server or groupId was provided'); - } - - this.channel = params.channel; - this.conversationId = params.conversationId; - this.groupId = OpenGroup.getGroupId(this.server, this.channel); - } - - /** - * Validate the URL of an open group server - * - * @param serverUrl The server URL to validate - */ - public static validate(serverUrl: string): boolean { - return this.serverRegex.test(serverUrl); - } - - public static getAllAlreadyJoinedOpenGroupsUrl(): Array { - const convos = ConversationController.getInstance().getConversations(); - return convos - .filter(c => !!c.get('active_at') && c.isPublic() && !c.get('left')) - .map(c => c.id.substring((c.id as string).lastIndexOf('@') + 1)) as Array; - } - - /** - * Try to make a new instance of `OpenGroup`. - * This does NOT respect `ConversationController` and does not guarentee the conversation's existence. - * - * @param groupId The string corresponding to the server. Eg. `${openGroupPrefix}1@chat.getsession.org` - * @param conversationId The conversation ID for the backbone model - * @returns `OpenGroup` if valid otherwise returns `undefined`. - */ - public static from( - groupId: string, - conversationId: string, - hasSSL: boolean = true - ): OpenGroup | undefined { - const server = this.getServer(groupId, hasSSL); - const channel = this.getChannel(groupId); - - // Was groupId successfully utilized? - if (!server || !channel) { - return; - } - - const openGroupParams = { - server, - channel, - groupId, - conversationId, - } as OpenGroupParams; - - const isValid = OpenGroup.serverRegex.test(server); - if (!isValid) { - return; - } - - return new OpenGroup(openGroupParams); - } - - /** - * Join an open group - * - * @param server The server URL - * @param onLoading Callback function to be called once server begins connecting - * @returns `OpenGroup` if connection success or if already connected - */ - public static async join(server: string, fromSyncMessage: boolean = false): Promise { - const prefixedServer = prefixify(server); - if (!OpenGroup.validate(server)) { - return; - } - - // Make this not hard coded - const channel = 1; - let conversation; - let conversationId; - - // Return OpenGroup if we're already connected - conversation = OpenGroup.getConversation(prefixedServer); - - if (conversation) { - return; - } - - // Try to connect to server - try { - conversation = await PromiseUtils.timeout( - OpenGroup.attemptConnectionOneAtATime(prefixedServer, channel), - 20000 - ); - - if (!conversation) { - throw new Error(window.i18n('connectToServerFail')); - } - conversationId = (conversation as any)?.cid; - - // here we managed to connect to the group. - // if this is not a Sync Message, we should trigger one - if (!fromSyncMessage) { - await forceSyncConfigurationNowIfNeeded(); - } - } catch (e) { - throw new Error(e); - } - } - - /** - * Get the conversation model of a server from its URL - * - * @param server The server URL - * @returns BackBone conversation model corresponding to the server if it exists, otherwise `undefined` - */ - public static getConversation(server: string): ConversationModel | undefined { - if (!OpenGroup.validate(server)) { - return; - } - const rawServerURL = server.replace(/^https?:\/\//i, '').replace(/[/\\]+$/i, ''); - const channelId = 1; - const conversationId = `${openGroupPrefix}${channelId}@${rawServerURL}`; - - // Quickly peak to make sure we don't already have it - return ConversationController.getInstance().get(conversationId); - } - - /** - * Check if the server exists. - * This does not compare against your conversations with the server. - * - * @param server The server URL - */ - public static async serverExists(server: string): Promise { - if (!OpenGroup.validate(server)) { - return false; - } - - const prefixedServer = prefixify(server); - return Boolean(await window.lokiPublicChatAPI.findOrCreateServer(prefixedServer)); - } - - private static getServer(groupId: string, hasSSL: boolean): string | undefined { - const isValid = this.groupIdRegex.test(groupId); - const strippedServer = isValid ? groupId.split('@')[1] : undefined; - - // We don't know for sure if the server is https or http when taken from the groupId. Preifx accordingly. - return strippedServer ? prefixify(strippedServer.toLowerCase(), hasSSL) : undefined; - } - - private static getChannel(groupId: string): number | undefined { - const isValid = this.groupIdRegex.test(groupId); - const channelMatch = groupId.match(/^.*\:([0-9]*)\@.*$/); - - return channelMatch && isValid ? Number(channelMatch[1]) : undefined; - } - - private static getGroupId(server: string, channel: number): string { - // Server is already validated in constructor; no need to re-check - - // Strip server prefix - const prefixRegex = new RegExp('https?:\\/\\/'); - const strippedServer = server.replace(prefixRegex, ''); - - return `${openGroupPrefix}${channel}@${strippedServer}`; - } - - /** - * When we get our configuration from the network, we might get a few times the same open group on two different messages. - * If we don't do anything, we will join them multiple times. - * Even if the convo exists only once, the lokiPublicChat API will have several instances polling for the same open group. - * Which will cause a lot of duplicate messages as they will be merged on a single conversation. - * - * To avoid this issue, we allow only a single join of a specific opengroup at a time. - */ - private static async attemptConnectionOneAtATime( - serverUrl: string, - channelId: number = 1 - ): Promise { - if (!serverUrl) { - throw new Error('Cannot join open group with empty URL'); - } - const oneAtaTimeStr = `oneAtaTimeOpenGroupJoin:${serverUrl}${channelId}`; - return allowOnlyOneAtATime(oneAtaTimeStr, async () => { - return OpenGroup.attemptConnection(serverUrl, channelId); - }); - } - - // Attempts a connection to an open group server - private static async attemptConnection( - serverUrl: string, - channelId: number - ): Promise { - let completeServerURL = serverUrl.toLowerCase(); - const valid = OpenGroup.validate(completeServerURL); - if (!valid) { - return new Promise((_resolve, reject) => { - reject(window.i18n('connectToServerFail')); - }); - } - - // Add http or https prefix to server - completeServerURL = prefixify(completeServerURL); - - const rawServerURL = serverUrl.replace(/^https?:\/\//i, '').replace(/[/\\]+$/i, ''); - - const conversationId = `${openGroupPrefix}${channelId}@${rawServerURL}`; - - // Quickly peak to make sure we don't already have it - const conversationExists = ConversationController.getInstance().get(conversationId); - if (conversationExists) { - // We are already a member of this public chat - return new Promise((_resolve, reject) => { - reject(window.i18n('publicChatExists')); - }); - } - - // Get server - const serverAPI = await window.lokiPublicChatAPI.findOrCreateServer(completeServerURL); - // SSL certificate failure or offline - if (!serverAPI) { - // Url incorrect or server not compatible - return new Promise((_resolve, reject) => { - reject(window.i18n('connectToServerFail')); - }); - } - - // Create conversation - const conversation = await ConversationController.getInstance().getOrCreateAndWait( - conversationId, - ConversationTypeEnum.GROUP // keep a group for this one as this is an old open group - ); - - // Convert conversation to a public one - await conversation.setPublicSource(completeServerURL, channelId); - - // and finally activate it - void conversation.getPublicSendData(); // may want "await" if you want to use the API - - return conversation; - } -} diff --git a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts index 3eca4e82a..1dac773d5 100644 --- a/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts +++ b/ts/opengroup/opengroupV2/JoinOpenGroupV2.ts @@ -1,8 +1,4 @@ -import { - getV2OpenGroupRoomByRoomId, - OpenGroupV2Room, - removeV2OpenGroupRoom, -} from '../../data/opengroups'; +import { getV2OpenGroupRoomByRoomId, OpenGroupV2Room } from '../../data/opengroups'; import { ConversationController } from '../../session/conversations'; import { PromiseUtils, ToastUtils } from '../../session/utils'; import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/syncUtils'; @@ -57,7 +53,7 @@ export function parseOpenGroupV2(urlWithPubkey: string): OpenGroupV2Room | undef * @param room The room id to join * @param publicKey The server publicKey. It comes from the joining link. (or is already here for the default open group server) */ -async function joinOpenGroupV2(room: OpenGroupV2Room, fromSyncMessage: boolean): Promise { +async function joinOpenGroupV2(room: OpenGroupV2Room, fromConfigMessage: boolean): Promise { if (!room.serverUrl || !room.roomId || room.roomId.length < 2 || !room.serverPublicKey) { return; } @@ -98,7 +94,7 @@ async function joinOpenGroupV2(room: OpenGroupV2Room, fromSyncMessage: boolean): // here we managed to connect to the group. // if this is not a Sync Message, we should trigger one - if (!fromSyncMessage) { + if (!fromConfigMessage) { await forceSyncConfigurationNowIfNeeded(); } } catch (e) { @@ -124,6 +120,7 @@ async function joinOpenGroupV2(room: OpenGroupV2Room, fromSyncMessage: boolean): export async function joinOpenGroupV2WithUIEvents( completeUrl: string, showToasts: boolean, + fromConfigMessage: boolean, uiCallback?: (loading: boolean) => void ): Promise { try { @@ -147,7 +144,7 @@ export async function joinOpenGroupV2WithUIEvents( if (uiCallback) { uiCallback(true); } - await joinOpenGroupV2(parsedRoom, showToasts); + await joinOpenGroupV2(parsedRoom, fromConfigMessage); const isConvoCreated = ConversationController.getInstance().get(conversationID); if (isConvoCreated) { diff --git a/ts/opengroup/utils/OpenGroupUtils.ts b/ts/opengroup/utils/OpenGroupUtils.ts index 4ba999ee3..5a99020a4 100644 --- a/ts/opengroup/utils/OpenGroupUtils.ts +++ b/ts/opengroup/utils/OpenGroupUtils.ts @@ -58,86 +58,6 @@ export function getCompleteUrlFromRoom(roomInfos: OpenGroupV2Room) { return `${roomInfos.serverUrl}/${roomInfos.roomId}?${publicKeyParam}${roomInfos.serverPublicKey}`; } -/** - * Tries to establish a connection with the specified open group url. - * - * This will try to do an onion routing call if the `useFileOnionRequests` feature flag is set, - * or call directly insecureNodeFetch if it's not. - * - * Returns - * * true if useFileOnionRequests is false and no exception where thrown by insecureNodeFetch - * * true if useFileOnionRequests is true and we established a connection to the server with onion routing - * * false otherwise - * - */ -export const validOpenGroupServer = async (serverUrl: string) => { - // test to make sure it's online (and maybe has a valid SSL cert) - try { - const url = new URL(serverUrl); - - if (!window.lokiFeatureFlags.useFileOnionRequests) { - // we are not running with onion request - // this is an insecure insecureNodeFetch. It will expose the user ip to the serverUrl (not onion routed) - window?.log?.info(`insecureNodeFetch => plaintext for ${url.toString()}`); - - // we probably have to check the response here - await insecureNodeFetch(serverUrl); - return true; - } - // This MUST be an onion routing call, no nodeFetch calls below here. - - /** - * this is safe (as long as node's in your trust model) - * - * First, we need to fetch the open group public key of this open group. - * The fileserver have all the open groups public keys. - * We need the open group public key because for onion routing we will need to encode - * our request with it. - * We can just ask the file-server to get the one for the open group we are trying to add. - */ - - const result = await window.tokenlessFileServerAdnAPI.serverRequest( - `loki/v1/getOpenGroupKey/${url.hostname}` - ); - - if (result.response.meta.code === 200) { - // we got the public key of the server we are trying to add. - // decode it. - const obj = JSON.parse(result.response.data); - const pubKey = window.dcodeIO.ByteBuffer.wrap(obj.data, 'base64').toArrayBuffer(); - const pubKeyHex = toHex(pubKey); - // verify we can make an onion routed call to that open group with the decoded public key - // get around the FILESERVER_HOSTS filter by not using serverRequest - const res = await sendViaOnion(pubKeyHex, url, { method: 'GET' }, { noJson: true }); - if (res && res.result && res.result.status === 200) { - window?.log?.info( - `loki_public_chat::validOpenGroupServer - onion routing enabled on ${url.toString()}` - ); - // save pubkey for use... - window.lokiPublicChatAPI.openGroupPubKeys[serverUrl] = pubKey; - return true; - } - // return here, just so we are sure adding some code below won't do a nodeFetch fallback - return false; - } else if (result.response.meta.code !== 404) { - // unknown error code - window?.log?.warn( - 'loki_public_chat::validOpenGroupServer - unknown error code', - result.response.meta - ); - } - return false; - } catch (e) { - window?.log?.warn( - `loki_public_chat::validOpenGroupServer - failing to create ${serverUrl}`, - e.code, - e.message - ); - // bail out if not valid enough - } - return false; -}; - /** * Prefix server with https:// if it's not already prefixed with http or https. */ diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index b84471c1e..b071cbff6 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -1,7 +1,11 @@ import _ from 'lodash'; import { createOrUpdateItem, getItemById, hasSyncedInitialConfigurationItem } from '../data/data'; import { ConversationTypeEnum } from '../models/conversation'; -import { OpenGroup } from '../opengroup/opengroupV1/OpenGroup'; +import { + joinOpenGroupV2WithUIEvents, + parseOpenGroupV2, +} from '../opengroup/opengroupV2/JoinOpenGroupV2'; +import { getOpenGroupV2ConversationId } from '../opengroup/utils/OpenGroupUtils'; import { SignalService } from '../protobuf'; import { ConversationController } from '../session/conversations'; import { UserUtils } from '../session/utils'; @@ -91,16 +95,22 @@ async function handleGroupsAndContactsFromConfigMessage( }) ); - const allOpenGroups = OpenGroup.getAllAlreadyJoinedOpenGroupsUrl(); const numberOpenGroup = configMessage.openGroups?.length || 0; // Trigger a join for all open groups we are not already in. // Currently, if you left an open group but kept the conversation, you won't rejoin it here. for (let i = 0; i < numberOpenGroup; i++) { - const current = configMessage.openGroups[i]; - if (!allOpenGroups.includes(current)) { - window?.log?.info(`triggering join of public chat '${current}' from ConfigurationMessage`); - void OpenGroup.join(current); + const currentOpenGroupUrl = configMessage.openGroups[i]; + const parsedRoom = parseOpenGroupV2(currentOpenGroupUrl); + if (!parsedRoom) { + continue; + } + const roomConvoId = getOpenGroupV2ConversationId(parsedRoom.serverUrl, parsedRoom.roomId); + if (!ConversationController.getInstance().get(roomConvoId)) { + window?.log?.info( + `triggering join of public chat '${currentOpenGroupUrl}' from ConfigurationMessage` + ); + void joinOpenGroupV2WithUIEvents(currentOpenGroupUrl, false, true); } } if (configMessage.contacts?.length) { diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 7e597e79e..77a8955fd 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -191,14 +191,6 @@ export class ConversationController { // Closed/Medium group leaving if (conversation.isClosedGroup()) { await conversation.leaveClosedGroup(); - // open group v1 - } else if (conversation.isPublic() && !conversation.isOpenGroupV2()) { - const channelAPI = await conversation.getPublicSendData(); - if (channelAPI === null) { - window?.log?.warn(`Could not get API for public conversation ${id}`); - } else { - channelAPI.serverAPI.partChannel((channelAPI as any).channelId); - } // open group v2 } else if (conversation.isOpenGroupV2()) { window?.log?.info('leaving open group v2', conversation.id); diff --git a/ts/session/group/index.ts b/ts/session/group/index.ts index 485b2b8cf..6f89423d0 100644 --- a/ts/session/group/index.ts +++ b/ts/session/group/index.ts @@ -34,7 +34,6 @@ import { ClosedGroupNameChangeMessage } from '../messages/outgoing/controlMessag import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupNewMessage'; import { SwarmPolling } from '../snode_api/swarmPolling'; import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; -import { updateOpenGroupV1 } from '../../opengroup/opengroupV1/OpenGroup'; import { updateOpenGroupV2 } from '../../opengroup/opengroupV2/OpenGroupUpdate'; export type GroupInfo = { diff --git a/ts/test/session/integration/stubs/stub_app_dot_net_api.ts b/ts/test/session/integration/stubs/stub_app_dot_net_api.ts deleted file mode 100644 index dcc69f813..000000000 --- a/ts/test/session/integration/stubs/stub_app_dot_net_api.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* global clearTimeout, Buffer, TextDecoder, process */ - -import LokiAppDotNetServerAPI from '../../../../../js/modules/loki_app_dot_net_api'; - -const samplesGetMessages = { - meta: { code: 200 }, - data: [ - { - channel_id: 1, - created_at: '2020-03-18T04:48:44.000Z', - entities: { - mentions: [], - hashtags: [], - links: [], - }, - id: 3662, - machine_only: false, - num_replies: 0, - source: {}, - thread_id: 3662, - reply_to: null, - text: 'hgt', - html: 'hgt', - annotations: [ - { - type: 'network.loki.messenger.publicChat', - value: { - timestamp: 1584506921361, - sig: - '262ab113810564d7ff6474dea264e10e2143d91c004903d06d8d9fddb5b74b2c6245865544d5cf76ee16a3fca045bc028a48c51f8a290508a29b6013d014dc83', - sigver: 1, - }, - }, - ], - user: { - id: 2448, - username: '050cd79763303bcc251bd489a6f7da823a2b8555402b01a7959ebca550d048600f', - created_at: '2020-03-18T02:42:05.000Z', - canonical_url: null, - type: null, - timezone: null, - locale: null, - avatar_image: { - url: null, - width: null, - height: null, - is_default: false, - }, - cover_image: { - url: null, - width: null, - height: null, - is_default: false, - }, - counts: { - following: 0, - posts: 0, - followers: 0, - stars: 0, - }, - name: 'asdf', - annotations: [], - }, - }, - ], -}; - -class StubAppDotNetAPI extends LokiAppDotNetServerAPI { - // make a request to the server - public async serverRequest(endpoint: string, options: { method?: string } = {}) { - const { method } = options; - - if (endpoint === 'channels/1/messages') { - if (!method) { - return { - statusCode: 200, - response: samplesGetMessages, - }; - } - return { - statusCode: 200, - response: { - data: [], - meta: { - max_id: 0, - }, - }, - }; - } - - if (endpoint === 'loki/v1/channel/1/deletes' || endpoint === 'loki/v1/channel/1/moderators') { - return { - statusCode: 200, - response: { - data: [], - meta: { - max_id: 0, - }, - }, - }; - } - if (endpoint === 'files') { - return { - statusCode: 200, - response: { - data: { - url: 'fakeurl', - id: 12345, - }, - }, - }; - } - - if (endpoint === 'channels/1') { - let name = 'Unknown group'; - if (this.baseServerUrl.includes('/chat-dev.lokinet.org')) { - name = 'Loki Dev Chat'; - } else if (this.baseServerUrl.includes('/chat.getsession.org')) { - name = 'Session Public Chat'; - } - return { - statusCode: 200, - response: { - data: { - annotations: [ - { - type: 'net.patter-app.settings', - value: { - name, - }, - }, - ], - }, - }, - }; - } - if (endpoint === 'token') { - return { - statusCode: 200, - response: { - data: { - user: { - name: 'unknown name', - }, - }, - }, - }; - } - - return { - statusCode: 200, - response: {}, - }; - } -} - -module.exports = StubAppDotNetAPI; diff --git a/ts/test/session/unit/messages/GroupInvitationMessage_test.ts b/ts/test/session/unit/messages/GroupInvitationMessage_test.ts index 6b55d0122..13c83c963 100644 --- a/ts/test/session/unit/messages/GroupInvitationMessage_test.ts +++ b/ts/test/session/unit/messages/GroupInvitationMessage_test.ts @@ -19,7 +19,7 @@ describe('GroupInvitationMessage', () => { }); }); - it('dataMessage.groupInvitation has serverAddress, channelId, and serverName set', () => { + it('dataMessage.groupInvitation has serverAddress, and serverName set', () => { const plainText = message.plainTextBuffer(); const decoded = SignalService.Content.decode(plainText); diff --git a/ts/test/session/unit/utils/Messages_test.ts b/ts/test/session/unit/utils/Messages_test.ts index 828c2ff06..ab4058e77 100644 --- a/ts/test/session/unit/utils/Messages_test.ts +++ b/ts/test/session/unit/utils/Messages_test.ts @@ -20,7 +20,6 @@ import { ClosedGroupEncryptionPairMessage } from '../../../../session/messages/o import { ClosedGroupNameChangeMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupNameChangeMessage'; import { ClosedGroupNewMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupNewMessage'; import { ClosedGroupRemovedMembersMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; -import { OpenGroup } from '../../../../opengroup/opengroupV1/OpenGroup'; import { openGroupPrefix } from '../../../../opengroup/utils/OpenGroupUtils'; const { expect } = chai; diff --git a/ts/window.d.ts b/ts/window.d.ts index af5dc847a..13c3b31d0 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -3,7 +3,6 @@ import { LibsignalProtocol } from '../../libtextsecure/libsignal-protocol'; import { SignalInterface } from '../../js/modules/signal'; import { Libloki } from '../libloki'; import { LokiPublicChatFactoryInterface } from '../js/modules/loki_public_chat_api'; -import { LokiAppDotNetServerInterface } from '../js/modules/loki_app_dot_net_api'; import { LibTextsecure } from '../libtextsecure'; import { ConfirmationDialogParams } from '../background'; @@ -29,8 +28,6 @@ declare global { Events: any; Lodash: any; LokiAppDotNetServerAPI: any; - LokiFileServerAPI: any; - LokiPublicChatAPI: any; LokiSnodeAPI: any; Session: any; Signal: SignalInterface; @@ -58,8 +55,6 @@ declare global { useRequestEncryptionKeyPair: boolean; padOutgoingAttachments: boolean; }; - lokiFileServerAPI: LokiFileServerInstance; - lokiPublicChatAPI: LokiPublicChatFactoryInterface; lokiSnodeAPI: LokiSnodeAPI; onLogin: any; resetDatabase: any; @@ -77,7 +72,6 @@ declare global { toggleMenuBar: any; toggleSpellCheck: any; setTheme: (newTheme: string) => any; - tokenlessFileServerAdnAPI: LokiAppDotNetServerInterface; userConfig: any; versionInfo: any; getStoragePubKey: (key: string) => string;