From b4cc9430bef0550f9f1f5e391fe7c06e240b2e46 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 18 Jul 2019 18:06:43 +1000 Subject: [PATCH 01/71] Add loki schema versioning and create new public conversation --- app/sql.js | 123 +++++++++++++++++++++++++++++++++++++ js/models/conversations.js | 2 +- 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/app/sql.js b/app/sql.js index ac5d5ec2c..e9be61f07 100644 --- a/app/sql.js +++ b/app/sql.js @@ -770,6 +770,129 @@ async function updateSchema(instance) { // eslint-disable-next-line no-await-in-loop await runSchemaUpdate(schemaVersion, instance); } + await updateLokiSchema(instance); +} + +const LOKI_SCHEMA_VERSIONS = [ + updateToLokiSchemaVersion1, +]; + +async function updateToLokiSchemaVersion1(currentVersion, instance) { + if (currentVersion >= 1) { + return; + } + console.log('updateToLokiSchemaVersion1: starting...'); + await instance.run('BEGIN TRANSACTION;'); + + const publicChatData = { + id: '06lokiPublicChat', + friendRequestStatus: 4, // Friends + sealedSender: 0, + sessionResetStatus: 0, + swarmNodes: [], + type: 'private', + profileName: 'Loki Public Chat', + unlockTimestamp: null, + unreadCount: 0, + verified: 0, + version: 2, + }; + + const { + id, + type, + name, + friendRequestStatus, + profileName, + } = publicChatData; + + await instance.run( + `INSERT INTO conversations ( + id, + json, + + type, + members, + name, + friendRequestStatus, + profileName + ) values ( + $id, + $json, + + $type, + $members, + $name, + $friendRequestStatus, + $profileName + );`, + { + $id: id, + $json: objectToJSON(publicChatData), + + $type: type, + $members: null, + $name: name, + $friendRequestStatus: friendRequestStatus, + $profileName: profileName, + } + ); + + await instance.run( + `INSERT INTO loki_schema ( + version + ) values ( + 1 + );` + ); + await instance.run('COMMIT TRANSACTION;'); + console.log('updateToLokiSchemaVersion1: success!'); +} + +async function updateLokiSchema(instance) { + const result = await instance.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'"); + if (!result) { + await createLokiSchemaTable(instance); + } + const lokiSchemaVersion = await getLokiSchemaVersion(instance); + console.log( + 'updateLokiSchema:', + `Current loki schema version: ${lokiSchemaVersion};`, + `Most recent schema version: ${LOKI_SCHEMA_VERSIONS.length};` + ); + for (let index = 0, max = LOKI_SCHEMA_VERSIONS.length; index < max; index += 1) { + const runSchemaUpdate = LOKI_SCHEMA_VERSIONS[index]; + + // Yes, we really want to do this asynchronously, in order + // eslint-disable-next-line no-await-in-loop + await runSchemaUpdate(lokiSchemaVersion, instance); + } +} + +async function getLokiSchemaVersion(instance) { + const result = await instance.get('SELECT version FROM loki_schema WHERE version = (SELECT MAX(version) FROM loki_schema);'); + if (!result.version) { + return 0; + } + return result.version; +} + +async function createLokiSchemaTable(instance) { + await instance.run('BEGIN TRANSACTION;'); + await instance.run( + `CREATE TABLE loki_schema( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + version INTEGER + );` + ); + await instance.run( + `INSERT INTO loki_schema ( + version + ) values ( + 0 + );` + ); + await instance.run('COMMIT TRANSACTION;'); } let db; diff --git a/js/models/conversations.js b/js/models/conversations.js index 891a345d5..15d265021 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -167,7 +167,7 @@ if (this.id === this.ourNumber) { this.set({ friendRequestStatus: FriendRequestStatusEnum.friends }); - } else if (lokiP2pAPI) { + } else if (typeof lokiP2pAPI !== 'undefined') { // Online status handling, only for contacts that aren't us this.set({ isOnline: lokiP2pAPI.isOnline(this.id) }); } else { From 06832e4978648561283ae8063f32e333152a08da Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 19 Jul 2019 10:12:58 +1000 Subject: [PATCH 02/71] Lint --- app/sql.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/sql.js b/app/sql.js index e9be61f07..fe6c45197 100644 --- a/app/sql.js +++ b/app/sql.js @@ -773,9 +773,7 @@ async function updateSchema(instance) { await updateLokiSchema(instance); } -const LOKI_SCHEMA_VERSIONS = [ - updateToLokiSchemaVersion1, -]; +const LOKI_SCHEMA_VERSIONS = [updateToLokiSchemaVersion1]; async function updateToLokiSchemaVersion1(currentVersion, instance) { if (currentVersion >= 1) { @@ -798,13 +796,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { version: 2, }; - const { - id, - type, - name, - friendRequestStatus, - profileName, - } = publicChatData; + const { id, type, name, friendRequestStatus, profileName } = publicChatData; await instance.run( `INSERT INTO conversations ( @@ -850,7 +842,9 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { } async function updateLokiSchema(instance) { - const result = await instance.get("SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'"); + const result = await instance.get( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'" + ); if (!result) { await createLokiSchemaTable(instance); } @@ -860,7 +854,11 @@ async function updateLokiSchema(instance) { `Current loki schema version: ${lokiSchemaVersion};`, `Most recent schema version: ${LOKI_SCHEMA_VERSIONS.length};` ); - for (let index = 0, max = LOKI_SCHEMA_VERSIONS.length; index < max; index += 1) { + for ( + let index = 0, max = LOKI_SCHEMA_VERSIONS.length; + index < max; + index += 1 + ) { const runSchemaUpdate = LOKI_SCHEMA_VERSIONS[index]; // Yes, we really want to do this asynchronously, in order @@ -870,7 +868,9 @@ async function updateLokiSchema(instance) { } async function getLokiSchemaVersion(instance) { - const result = await instance.get('SELECT version FROM loki_schema WHERE version = (SELECT MAX(version) FROM loki_schema);'); + const result = await instance.get( + 'SELECT version FROM loki_schema WHERE version = (SELECT MAX(version) FROM loki_schema);' + ); if (!result.version) { return 0; } From 8225bf58470e98edb99093a1312965b4eaef6836 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 19 Jul 2019 10:15:26 +1000 Subject: [PATCH 03/71] Slight sql changes --- app/sql.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/sql.js b/app/sql.js index fe6c45197..e24f14a1a 100644 --- a/app/sql.js +++ b/app/sql.js @@ -843,7 +843,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { async function updateLokiSchema(instance) { const result = await instance.get( - "SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema'" + "SELECT name FROM sqlite_master WHERE type = 'table' AND name='loki_schema';" ); if (!result) { await createLokiSchemaTable(instance); @@ -869,7 +869,8 @@ async function updateLokiSchema(instance) { async function getLokiSchemaVersion(instance) { const result = await instance.get( - 'SELECT version FROM loki_schema WHERE version = (SELECT MAX(version) FROM loki_schema);' + `SELECT version FROM loki_schema WHERE version = + (SELECT MAX(version) FROM loki_schema);` ); if (!result.version) { return 0; From 89980e3248389093c72e5f150f2f3820ea4f3384 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 19 Jul 2019 11:19:16 +1000 Subject: [PATCH 04/71] Better sql for selecting version --- app/sql.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/sql.js b/app/sql.js index e24f14a1a..aa8bb275b 100644 --- a/app/sql.js +++ b/app/sql.js @@ -869,10 +869,9 @@ async function updateLokiSchema(instance) { async function getLokiSchemaVersion(instance) { const result = await instance.get( - `SELECT version FROM loki_schema WHERE version = - (SELECT MAX(version) FROM loki_schema);` + 'SELECT MAX(version) as version FROM loki_schema;' ); - if (!result.version) { + if (!result || !result.version) { return 0; } return result.version; From 90f788c5916105f7b8661574948ba72661b8eba2 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 19 Jul 2019 13:08:08 +1000 Subject: [PATCH 05/71] Add the server and channel plus proper displayName --- app/sql.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/sql.js b/app/sql.js index aa8bb275b..5ff936e18 100644 --- a/app/sql.js +++ b/app/sql.js @@ -789,7 +789,11 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { sessionResetStatus: 0, swarmNodes: [], type: 'private', - profileName: 'Loki Public Chat', + profile: { + displayName: 'Loki Public Chat', + }, + server: 'https://chat.lokinet.org', + channelId: '1', unlockTimestamp: null, unreadCount: 0, verified: 0, From 014558d93973fc6ebb6b4cf1de4f8627358b2a40 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Mon, 22 Jul 2019 16:40:17 +1000 Subject: [PATCH 06/71] Squash and clean of old PRs, move towards sending cleartext again --- app/sql.js | 16 ++- js/background.js | 47 ++++++- js/models/conversations.js | 31 +++++ js/models/messages.js | 12 ++ js/modules/data.js | 9 ++ js/modules/loki_message_api.js | 47 ++++++- js/modules/loki_public_chat_api.js | 209 +++++++++++++++++++++++++++++ libtextsecure/errors.js | 13 ++ libtextsecure/message_receiver.js | 8 ++ libtextsecure/outgoing_message.js | 28 +++- libtextsecure/sendmessage.js | 5 +- preload.js | 2 + 12 files changed, 420 insertions(+), 7 deletions(-) create mode 100644 js/modules/loki_public_chat_api.js diff --git a/app/sql.js b/app/sql.js index 5ff936e18..901be18d7 100644 --- a/app/sql.js +++ b/app/sql.js @@ -97,6 +97,7 @@ module.exports = { updateConversation, removeConversation, getAllConversations, + getAllPublicConversations, getPubKeysWithFriendStatus, getAllConversationIds, getAllPrivateConversations, @@ -783,12 +784,12 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { await instance.run('BEGIN TRANSACTION;'); const publicChatData = { - id: '06lokiPublicChat', + id: 'publicChat:1@chat.lokinet.org', friendRequestStatus: 4, // Friends sealedSender: 0, sessionResetStatus: 0, swarmNodes: [], - type: 'private', + type: 'group', profile: { displayName: 'Loki Public Chat', }, @@ -1604,6 +1605,17 @@ async function getAllPrivateConversations() { return map(rows, row => jsonToObject(row.json)); } +async function getAllPublicConversations() { + const rows = await db.all( + `SELECT json FROM conversations WHERE + type = 'group' AND + id LIKE 'publicChat:%' + ORDER BY id ASC;` + ); + + return map(rows, row => jsonToObject(row.json)); +} + async function getAllGroupsInvolvingId(id) { const rows = await db.all( `SELECT json FROM conversations WHERE diff --git a/js/background.js b/js/background.js index 3cd81dcbd..544deb735 100644 --- a/js/background.js +++ b/js/background.js @@ -204,9 +204,30 @@ window.log.info('Storage fetch'); storage.fetch(); - const initAPIs = () => { + const initAPIs = async () => { const ourKey = textsecure.storage.user.getNumber(); window.lokiMessageAPI = new window.LokiMessageAPI(ourKey); + window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); + const publicConversations = await window.Signal.Data.getAllPublicConversations( + { + ConversationCollection: Whisper.ConversationCollection, + } + ); + publicConversations.forEach(conversation => { + const settings = conversation.getPublicSource(); + window.log.info(`Setting up public conversation for ${conversation.id}`); + const publicChatServer = window.lokiPublicChatAPI.findOrCreateServer( + settings.server + ); + if (publicChatServer) { + publicChatServer.findOrCreateChannel( + settings.channel_id, + conversation.id + ); + } else { + window.log.warn(`Could not set up channel for ${conversation.id}`); + } + }); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { const isPing = true; @@ -246,7 +267,7 @@ if (Whisper.Registration.isDone()) { startLocalLokiServer(); - initAPIs(); + await initAPIs(); } const currentPoWDifficulty = storage.get('PoWDifficulty', null); @@ -729,6 +750,15 @@ } }); + Whisper.events.on('publicMessageSent', ({ pubKey, timestamp }) => { + try { + const conversation = ConversationController.get(pubKey); + conversation.onPublicMessageSent(pubKey, timestamp); + } catch (e) { + window.log.error('Error setting public on message'); + } + }); + Whisper.events.on('password-updated', () => { if (appView && appView.inboxView) { appView.inboxView.trigger('password-updated'); @@ -1245,6 +1275,18 @@ return handleProfileUpdate({ data, confirm, messageDescriptor }); } + const ourNumber = textsecure.storage.user.getNumber(); + const descriptorId = await textsecure.MessageReceiver.arrayBufferToString( + messageDescriptor.id + ); + if ( + messageDescriptor.type === 'group' && + descriptorId.match(/^publicChat:/) && + data.source === ourNumber + ) { + // Remove public chat messages to ourselves + return event.confirm(); + } const message = await createMessage(data); const isDuplicate = await isMessageDuplicate(message); if (isDuplicate) { @@ -1378,6 +1420,7 @@ type: 'incoming', unread: 1, isP2p: data.isP2p, + isPublic: data.isPublic, }; if (data.friendRequest) { diff --git a/js/models/conversations.js b/js/models/conversations.js index 15d265021..b19674da6 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -193,6 +193,9 @@ isMe() { return this.id === this.ourNumber; }, + isPublic() { + return this.id.match(/^publicChat:/); + }, isBlocked() { return BlockedNumberController.isBlocked(this.id); }, @@ -365,6 +368,11 @@ await Promise.all(messages.map(m => m.setIsP2p(true))); }, + async onPublicMessageSent(pubKey, timestamp) { + const messages = this._getMessagesWithTimestamp(pubKey, timestamp); + await Promise.all(messages.map(m => m.setIsPublic(true))); + }, + async onNewMessage(message) { await this.updateLastMessage(); @@ -1347,6 +1355,9 @@ const options = this.getSendOptions(); options.messageType = message.get('type'); + if (this.isPublic()) { + options.publicEndpoint = this.getEndpoint(); + } const groupNumbers = this.getRecipients(); @@ -2015,6 +2026,26 @@ getNickname() { return this.get('nickname'); }, + // maybe "Backend" instead of "Source"? + getPublicSource() { + if (!this.isPublic()) { + return null; + } + return { + server: this.get('server'), + channel_id: this.get('channelId'), + }; + }, + // FIXME: remove or add public and/or "sending" hint to name... + getEndpoint() { + if (!this.isPublic()) { + return null; + } + const server = this.get('server'); + const channelId = this.get('channelId'); + const endpoint = `${server}/channels/${channelId}/messages`; + return endpoint; + }, // SIGNAL PROFILES diff --git a/js/models/messages.js b/js/models/messages.js index 8f91658a8..9bc338259 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -670,6 +670,7 @@ expirationLength, expirationTimestamp, isP2p: !!this.get('isP2p'), + isPublic: !!this.get('isPublic'), onCopyText: () => this.copyText(), onReply: () => this.trigger('reply', this), @@ -1238,6 +1239,17 @@ Message: Whisper.Message, }); }, + async setIsPublic(isPublic) { + if (_.isEqual(this.get('isPublic'), isPublic)) return; + + this.set({ + isPublic: !!isPublic, + }); + + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); + }, send(promise) { this.trigger('pending'); return promise diff --git a/js/modules/data.js b/js/modules/data.js index e5a9e1af1..347fc36fc 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -118,6 +118,7 @@ module.exports = { getPubKeysWithFriendStatus, getAllConversationIds, getAllPrivateConversations, + getAllPublicConversations, getAllGroupsInvolvingId, searchConversations, @@ -739,6 +740,14 @@ async function getAllConversationIds() { return ids; } +async function getAllPublicConversations({ ConversationCollection }) { + const conversations = await channels.getAllPublicConversations(); + + const collection = new ConversationCollection(); + collection.add(conversations); + return collection; +} + async function getAllPrivateConversations({ ConversationCollection }) { const conversations = await channels.getAllPrivateConversations(); diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 199448a57..87c3ec0d3 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -4,6 +4,7 @@ const _ = require('lodash'); const { rpc } = require('./loki_rpc'); +const nodeFetch = require('node-fetch'); const DEFAULT_CONNECTIONS = 3; const MAX_ACCEPTABLE_FAILURES = 1; @@ -75,13 +76,57 @@ class LokiMessageAPI { } async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) { - const { isPing = false, numConnections = DEFAULT_CONNECTIONS } = options; + const { + isPing = false, + numConnections = DEFAULT_CONNECTIONS, + publicEndpoint = null, + } = options; // Data required to identify a message in a conversation const messageEventData = { pubKey, timestamp: messageTimeStamp, }; + // FIXME: should have public/sending(ish hint) in the option to make + // this more obvious... + if (publicEndpoint) { + // could we emit back to LokiPublicChannelAPI somehow? + const { profile } = data; + let displayName = 'Anonymous'; + if (profile && profile.displayName) { + ({ displayName } = profile); + } + const payload = { + text: data.body, + annotations: [ + { + type: 'network.loki.messenger.publicChat', + value: { + timestamp: messageTimeStamp, + from: displayName, + source: this.ourKey, + }, + }, + ], + }; + try { + await nodeFetch(publicEndpoint, { + method: 'post', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer loki', + }, + body: JSON.stringify(payload), + }); + window.Whisper.events.trigger('publicMessageSent', messageEventData); + return; + } catch (e) { + throw new window.textsecure.PublicChatError( + 'Failed to send public chat message.' + ); + } + } + const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64'); const p2pSuccess = await trySendP2p( pubKey, diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js new file mode 100644 index 000000000..b2b55c4e2 --- /dev/null +++ b/js/modules/loki_public_chat_api.js @@ -0,0 +1,209 @@ +/* global log, textsecure */ +const EventEmitter = require('events'); +const nodeFetch = require('node-fetch'); +const { URL, URLSearchParams } = require('url'); + +const GROUPCHAT_POLL_EVERY = 1000; // 1 second + +// singleton to relay events to libtextsecure/message_receiver +class LokiPublicChatAPI extends EventEmitter { + constructor(ourKey) { + super(); + this.ourKey = ourKey; + this.lastGot = {}; + this.servers = []; + } + findOrCreateServer(hostport) { + let thisServer = null; + log.info(`LokiPublicChatAPI looking for ${hostport}`); + this.servers.some(server => { + // if we already have this hostport registered + if (server.server === hostport) { + thisServer = server; + return true; + } + return false; + }); + if (thisServer === null) { + thisServer = new LokiPublicServerAPI(this, hostport); + this.servers.push(thisServer); + } + return thisServer; + } +} + +class LokiPublicServerAPI { + constructor(chatAPI, hostport) { + this.chatAPI = chatAPI; + this.server = hostport; + this.channels = []; + } + findOrCreateChannel(channelId, conversationId) { + let thisChannel = null; + this.channels.forEach(channel => { + if ( + channel.channelId === channelId && + channel.conversationId === conversationId + ) { + thisChannel = channel; + } + }); + if (thisChannel === null) { + thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId); + this.channels.push(thisChannel); + } + return thisChannel; + } + unregisterChannel(channelId) { + // find it, remove it + // if no channels left, request we deregister server + return channelId || this; // this is just to make eslint happy + } +} + +class LokiPublicChannelAPI { + constructor(serverAPI, channelId, conversationId) { + this.serverAPI = serverAPI; + this.channelId = channelId; + this.baseChannelUrl = `${serverAPI.server}/channels/${this.channelId}`; + this.groupName = 'unknown'; + this.conversationId = conversationId; + this.lastGot = 0; + log.info(`registered LokiPublicChannel ${channelId}`); + // start polling + this.pollForMessages(); + } + + async pollForChannel(source, endpoint) { + // groupName will be loaded from server + const url = new URL(this.baseChannelUrl); + /* + const params = { + include_annotations: 1, + }; + */ + let res; + let success = true; + try { + res = await nodeFetch(url); + } catch (e) { + success = false; + } + + const response = await res.json(); + if (response.meta.code !== 200) { + success = false; + } + // update this.groupId + return endpoint || success; + } + + async pollForDeletions() { + // let id = 0; + // read all messages from 0 to current + // delete local copies if server state has changed to delete + // run every minute + const url = new URL(this.baseChannelUrl); + /* + const params = { + include_annotations: 1, + }; + */ + let res; + let success = true; + try { + res = await nodeFetch(url); + } catch (e) { + success = false; + } + + const response = await res.json(); + if (response.meta.code !== 200) { + success = false; + } + return success; + } + + async pollForMessages() { + const url = new URL(`${this.baseChannelUrl}/messages`); + const params = { + include_annotations: 1, + count: -20, + }; + if (this.lastGot) { + params.since_id = this.lastGot; + } + url.search = new URLSearchParams(params); + + let res; + let success = true; + try { + res = await nodeFetch(url); + } catch (e) { + success = false; + } + + const response = await res.json(); + if (response.meta.code !== 200) { + success = false; + } + + if (success) { + let receivedAt = new Date().getTime(); + response.data.forEach(adnMessage => { + // FIXME: create proper message for this message.DataMessage.body + let timestamp = new Date(adnMessage.created_at).getTime(); + let from = adnMessage.user.username; + let source; + if (adnMessage.annotations.length) { + const noteValue = adnMessage.annotations[0].value; + ({ from, timestamp, source } = noteValue); + } + + const messageData = { + friendRequest: false, + source, + sourceDevice: 1, + timestamp, + serverTimestamp: timestamp, + receivedAt, + isPublic: true, + message: { + body: adnMessage.text, + attachments: [], + group: { + id: this.conversationId, + type: textsecure.protobuf.GroupContext.Type.DELIVER, + }, + flags: 0, + expireTimer: 0, + profileKey: null, + timestamp, + received_at: receivedAt, + sent_at: timestamp, + quote: null, + contact: [], + preview: [], + profile: { + displayName: from, + }, + }, + }; + receivedAt += 1; // Ensure different arrival times + + this.serverAPI.chatAPI.emit('publicMessage', { + message: messageData, + }); + this.lastGot = !this.lastGot + ? adnMessage.id + : Math.max(this.lastGot, adnMessage.id); + }); + } + + setTimeout(() => { + this.pollForMessages(); + }, GROUPCHAT_POLL_EVERY); + } +} + +module.exports = LokiPublicChatAPI; diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index c97d21fe3..7e16d4ecf 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -273,6 +273,18 @@ } inherit(ReplayableError, TimestampError); + function PublicChatError(message) { + this.name = 'PublicChatError'; + this.message = message; + Error.call(this, message); + + // Maintains proper stack trace, where our error was thrown (only available on V8) + // via https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + if (Error.captureStackTrace) { + Error.captureStackTrace(this); + } + } + window.textsecure.UnregisteredUserError = UnregisteredUserError; window.textsecure.SendMessageNetworkError = SendMessageNetworkError; window.textsecure.IncomingIdentityKeyError = IncomingIdentityKeyError; @@ -292,4 +304,5 @@ window.textsecure.WrongSwarmError = WrongSwarmError; window.textsecure.WrongDifficultyError = WrongDifficultyError; window.textsecure.TimestampError = TimestampError; + window.textsecure.PublicChatError = PublicChatError; })(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 9c356df08..db640c623 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -13,6 +13,7 @@ /* global GroupBuffer: false */ /* global WebSocketResource: false */ /* global localLokiServer: false */ +/* global lokiPublicChatAPI: false */ /* global localServerPort: false */ /* global lokiMessageAPI: false */ /* global lokiP2pAPI: false */ @@ -75,6 +76,7 @@ MessageReceiver.prototype.extend({ }); this.httpPollingResource.pollServer(); localLokiServer.on('message', this.handleP2pMessage.bind(this)); + lokiPublicChatAPI.on('publicMessage', this.handlePublicMessage.bind(this)); this.startLocalServer(); // TODO: Rework this socket stuff to work with online messaging @@ -142,6 +144,12 @@ MessageReceiver.prototype.extend({ }; this.httpPollingResource.handleMessage(message, options); }, + handlePublicMessage({ message }) { + const ev = new Event('message'); + ev.confirm = function confirmTerm() {}; + ev.data = message; + this.dispatchAndWait(ev); + }, stopProcessing() { window.log.info('MessageReceiver: stopProcessing requested'); this.stoppingProcessing = true; diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index ad12ee51a..a0ab663a4 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -43,9 +43,17 @@ function OutgoingMessage( this.failoverNumbers = []; this.unidentifiedDeliveries = []; - const { numberInfo, senderCertificate, online, messageType, isPing } = + const { + numberInfo, + senderCertificate, + online, + messageType, + isPing, + publicEndpoint, + } = options || {}; this.numberInfo = numberInfo; + this.publicEndpoint = publicEndpoint; this.senderCertificate = senderCertificate; this.online = online; this.messageType = messageType || 'outgoing'; @@ -193,6 +201,9 @@ OutgoingMessage.prototype = { numConnections: NUM_SEND_CONNECTIONS, isPing: this.isPing, }; + if (this.publicEndpoint) { + options.publicEndpoint = this.publicEndpoint; + } await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); } catch (e) { if (e.name === 'HTTPError' && (e.code !== 409 && e.code !== 410)) { @@ -259,6 +270,21 @@ OutgoingMessage.prototype = { }, doSendMessage(number, deviceIds, recurse) { const ciphers = {}; + if (this.publicEndpoint) { + return this.transmitMessage( + number, + this.message.dataMessage, + this.timestamp, + 0 // ttl + ) + .then(() => { + this.successfulNumbers[this.successfulNumbers.length] = number; + this.numberCompleted(); + }) + .catch(error => { + throw error; + }); + } /* Disabled because i'm not sure how senderCertificate works :thinking: const { numberInfo, senderCertificate } = this; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index c2be038d8..59cf18447 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -942,7 +942,10 @@ MessageSender.prototype = { options ) { const me = textsecure.storage.user.getNumber(); - const numbers = groupNumbers.filter(number => number !== me); + let numbers = groupNumbers.filter(number => number !== me); + if (options.publicEndpoint) { + numbers = [groupId]; + } const attrs = { recipients: numbers, body: messageText, diff --git a/preload.js b/preload.js index be63b33f6..7ff52dc3d 100644 --- a/preload.js +++ b/preload.js @@ -324,6 +324,8 @@ window.LokiP2pAPI = require('./js/modules/loki_p2p_api'); window.LokiMessageAPI = require('./js/modules/loki_message_api'); +window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api'); + window.LocalLokiServer = require('./libloki/modules/local_loki_server'); window.localServerPort = config.localServerPort; From 21f0149e22689d4fbb648aece4915f83c3de281f Mon Sep 17 00:00:00 2001 From: Beaudan Date: Mon, 22 Jul 2019 17:53:04 +1000 Subject: [PATCH 07/71] Send profile and update it --- js/models/messages.js | 6 ++++++ libtextsecure/sendmessage.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/js/models/messages.js b/js/models/messages.js index 9bc338259..94086caf8 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1972,6 +1972,12 @@ } ); } + } else if (dataMessage.profile) { + ConversationController.getOrCreateAndWait(source, 'private').then( + sender => { + sender.setProfile(dataMessage.profile); + } + ); } let autoAccept = false; diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 59cf18447..649e668e7 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -946,6 +946,7 @@ MessageSender.prototype = { if (options.publicEndpoint) { numbers = [groupId]; } + const profile = textsecure.storage.impl.getLocalProfile(); const attrs = { recipients: numbers, body: messageText, @@ -956,6 +957,7 @@ MessageSender.prototype = { needsSync: true, expireTimer, profileKey, + profile, group: { id: groupId, type: textsecure.protobuf.GroupContext.Type.DELIVER, From 1d0067e6e50190d1392a1ae436eb24048f53f4f5 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Tue, 23 Jul 2019 12:16:11 +1000 Subject: [PATCH 08/71] Forgot to add this part --- stylesheets/_modules.scss | 13 +++++++++++++ ts/components/conversation/Message.tsx | 13 +++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 4a011d833..ee97bdf7a 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -517,10 +517,20 @@ color: $color-gray-60; text-transform: uppercase; } +.module-message__metadata__public { + font-size: 11px; + line-height: 16px; + letter-spacing: 0.3px; + color: $color-gray-60; + text-transform: uppercase; +} .module-message__metadata__date--incoming, .module-message__metadata__p2p--incoming { color: $color-white-08; } +.module-message__metadata__public--incoming { + color: $color-white-08; +} .module-message__metadata__date--with-image-no-caption { color: $color-white; } @@ -528,6 +538,9 @@ .module-message__metadata__p2p { font-weight: bold; } +.module-message__metadata__public { + font-weight: bold; +} .module-message__metadata__spacer { flex-grow: 1; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 8c29c60fe..279d98425 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -86,6 +86,7 @@ export interface Props { expirationLength?: number; expirationTimestamp?: number; isP2p?: boolean; + isPublic?: boolean; onClickAttachment?: (attachment: AttachmentType) => void; onClickLinkPreview?: (url: string) => void; @@ -203,6 +204,7 @@ export class Message extends React.PureComponent { textPending, timestamp, isP2p, + isPublic, } = this.props; if (collapseMetadata) { @@ -212,6 +214,9 @@ export class Message extends React.PureComponent { const isShowingImage = this.isShowingImage(); const withImageNoCaption = Boolean(!text && isShowingImage); const showError = status === 'error' && direction === 'outgoing'; + const hasBadge = isP2p || isPublic; + const badgeText = isPublic ? 'Public' : isP2p ? 'P2p' : ''; + const badgeType = badgeText.toLowerCase(); return (
{ module="module-message__metadata__date" /> )} - {isP2p ? ( + {hasBadge ? ( -  • P2P +  • ${badgeText} ) : null} {expirationLength && expirationTimestamp ? ( From 1fc2173d9b33bfc71e8b7c7c1e4aa8a659aae986 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Tue, 23 Jul 2019 13:13:08 +1000 Subject: [PATCH 09/71] Update header template to display group names properly and update migration to only store name for public chat --- app/sql.js | 13 ++++--------- ts/components/conversation/ConversationHeader.tsx | 3 ++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/sql.js b/app/sql.js index 901be18d7..f1f6ad229 100644 --- a/app/sql.js +++ b/app/sql.js @@ -790,10 +790,8 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { sessionResetStatus: 0, swarmNodes: [], type: 'group', - profile: { - displayName: 'Loki Public Chat', - }, server: 'https://chat.lokinet.org', + name: 'Loki Public Chat', channelId: '1', unlockTimestamp: null, unreadCount: 0, @@ -801,7 +799,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { version: 2, }; - const { id, type, name, friendRequestStatus, profileName } = publicChatData; + const { id, type, name, friendRequestStatus } = publicChatData; await instance.run( `INSERT INTO conversations ( @@ -811,8 +809,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { type, members, name, - friendRequestStatus, - profileName + friendRequestStatus ) values ( $id, $json, @@ -820,8 +817,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { $type, $members, $name, - $friendRequestStatus, - $profileName + $friendRequestStatus );`, { $id: id, @@ -831,7 +827,6 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { $members: null, $name: name, $friendRequestStatus: friendRequestStatus, - $profileName: profileName, } ); diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 096ca3ecd..684d35a84 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -96,7 +96,7 @@ export class ConversationHeader extends React.Component { } public renderTitle() { - const { phoneNumber, i18n, profileName, isKeysPending, isMe } = this.props; + const { phoneNumber, i18n, profileName, isKeysPending, isMe, name } = this.props; if (isMe) { return ( @@ -111,6 +111,7 @@ export class ConversationHeader extends React.Component { {isKeysPending ? '(pending)' : null} From 3a5e6e2fd3ccec4c0cf9348123c1d89c28080047 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Tue, 23 Jul 2019 13:14:09 +1000 Subject: [PATCH 10/71] Lint --- ts/components/conversation/ConversationHeader.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 684d35a84..b1497e21c 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -96,7 +96,14 @@ export class ConversationHeader extends React.Component { } public renderTitle() { - const { phoneNumber, i18n, profileName, isKeysPending, isMe, name } = this.props; + const { + phoneNumber, + i18n, + profileName, + isKeysPending, + isMe, + name, + } = this.props; if (isMe) { return ( From 143bf1bdc1df62b7d8b8ccb8ea843b0df0c8a7db Mon Sep 17 00:00:00 2001 From: Beaudan Date: Tue, 23 Jul 2019 16:42:51 +1000 Subject: [PATCH 11/71] Fill out unregister channel stub, trigger unregister channel on conversation deletion --- js/background.js | 2 +- js/conversation_controller.js | 6 ++- js/models/conversations.js | 2 +- js/modules/loki_public_chat_api.js | 67 ++++++++++++++---------------- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/js/background.js b/js/background.js index 544deb735..c8674a4cd 100644 --- a/js/background.js +++ b/js/background.js @@ -221,7 +221,7 @@ ); if (publicChatServer) { publicChatServer.findOrCreateChannel( - settings.channel_id, + settings.channelId, conversation.id ); } else { diff --git a/js/conversation_controller.js b/js/conversation_controller.js index fe2a45074..50b3aae33 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -1,4 +1,4 @@ -/* global _, Whisper, Backbone, storage, textsecure, libsignal */ +/* global _, Whisper, Backbone, storage, textsecure, libsignal, lokiPublicChatAPI */ /* eslint-disable more/no-then */ @@ -159,6 +159,10 @@ if (!conversation) { return; } + if (conversation.isPublic()) { + const server = conversation.getPublicSource(); + lokiPublicChatAPI.unregisterChannel(server.server, server.channelId); + } await conversation.destroyMessages(); const deviceIds = await textsecure.storage.protocol.getDeviceIds(id); await Promise.all( diff --git a/js/models/conversations.js b/js/models/conversations.js index b19674da6..c8c124b4d 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2033,7 +2033,7 @@ } return { server: this.get('server'), - channel_id: this.get('channelId'), + channelId: this.get('channelId'), }; }, // FIXME: remove or add public and/or "sending" hint to name... diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index b2b55c4e2..f563c36e6 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -14,22 +14,28 @@ class LokiPublicChatAPI extends EventEmitter { this.servers = []; } findOrCreateServer(hostport) { - let thisServer = null; log.info(`LokiPublicChatAPI looking for ${hostport}`); - this.servers.some(server => { - // if we already have this hostport registered - if (server.server === hostport) { - thisServer = server; - return true; - } - return false; - }); - if (thisServer === null) { + let thisServer = this.servers.find(server => server.server === hostport); + if (!thisServer) { thisServer = new LokiPublicServerAPI(this, hostport); this.servers.push(thisServer); } return thisServer; } + unregisterChannel(hostport, channelId) { + const thisServer = this.servers.find(server => server.server === hostport); + if (!thisServer) { + log.warn(`Tried to unregister from nonexistent server ${hostport}`); + return; + } + thisServer.unregisterChannel(channelId); + if (thisServer.channels.length === 0) { + const index = this.servers.indexOf(thisServer); + if (index > -1) { + this.servers.splice(index, 1); + } + } + } } class LokiPublicServerAPI { @@ -39,25 +45,23 @@ class LokiPublicServerAPI { this.channels = []; } findOrCreateChannel(channelId, conversationId) { - let thisChannel = null; - this.channels.forEach(channel => { - if ( - channel.channelId === channelId && - channel.conversationId === conversationId - ) { - thisChannel = channel; - } - }); - if (thisChannel === null) { + let thisChannel = this.channels.find(channel => channel.channelId === channelId); + if (!thisChannel) { thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId); this.channels.push(thisChannel); } return thisChannel; } unregisterChannel(channelId) { - // find it, remove it - // if no channels left, request we deregister server - return channelId || this; // this is just to make eslint happy + const thisChannel = this.channels.find(channel => channel.channelId === channelId); + if (!thisChannel) { + return; + } + thisChannel.stopPolling = true; + const index = this.channels.indexOf(thisChannel); + if (index > -1) { + this.channels.splice(index, 1); + } } } @@ -69,6 +73,7 @@ class LokiPublicChannelAPI { this.groupName = 'unknown'; this.conversationId = conversationId; this.lastGot = 0; + this.stopPolling = false; log.info(`registered LokiPublicChannel ${channelId}`); // start polling this.pollForMessages(); @@ -77,11 +82,6 @@ class LokiPublicChannelAPI { async pollForChannel(source, endpoint) { // groupName will be loaded from server const url = new URL(this.baseChannelUrl); - /* - const params = { - include_annotations: 1, - }; - */ let res; let success = true; try { @@ -99,16 +99,10 @@ class LokiPublicChannelAPI { } async pollForDeletions() { - // let id = 0; // read all messages from 0 to current // delete local copies if server state has changed to delete // run every minute const url = new URL(this.baseChannelUrl); - /* - const params = { - include_annotations: 1, - }; - */ let res; let success = true; try { @@ -144,6 +138,10 @@ class LokiPublicChannelAPI { } const response = await res.json(); + if (this.stopPolling) { + // Stop after latest await possible + return; + } if (response.meta.code !== 200) { success = false; } @@ -151,7 +149,6 @@ class LokiPublicChannelAPI { if (success) { let receivedAt = new Date().getTime(); response.data.forEach(adnMessage => { - // FIXME: create proper message for this message.DataMessage.body let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; From 1a82a35a0878cfd8d3a0c1e80468c4425f164cac Mon Sep 17 00:00:00 2001 From: Beaudan Date: Wed, 24 Jul 2019 11:45:32 +1000 Subject: [PATCH 12/71] More efficient channel search --- js/modules/loki_public_chat_api.js | 31 ++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index f563c36e6..b93a36184 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -23,18 +23,21 @@ class LokiPublicChatAPI extends EventEmitter { return thisServer; } unregisterChannel(hostport, channelId) { - const thisServer = this.servers.find(server => server.server === hostport); + let thisServer; + let i = 0; + for (; i < this.servers.length; i += 1) { + if (this.servers[i].server === hostport) { + thisServer = this.servers[i]; + break; + } + } + if (!thisServer) { log.warn(`Tried to unregister from nonexistent server ${hostport}`); return; } thisServer.unregisterChannel(channelId); - if (thisServer.channels.length === 0) { - const index = this.servers.indexOf(thisServer); - if (index > -1) { - this.servers.splice(index, 1); - } - } + this.servers.splice(i, 1); } } @@ -53,15 +56,19 @@ class LokiPublicServerAPI { return thisChannel; } unregisterChannel(channelId) { - const thisChannel = this.channels.find(channel => channel.channelId === channelId); + 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; } + this.channels.splice(i, 1); thisChannel.stopPolling = true; - const index = this.channels.indexOf(thisChannel); - if (index > -1) { - this.channels.splice(index, 1); - } } } From f718bf15d24b0e09d34218ca2b595f9acce3f92e Mon Sep 17 00:00:00 2001 From: Beaudan Date: Tue, 23 Jul 2019 17:53:18 +1000 Subject: [PATCH 13/71] Prepare to delete message, send message id to server --- js/models/conversations.js | 1 + js/models/messages.js | 3 +++ js/modules/loki_message_api.js | 2 ++ libtextsecure/outgoing_message.js | 3 +++ 4 files changed, 9 insertions(+) diff --git a/js/models/conversations.js b/js/models/conversations.js index c8c124b4d..fbcf5da03 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1357,6 +1357,7 @@ options.messageType = message.get('type'); if (this.isPublic()) { options.publicEndpoint = this.getEndpoint(); + options.messageId = id; } const groupNumbers = this.getRecipients(); diff --git a/js/models/messages.js b/js/models/messages.js index 94086caf8..d1f77a3a5 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -357,6 +357,9 @@ onDestroy() { this.cleanup(); }, + deleteMessage() { + this.trigger('delete', this); + }, async cleanup() { MessageController.unregister(this.id); this.unload(); diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 87c3ec0d3..8216ca084 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -80,6 +80,7 @@ class LokiMessageAPI { isPing = false, numConnections = DEFAULT_CONNECTIONS, publicEndpoint = null, + messageId = null, } = options; // Data required to identify a message in a conversation const messageEventData = { @@ -105,6 +106,7 @@ class LokiMessageAPI { timestamp: messageTimeStamp, from: displayName, source: this.ourKey, + id: messageId, }, }, ], diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index a0ab663a4..02d8fc77d 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -50,10 +50,12 @@ function OutgoingMessage( messageType, isPing, publicEndpoint, + messageId, } = options || {}; this.numberInfo = numberInfo; this.publicEndpoint = publicEndpoint; + this.messageId = messageId; this.senderCertificate = senderCertificate; this.online = online; this.messageType = messageType || 'outgoing'; @@ -203,6 +205,7 @@ OutgoingMessage.prototype = { }; if (this.publicEndpoint) { options.publicEndpoint = this.publicEndpoint; + options.messageId = this.messageId; } await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); } catch (e) { From f7039080af2d608a49f8423899129349fdafe98a Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 1 Aug 2019 15:12:16 +1000 Subject: [PATCH 14/71] Remove accidental swapfile, save messages with the id given to them from public server --- js/background.js | 1 + js/models/messages.js | 15 ++++++++++++--- js/modules/loki_public_chat_api.js | 4 +++- js/views/.inbox_view.js.swp | Bin 24576 -> 0 bytes 4 files changed, 16 insertions(+), 4 deletions(-) delete mode 100644 js/views/.inbox_view.js.swp diff --git a/js/background.js b/js/background.js index c8674a4cd..b7e5fd804 100644 --- a/js/background.js +++ b/js/background.js @@ -1411,6 +1411,7 @@ const { isError } = options; let messageData = { + id: data.id, source: data.source, sourceDevice: data.sourceDevice, sent_at: data.timestamp, diff --git a/js/models/messages.js b/js/models/messages.js index d1f77a3a5..b5204c504 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -2014,9 +2014,18 @@ } else { await conversation.onFriendRequestAccepted(); } - const id = await window.Signal.Data.saveMessage(message.attributes, { - Message: Whisper.Message, - }); + let id; + // Force save if the message already has an id, used for public channels + if (message.id) { + id = await window.Signal.Data.saveMessage(message.attributes, { + forceSave: true, + Message: Whisper.Message, + }); + } else { + id = await window.Signal.Data.saveMessage(message.attributes, { + Message: Whisper.Message, + }); + } message.set({ id }); MessageController.register(message.id, message); diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index b93a36184..edc020059 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -159,12 +159,14 @@ class LokiPublicChannelAPI { let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; + let id; if (adnMessage.annotations.length) { const noteValue = adnMessage.annotations[0].value; - ({ from, timestamp, source } = noteValue); + ({ from, timestamp, source, id } = noteValue); } const messageData = { + id, friendRequest: false, source, sourceDevice: 1, diff --git a/js/views/.inbox_view.js.swp b/js/views/.inbox_view.js.swp deleted file mode 100644 index 460fcffa56b5a06b899333d606641f374dcf6c98..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeI4Ymgk*Re(Fji37pG1WZA|aAhx=8GC26Yo)NXMy^O}$yV8Vgq0<*D@eUFcW2w% z(>-+e>}tK({mVa$El6;Z3KbxQi2(;F9)W@?utPZ%glARaqMR3om@?+O zukN0nSt(A)k5qc9KJ85R?fW?Q+}rn@d#;w}P9Hrc_I2hwK393(>tEWq=S!`b%jP4` z>yEN4lq*5Hs&4N5S(bj9?R-1G^R^w(-z_XN>$eEzKZ z-nlo;pRB*s5~wBcRY_o!t+e-D>FvF7UJv!_uH7ZR;|+IxRaw>#*Al2DP)neeKrMk< z0<{Ec3Dgp(CGhnnfoyoW_jam$nXUHy_V24Ie_w5XYvY0M+m-v5+uw)m-w#&qf0O;) zv45Yh+_yjLKeYsE3Dgp(B~VMCmOw3mS^~8MY6;X5s3lNKpq9YbKmvZx^LCMMTR8yO z|9AHP)8FQKAAw(l4Y&vHhQ08Ea5a1U z@VxiKPr`kW!ria{d!Y@p;K8R}<9Q#0N8#O&!ErbSGw^wwET4nlfS-YT;4N?xcEi8p zWcfY#SvUtj3TNOtxEB5mC(G}{d*J8b9k3tv!He|o-@`}XF?bZ-0ew(?KMR-q2>WUH zy`Jc<`h;#QWC2kenLil13z0vN`S;OaStgBH&poNbI~LSJicuYi_wp>1^|=R%;*{sZ=d(%$oA!p^VD;OBpifHP(OeAiJEt}z`>Z+n~{JuP#Br#nknk{kQ79O@wcjG9{ zsxn4aeVH9qF*`-4s{XML6>ZUlqbEs*03y@-N}Mtp?@*o29}e$~{az6Dm%53RQD;Vm zonayoUk{DYX=GTE+4QF-#WXctrNa+a3NO@633*pMRGOf)HOK-#RP|hp2g6WiQi<9% zyi~bC+*2C3B{qVn7jG;GmxcK)ww*3kG!=>Gn%72j-L;C^mJ^)S2`WuN)Oil5Pj{_Q z+_H)viKA>OxKAqGYHpQ=iws4fnS1sKnTA1>wR=J8FNd-n$@5v8D^f%;KQ>(BwUkwt z`B|1U8=1dM!{vE&sxjZl1*20{yU1Sjdz&<6#Sc?y#=4a@m26t+aer&uDR(#Y)ATqK z&w^;^L5QO25_NZElK$~CX%jG-+RPiHL8K=pH_){fD-n>hyQW7QtwisP=$tt6KC^>{ z*=K%V)NF)CZCyv1=ABh?2$n~gOi=?}rTqEI2vIYX&9c$zWE9=BKXbVWLBBH;?R9E9 zR`fA5ic_TBjYm<|$>JqSkNVA4o`q6meN)^b=9~;i!yYm^nZ*5sEX;j(uPk2%XWKTj zo$BFf=l6=3cSz?%TODSvNb0$Ub*X7%1^sQO=mu>*H?yU0c1H=q*J`~zJb%tM-Kkr( z7pCfB(KW?4J0+KL7dIRb^Ti7(pNPvzepn3r^J;`Sc{0m0%u*>-@-?}+$P^(C93ZEC zH;Mc27h{*^ZYmy|@_Hy2)V_3GZV`hZ8n4S_CH7BNVt-KytA8dN`fdJAuKo6oX)Jec zyzyqiyg>oWL7zBJp)^|bW0{J0l%)Z!CIGaj13`K9=-YlGeLIVq>~diU+1XePx~r;k zvSTm1qKiICWHw47cU?7Zm+W-2B)mgzx<=d=$AeR9bmd(l*HITWK9%s9rX!{jm#X3B z=zQ2tRhX*cApm`;Cs+JYsCCiQsx+Iq3fWFsK$zxrU^Qkz?42_gO1ZdHZY-52@K-dB z5ODs2=rBvDK5919GDY@^nYs||rQX9uN+C!~UDnlFj<2pe1fPq&NqEbz$fVt4O__|k zbR;vE)duybn}5}J^>MFMEoc3r?U+l&J5?pKui$cX%L$C0sj5=r@|$t7Bp>xI;3c54 zQy-;Qx`~Y1XcUG@DV2Ca0-Cc`J(%;URi$TvU`3+qI@3IN-ON_d+jdivN;BC$op$Jt zQdulRRJxAzyQ|HWQKXmALZ>(;dx6iYt*9gER(KIM8|#d;cvkqMUJy50p31>|xSyn1 zv$$j`CqvKNSFH?sVi%3ncW9t~VsSMHd!3+HlxAc|+5_TTSD5Wfl630bvUvEO zCCXk(N=cRde>ryX-+{9KJJtyN1h)O-@Gg)b;AQOjkHe!7gM`yC4=uPBUc#<_7XAR< z32%i4d>_1sJ^y)l4*mvy9=0Hby)X;k3ol{AzW|?w=iv!>2x7Pkl)e95@I~tU9Q-T% z1E@B90_smKfm#B!1ZoM?5~w9mOQ4p(H=YExTgG5=24ilyv%WW`jS!<@Kk<9`42=WL z{&c=Ol}er;28ryQ@skKU)Fm9hTBJ9Q2Wx*#xc+j>tZd!btPZj$&qOzrezFqaDAiT0 z_|t6Cv%X3@=D9pqMWF(s5;be%#>BOvF=6n zjFE83QZ?TCjy8bvR6FE6@RPNJ>5|?^lD)A)@`@=ly<5uP8Pc}s6us)MYG?K;o`BnN z;-{{G)jg@H`Q*J`h%JbQql}Io#OrcVx!;=&A7{k6EOoK*aTflF;%WRRlh22lQW*9Y zZGK4>7MYE?OqCn))&MofFVWD+#=IU=^45t;dF!%@sXz&EEK_~Ilqu51=bk5Vo&&mF z?f&4}?=5AUp=|1+@0ojRtJ7oV(B7F+DZ0%?1FHhl-WHu^-4C0sDSGXk|GF4Pxf_M1 z#ipMNnd!Tw&R+?_OeRWeDLK`HT-pC>zxr+LWh?vtkbPDB7xw;V;h*8p;W0?yRyY9H zz!$LhKL+oGcft_f0uA^V?ER;2CWEC*arMJ+Kb@;Yv{Z{hx+s;4k2# z@LO;Jwjh8-n1va*2GpLv+VB4}xBw5sgRlkrp#{4@z^B>Ue?MG+2jC2-egA)={m;Ua z@Gv|GS|;Ckug-Jrm+E$tCXBl}2#YnR3O{h`SM%!- z!7crSc!%Ce_K|Z_D_^&|b<$L8Wgw%`MJyHf>~*UWYcZJ>oj(6Dk~VI}mcKz%UC+5E z$4YgfJf+2G^+|1w+^$ReGMkcT`eoK=N?VF$X}LPqIw@A}uiRQA_NFUWrx2yI<^sWE z#1?wyhx@m2SF{!p2q8A)vPk3Z8avn~;ez#0xRgRP+h zde|&g!^S);lrBwekQRL$SqY6d*2JG|NH$-CQ1(O?s{kq$f;dad1OW#syi0xU$C>aW zL9|%5DRex@Sh>0oOlO&3omKkPNJ2Ysj@lF^Owpwu6M5HXclnH5UeZGCEZ%uy>F}-P zMovj^^!RP%6g!K-l{mGAP8f}KVswoZWKYYXOUpO>AQLNbB2bxdM>%|jtvqq^@bOEu zpxW#c$B!RgJT=zxbR)=!0BRaVEQYxo=;k_Yw`zrfZlt`VRlFFl)!s~19INTuCWB0e zzN%i`L5nBHA%TbZ0jyucTH?eIjlbUVfz#+JznU0(Gc;84sw1lUgq^wbwB|LDr_$My zPi!1__Ie)Nq!qfD(r)&Y{r@R!-H&1eEBk-WzVV;IzW+=3b$9^Y4nGDb;RL(^RP6sB z!4vR1@DMx*cYyK%c<=)D{^Rg2I0vWU+uFpS^? z+y-xk-JtgT{|c17KY$zH3Q%qNP1uGz;5anl1={(i@Dw}(8*l_>;R^U7evqf(ap=Qg zxDl>`=ZVk%V^FdCcf&1k1AI5UNNoO-@BrKo1GpPh-2R(kKeXUwV)Xw5{scY*?}bO; z-EbC;K?`=l8{uW*_Me0gzy){=1~3OLxEel5-2NjV;m6@1?1DGKWsv{J4z^z>{^*%& zTuG;Rd-F-UUV|xt>B&RI>N1Ds0d(~mOzV=4jH}mRo)L!mw_bxOt*F;vS{v#$m|LKK zy{^IXq0MGmlm*!)k>$nGcIrizml+l(0p@12S^qW%Gfrchc9u6e9dar@gk|tUZ)&O- zm?z{w5K)c`+Q~$V+!$O1BrpSCpscvYJ{=s$Fbyvm(~JiAUn(q>MTDbRxM8 z$IGzPGPT(_aeF)mbt>+l-H0kgQodSFzahhJJdmg2rJx`AoTV}{tXwXh7~6<|k>4jO}8Zzms?Q=R_eD(H*azt5gjuXON%k)VUY?sGMU{yXKrW z(l@F*=(%_cm Z)w=3W>8D&7Y;G4X+_+Egw9a(z{{WDX@WcQB From 958766dc65a43521d81ed6aa8b4ee4aadad859e5 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Mon, 5 Aug 2019 15:41:42 +1000 Subject: [PATCH 15/71] Bold the profile name and truncate the pubkey --- ts/components/conversation/ContactName.tsx | 2 +- ts/components/conversation/Message.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index d2d14d7a8..9d40a04ae 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -21,7 +21,7 @@ export class ContactName extends React.Component { const shouldShowProfile = Boolean(profileName && !name); const profileElement = shouldShowProfile ? ( - + ) : null; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 279d98425..2955c26af 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -307,7 +307,7 @@ export class Message extends React.PureComponent { return (
Date: Mon, 5 Aug 2019 16:05:40 +1000 Subject: [PATCH 16/71] Only bold the profile name for messages --- ts/components/conversation/ContactName.tsx | 19 +++++++++++++------ ts/components/conversation/Message.tsx | 1 + 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index 9d40a04ae..d8d02efb2 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -10,20 +10,27 @@ interface Props { profileName?: string; i18n: LocalizerType; module?: string; + boldProfileName?: Boolean; } export class ContactName extends React.Component { public render() { - const { phoneNumber, name, profileName, i18n, module } = this.props; + const { phoneNumber, name, profileName, i18n, module, boldProfileName } = this.props; const prefix = module ? module : 'module-contact-name'; const title = name ? name : phoneNumber; const shouldShowProfile = Boolean(profileName && !name); - const profileElement = shouldShowProfile ? ( - - - - ) : null; + const profileElement = shouldShowProfile ? + (boldProfileName ? ( + + + + ) : ( + + + + )) + : null; return ( diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 2955c26af..c547da4c6 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -312,6 +312,7 @@ export class Message extends React.PureComponent { profileName={authorProfileName} module="module-message__author" i18n={i18n} + boldProfileName={true} />
); From 86e55e5dc63f3b906842a9cdfb489994946d3e4b Mon Sep 17 00:00:00 2001 From: Beaudan Date: Mon, 5 Aug 2019 17:19:11 +1000 Subject: [PATCH 17/71] Clean up bold check --- js/modules/loki_public_chat_api.js | 4 ++- ts/components/conversation/ContactName.tsx | 30 ++++++++++++++-------- ts/components/conversation/Message.tsx | 5 +++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index edc020059..637e6f6de 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -48,7 +48,9 @@ class LokiPublicServerAPI { this.channels = []; } findOrCreateChannel(channelId, conversationId) { - let thisChannel = this.channels.find(channel => channel.channelId === channelId); + let thisChannel = this.channels.find( + channel => channel.channelId === channelId + ); if (!thisChannel) { thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId); this.channels.push(thisChannel); diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index d8d02efb2..bd0db5d5b 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -15,22 +15,30 @@ interface Props { export class ContactName extends React.Component { public render() { - const { phoneNumber, name, profileName, i18n, module, boldProfileName } = this.props; + const { + phoneNumber, + name, + profileName, + i18n, + module, + boldProfileName, + } = this.props; const prefix = module ? module : 'module-contact-name'; const title = name ? name : phoneNumber; const shouldShowProfile = Boolean(profileName && !name); - const profileElement = shouldShowProfile ? - (boldProfileName ? ( - - - - ) : ( - + const styles = (boldProfileName + ? { + fontWeight: 'bold', + } + : {}) as React.CSSProperties; + const profileElement = shouldShowProfile ? ( + + - - )) - : null; + + + ) : null; return ( diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index c547da4c6..d1b611416 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -307,7 +307,10 @@ export class Message extends React.PureComponent { return (
Date: Tue, 6 Aug 2019 12:48:19 +1000 Subject: [PATCH 18/71] Review cleanup --- ts/components/conversation/ContactName.tsx | 4 +--- ts/components/conversation/Message.tsx | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index bd0db5d5b..6f9a4998e 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -34,9 +34,7 @@ export class ContactName extends React.Component { : {}) as React.CSSProperties; const profileElement = shouldShowProfile ? ( - - - + ) : null; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index d1b611416..764aa397b 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -308,8 +308,7 @@ export class Message extends React.PureComponent {
Date: Tue, 6 Aug 2019 12:05:54 +1000 Subject: [PATCH 19/71] Cleaner forceSave --- js/models/messages.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/js/models/messages.js b/js/models/messages.js index b5204c504..cc6c885ef 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -2014,18 +2014,11 @@ } else { await conversation.onFriendRequestAccepted(); } - let id; // Force save if the message already has an id, used for public channels - if (message.id) { - id = await window.Signal.Data.saveMessage(message.attributes, { - forceSave: true, - Message: Whisper.Message, - }); - } else { - id = await window.Signal.Data.saveMessage(message.attributes, { - Message: Whisper.Message, - }); - } + const id = await window.Signal.Data.saveMessage(message.attributes, { + forceSave: !!message.id, + Message: Whisper.Message, + }); message.set({ id }); MessageController.register(message.id, message); From 5bdfaeaf1d569a66a325dd17c92447fbf2871020 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Tue, 6 Aug 2019 15:10:19 +1000 Subject: [PATCH 20/71] Don't send read receipts to public chats --- js/models/conversations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index fbcf5da03..3715a3aba 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1958,7 +1958,7 @@ return; } - if (read.length && options.sendReadReceipts) { + if (!this.isPublic() && read.length && options.sendReadReceipts) { window.log.info(`Sending ${read.length} read receipts`); // Because syncReadMessages sends to our other devices, and sendReadReceipts goes // to a contact, we need accessKeys for both. From 145b8b6682c1a12c87472c54b9270a01954f83ab Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 8 Aug 2019 15:29:56 +1000 Subject: [PATCH 21/71] Fix message order --- js/modules/loki_public_chat_api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 637e6f6de..17d8e004d 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -157,7 +157,7 @@ class LokiPublicChannelAPI { if (success) { let receivedAt = new Date().getTime(); - response.data.forEach(adnMessage => { + response.data.reverse().forEach(adnMessage => { let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; From f857744e88e5fa6b0d61f9c1a4a24e2b774f1c01 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 9 Aug 2019 13:33:48 +1000 Subject: [PATCH 22/71] Revert sending id --- js/background.js | 1 - js/models/conversations.js | 1 - js/models/messages.js | 2 -- js/modules/loki_message_api.js | 2 -- js/modules/loki_public_chat_api.js | 4 +--- libtextsecure/outgoing_message.js | 3 --- 6 files changed, 1 insertion(+), 12 deletions(-) diff --git a/js/background.js b/js/background.js index b7e5fd804..c8674a4cd 100644 --- a/js/background.js +++ b/js/background.js @@ -1411,7 +1411,6 @@ const { isError } = options; let messageData = { - id: data.id, source: data.source, sourceDevice: data.sourceDevice, sent_at: data.timestamp, diff --git a/js/models/conversations.js b/js/models/conversations.js index 3715a3aba..fd887826b 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1357,7 +1357,6 @@ options.messageType = message.get('type'); if (this.isPublic()) { options.publicEndpoint = this.getEndpoint(); - options.messageId = id; } const groupNumbers = this.getRecipients(); diff --git a/js/models/messages.js b/js/models/messages.js index cc6c885ef..d1f77a3a5 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -2014,9 +2014,7 @@ } else { await conversation.onFriendRequestAccepted(); } - // Force save if the message already has an id, used for public channels const id = await window.Signal.Data.saveMessage(message.attributes, { - forceSave: !!message.id, Message: Whisper.Message, }); message.set({ id }); diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 8216ca084..87c3ec0d3 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -80,7 +80,6 @@ class LokiMessageAPI { isPing = false, numConnections = DEFAULT_CONNECTIONS, publicEndpoint = null, - messageId = null, } = options; // Data required to identify a message in a conversation const messageEventData = { @@ -106,7 +105,6 @@ class LokiMessageAPI { timestamp: messageTimeStamp, from: displayName, source: this.ourKey, - id: messageId, }, }, ], diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 17d8e004d..d9ebde10f 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -161,14 +161,12 @@ class LokiPublicChannelAPI { let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; - let id; if (adnMessage.annotations.length) { const noteValue = adnMessage.annotations[0].value; - ({ from, timestamp, source, id } = noteValue); + ({ from, timestamp, source } = noteValue); } const messageData = { - id, friendRequest: false, source, sourceDevice: 1, diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 02d8fc77d..a0ab663a4 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -50,12 +50,10 @@ function OutgoingMessage( messageType, isPing, publicEndpoint, - messageId, } = options || {}; this.numberInfo = numberInfo; this.publicEndpoint = publicEndpoint; - this.messageId = messageId; this.senderCertificate = senderCertificate; this.online = online; this.messageType = messageType || 'outgoing'; @@ -205,7 +203,6 @@ OutgoingMessage.prototype = { }; if (this.publicEndpoint) { options.publicEndpoint = this.publicEndpoint; - options.messageId = this.messageId; } await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); } catch (e) { From 899bfc3b2b69a2cef37edfe7e12e277dc3497d3d Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 9 Aug 2019 13:34:48 +1000 Subject: [PATCH 23/71] Add channel id to message table and provide helper functions --- app/sql.js | 23 +++++++++++++++++++++++ js/models/messages.js | 11 +++++++++++ js/modules/data.js | 10 ++++++++++ 3 files changed, 44 insertions(+) diff --git a/app/sql.js b/app/sql.js index f1f6ad229..d64428152 100644 --- a/app/sql.js +++ b/app/sql.js @@ -118,6 +118,7 @@ module.exports = { removeMessage, getUnreadByConversation, getMessageBySender, + getMessageByServerId, getMessageById, getAllMessages, getAllMessageIds, @@ -801,6 +802,11 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { const { id, type, name, friendRequestStatus } = publicChatData; + await instance.run( + `ALTER TABLE messages + ADD COLUMN serverId STRING;` + ); + await instance.run( `INSERT INTO conversations ( id, @@ -1718,6 +1724,7 @@ async function saveMessage(data, { forceSave } = {}) { hasFileAttachments, hasVisualMediaAttachments, id, + serverId, // eslint-disable-next-line camelcase received_at, schemaVersion, @@ -1736,6 +1743,7 @@ async function saveMessage(data, { forceSave } = {}) { $id: id, $json: objectToJSON(data), + $serverId: serverId, $body: body, $conversationId: conversationId, $expirationStartTimestamp: expirationStartTimestamp, @@ -1758,6 +1766,7 @@ async function saveMessage(data, { forceSave } = {}) { await db.run( `UPDATE messages SET json = $json, + serverId = $serverId, body = $body, conversationId = $conversationId, expirationStartTimestamp = $expirationStartTimestamp, @@ -1792,6 +1801,7 @@ async function saveMessage(data, { forceSave } = {}) { id, json, + serverId, body, conversationId, expirationStartTimestamp, @@ -1812,6 +1822,7 @@ async function saveMessage(data, { forceSave } = {}) { $id, $json, + $serverId, $body, $conversationId, $expirationStartTimestamp, @@ -1934,6 +1945,18 @@ async function removeMessage(id) { ); } +async function getMessageByServerId(id) { + const row = await db.get('SELECT * FROM messages WHERE ServerId = $ServerId;', { + $ServerId: ServerId, + }); + + if (!row) { + return null; + } + + return jsonToObject(row.json); +} + async function getMessageById(id) { const row = await db.get('SELECT * FROM messages WHERE id = $id;', { $id: id, diff --git a/js/models/messages.js b/js/models/messages.js index d1f77a3a5..6aee70a89 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -1242,6 +1242,17 @@ Message: Whisper.Message, }); }, + async setServerId(serverId) { + if (_.isEqual(this.get('serverId'), serverId)) return; + + this.set({ + serverId, + }); + + await window.Signal.Data.saveMessage(this.attributes, { + Message: Whisper.Message, + }); + }, async setIsPublic(isPublic) { if (_.isEqual(this.get('isPublic'), isPublic)) return; diff --git a/js/modules/data.js b/js/modules/data.js index 347fc36fc..cebc102cb 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -141,6 +141,7 @@ module.exports = { removeAllMessagesInConversation, getMessageBySender, + getMessageByServerId, getMessageById, getAllMessages, getAllUnsentMessages, @@ -875,6 +876,15 @@ async function _removeMessages(ids) { await channels.removeMessage(ids); } +async function getMessageByServerId(id, { Message }) { + const message = await channels.getMessageByServerId(id); + if (!message) { + return null; + } + + return new Message(message); +} + async function getMessageById(id, { Message }) { const message = await channels.getMessageById(id); if (!message) { From c9f433fbbaca7dbb90f27799371a12ddb6e4e468 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Fri, 9 Aug 2019 13:46:05 +1000 Subject: [PATCH 24/71] Save message id when sending/receiving to/from public server and lint --- app/sql.js | 11 +++++++---- js/background.js | 17 ++++++++++------- js/models/conversations.js | 9 +++++++-- js/modules/loki_message_api.js | 4 +++- js/modules/loki_public_chat_api.js | 1 + 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/sql.js b/app/sql.js index d64428152..64fa1848a 100644 --- a/app/sql.js +++ b/app/sql.js @@ -1945,10 +1945,13 @@ async function removeMessage(id) { ); } -async function getMessageByServerId(id) { - const row = await db.get('SELECT * FROM messages WHERE ServerId = $ServerId;', { - $ServerId: ServerId, - }); +async function getMessageByServerId(serverId) { + const row = await db.get( + 'SELECT * FROM messages WHERE serverId = $serverId;', + { + $serverId: serverId, + } + ); if (!row) { return null; diff --git a/js/background.js b/js/background.js index c8674a4cd..1b2317c80 100644 --- a/js/background.js +++ b/js/background.js @@ -750,14 +750,17 @@ } }); - Whisper.events.on('publicMessageSent', ({ pubKey, timestamp }) => { - try { - const conversation = ConversationController.get(pubKey); - conversation.onPublicMessageSent(pubKey, timestamp); - } catch (e) { - window.log.error('Error setting public on message'); + Whisper.events.on( + 'publicMessageSent', + ({ pubKey, timestamp, serverId }) => { + try { + const conversation = ConversationController.get(pubKey); + conversation.onPublicMessageSent(pubKey, timestamp, serverId); + } catch (e) { + window.log.error('Error setting public on message'); + } } - }); + ); Whisper.events.on('password-updated', () => { if (appView && appView.inboxView) { diff --git a/js/models/conversations.js b/js/models/conversations.js index fd887826b..26a27e8a8 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -368,9 +368,14 @@ await Promise.all(messages.map(m => m.setIsP2p(true))); }, - async onPublicMessageSent(pubKey, timestamp) { + async onPublicMessageSent(pubKey, timestamp, serverId) { const messages = this._getMessagesWithTimestamp(pubKey, timestamp); - await Promise.all(messages.map(m => m.setIsPublic(true))); + await Promise.all( + messages.map(message => [ + message.setIsPublic(true), + message.setServerId(serverId), + ]) + ); }, async onNewMessage(message) { diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 87c3ec0d3..672e6c4f6 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -110,7 +110,7 @@ class LokiMessageAPI { ], }; try { - await nodeFetch(publicEndpoint, { + const result = await nodeFetch(publicEndpoint, { method: 'post', headers: { 'Content-Type': 'application/json', @@ -118,6 +118,8 @@ class LokiMessageAPI { }, body: JSON.stringify(payload), }); + const body = await result.json(); + messageEventData.serverId = body.data.id; window.Whisper.events.trigger('publicMessageSent', messageEventData); return; } catch (e) { diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index d9ebde10f..1acc1e884 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -167,6 +167,7 @@ class LokiPublicChannelAPI { } const messageData = { + serverId: adnMessage.id, friendRequest: false, source, sourceDevice: 1, From c57571fdfda33b78a57291036041edbbacb3fe19 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Mon, 22 Jul 2019 23:15:39 -0700 Subject: [PATCH 25/71] loki.network rss feed --- js/background.js | 2 + js/models/conversations.js | 23 +++ js/models/messages.js | 1 + js/modules/loki_rss_api.js | 163 +++++++++++++++++++++ libtextsecure/message_receiver.js | 2 + preload.js | 2 + ts/components/ConversationListItem.tsx | 9 +- ts/components/conversation/Linkify.tsx | 21 ++- ts/components/conversation/Message.tsx | 4 +- ts/components/conversation/MessageBody.tsx | 3 + ts/state/ducks/conversations.ts | 1 + tslint.json | 10 +- 12 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 js/modules/loki_rss_api.js diff --git a/js/background.js b/js/background.js index 1b2317c80..13ae7d9c3 100644 --- a/js/background.js +++ b/js/background.js @@ -228,6 +228,7 @@ window.log.warn(`Could not set up channel for ${conversation.id}`); } }); + window.lokiRssAPI = new window.LokiRssAPI(); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { const isPing = true; @@ -1424,6 +1425,7 @@ unread: 1, isP2p: data.isP2p, isPublic: data.isPublic, + isRss: data.isRss, }; if (data.friendRequest) { diff --git a/js/models/conversations.js b/js/models/conversations.js index 26a27e8a8..d5b0c31bb 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -196,6 +196,9 @@ isPublic() { return this.id.match(/^publicChat:/); }, + isRss() { + return this.id.match(/^rss:/); + }, isBlocked() { return BlockedNumberController.isBlocked(this.id); }, @@ -453,6 +456,7 @@ lastMessage: { status: this.get('lastMessageStatus'), text: this.get('lastMessage'), + isRss: this.isRss(), }, isOnline: this.isOnline(), hasNickname: !!this.getNickname(), @@ -642,6 +646,11 @@ ); }, updateTextInputState() { + if (this.isRss()) { + // or if we're an rss conversation, disable it + this.trigger('disable:input', true); + return; + } switch (this.get('friendRequestStatus')) { case FriendRequestStatusEnum.none: case FriendRequestStatusEnum.requestExpired: @@ -2088,6 +2097,20 @@ }); } }, + async setGroupNameAndAvatar(name, avatarPath) { + const currentName = this.get('name'); + const profileAvatar = this.get('profileAvatar'); + if (profileAvatar !== avatarPath || currentName !== name) { + // only update changed items + if (profileAvatar !== avatarPath) + this.set({ profileAvatar: avatarPath }); + if (currentName !== name) this.set({ name }); + // save + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } + }, async setProfileAvatar(avatarPath) { const profileAvatar = this.get('profileAvatar'); if (profileAvatar !== avatarPath) { diff --git a/js/models/messages.js b/js/models/messages.js index 6aee70a89..b79307473 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -674,6 +674,7 @@ expirationTimestamp, isP2p: !!this.get('isP2p'), isPublic: !!this.get('isPublic'), + isRss: !!this.get('isRss'), onCopyText: () => this.copyText(), onReply: () => this.trigger('reply', this), diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js new file mode 100644 index 000000000..1138d8ffe --- /dev/null +++ b/js/modules/loki_rss_api.js @@ -0,0 +1,163 @@ +/* eslint-disable no-await-in-loop */ +/* eslint-disable no-loop-func */ +/* global log, window, textsecure, ConversationController */ + +const EventEmitter = require('events'); +const nodeFetch = require('node-fetch'); + +const RSS_FEED = 'https://loki.network/feed/'; +const CONVO_ID = 'rss://loki.network/feed/'; +const PER_MIN = 60 * 1000; +const PER_HR = 60 * PER_MIN; +const RSS_POLL_EVERY = 1 * PER_HR; // once an hour + +function xml2json(xml) { + try { + let obj = {}; + if (xml.children.length > 0) { + for (let i = 0; i < xml.children.length; i += 1) { + const item = xml.children.item(i); + const { nodeName } = item.nodeName; + + if (typeof obj[nodeName] === 'undefined') { + obj[nodeName] = xml2json(item); + } else { + if (typeof obj[nodeName].push === 'undefined') { + const old = obj[nodeName]; + + obj[nodeName] = []; + obj[nodeName].push(old); + } + obj[nodeName].push(xml2json(item)); + } + } + } else { + obj = xml.textContent; + } + return obj; + } catch (e) { + log.error(e.message); + } + return {}; +} + +// hate duplicating this here... +const friendRequestStatusEnum = Object.freeze({ + // New conversation, no messages sent or received + none: 0, + // This state is used to lock the input early while sending + pendingSend: 1, + // Friend request sent, awaiting response + requestSent: 2, + // Friend request received, awaiting user input + requestReceived: 3, + // We did it! + friends: 4, + // Friend Request sent but timed out + requestExpired: 5, +}); + +class LokiRssAPI extends EventEmitter { + constructor() { + super(); + // properties + this.groupId = CONVO_ID; + this.feedTimer = null; + this.conversationSetup = false; + // initial set up + this.getFeed(); + } + + setupConversation() { + // only run once + if (this.conversationSetup) return; + // wait until conversations are loaded + if (ConversationController._initialFetchComplete) { + const conversation = ConversationController.getOrCreate( + this.groupId, + 'group' + ); + conversation.setFriendRequestStatus(friendRequestStatusEnum.friends); + conversation.setGroupNameAndAvatar( + 'Loki.network News', + 'images/loki/loki_icon.png' + ); + conversation.updateTextInputState(); + this.conversationSetup = true; // prevent running again + } + } + + async getFeed() { + let response; + let success = true; + try { + response = await nodeFetch(RSS_FEED); + } catch (e) { + log.error('fetcherror', e); + success = false; + } + const responseXML = await response.text(); + let feedDOM = {}; + try { + feedDOM = await new window.DOMParser().parseFromString( + responseXML, + 'text/xml' + ); + } catch (e) { + log.error('xmlerror', e); + success = false; + } + if (!success) return; + const feedObj = xml2json(feedDOM); + let receivedAt = new Date().getTime(); + + // make sure conversation is set up properly + // (delay to after the network response intentionally) + this.setupConversation(); + + feedObj.rss.channel.item.reverse().forEach(item => { + // log.debug('item', item) + + const pubDate = new Date(item.pubDate); + + // if we use group style, we can put the title in the source + const messageData = { + friendRequest: false, + source: this.groupId, + sourceDevice: 1, + timestamp: pubDate.getTime(), + serverTimestamp: pubDate.getTime(), + receivedAt, + isRss: true, + message: { + body: `

${item.title}

${item.description}`, + attachments: [], + group: { + id: this.groupId, + type: textsecure.protobuf.GroupContext.Type.DELIVER, + }, + flags: 0, + expireTimer: 0, + profileKey: null, + timestamp: pubDate.getTime(), + received_at: receivedAt, + sent_at: pubDate.getTime(), + quote: null, + contact: [], + preview: [], + profile: null, + }, + }; + receivedAt += 1; // Ensure different arrival times + this.emit('rssMessage', { + message: messageData, + }); + }); + function callTimer() { + this.getFeed(); + } + this.feedTimer = setTimeout(callTimer, RSS_POLL_EVERY); + } +} + +module.exports = LokiRssAPI; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index db640c623..42312b596 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -17,6 +17,7 @@ /* global localServerPort: false */ /* global lokiMessageAPI: false */ /* global lokiP2pAPI: false */ +/* global lokiRssAPI: false */ /* eslint-disable more/no-then */ /* eslint-disable no-unreachable */ @@ -77,6 +78,7 @@ MessageReceiver.prototype.extend({ this.httpPollingResource.pollServer(); localLokiServer.on('message', this.handleP2pMessage.bind(this)); lokiPublicChatAPI.on('publicMessage', this.handlePublicMessage.bind(this)); + lokiRssAPI.on('rssMessage', this.handlePublicMessage.bind(this)); this.startLocalServer(); // TODO: Rework this socket stuff to work with online messaging diff --git a/preload.js b/preload.js index 7ff52dc3d..54577110a 100644 --- a/preload.js +++ b/preload.js @@ -326,6 +326,8 @@ window.LokiMessageAPI = require('./js/modules/loki_message_api'); window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api'); +window.LokiRssAPI = require('./js/modules/loki_rss_api'); + window.LocalLokiServer = require('./libloki/modules/local_loki_server'); window.localServerPort = config.localServerPort; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index c158a5372..60870f6b3 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -30,6 +30,7 @@ export type PropsData = { lastMessage?: { status: 'sending' | 'sent' | 'delivered' | 'read' | 'error'; text: string; + isRss: boolean; }; showFriendRequestIndicator?: boolean; @@ -213,7 +214,13 @@ export class ConversationListItem extends React.PureComponent { if (!lastMessage && !isTyping) { return null; } - const text = lastMessage && lastMessage.text ? lastMessage.text : ''; + let text = lastMessage && lastMessage.text ? lastMessage.text : ''; + + // if coming from Rss feed + if (lastMessage && lastMessage.isRss) { + // strip any HTML + text = text.replace(/<[^>]*>?/gm, ''); + } if (isEmpty(text)) { return null; diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 200a156bc..975ffd668 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -9,6 +9,7 @@ const linkify = LinkifyIt(); interface Props { text: string; + isRss?: boolean; /** Allows you to customize now non-links are rendered. Simplest is just a . */ renderNonLink?: RenderTextCallbackType; } @@ -22,9 +23,25 @@ export class Linkify extends React.Component { }; public render() { - const { text, renderNonLink } = this.props; - const matchData = linkify.match(text) || []; + const { text, renderNonLink, isRss } = this.props; const results: Array = []; + + if (isRss && text.indexOf(' + ); + // should already have links + + return results; + } + + const matchData = linkify.match(text) || []; let last = 0; let count = 1; diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 764aa397b..e9489ed96 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -87,6 +87,7 @@ export interface Props { expirationTimestamp?: number; isP2p?: boolean; isPublic?: boolean; + isRss?: boolean; onClickAttachment?: (attachment: AttachmentType) => void; onClickLinkPreview?: (url: string) => void; @@ -676,7 +677,7 @@ export class Message extends React.PureComponent { } public renderText() { - const { text, textPending, i18n, direction, status } = this.props; + const { text, textPending, i18n, direction, status, isRss } = this.props; const contents = direction === 'incoming' && status === 'error' @@ -700,6 +701,7 @@ export class Message extends React.PureComponent { > diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index fa04036c9..f9a750f32 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -9,6 +9,7 @@ import { LocalizerType, RenderTextCallbackType } from '../../types/Util'; interface Props { text: string; + isRss?: boolean; textPending?: boolean; /** If set, all emoji will be the same size. Otherwise, just one emoji will be large. */ disableJumbomoji?: boolean; @@ -73,6 +74,7 @@ export class MessageBody extends React.Component { textPending, disableJumbomoji, disableLinks, + isRss, i18n, } = this.props; const sizeClass = disableJumbomoji ? undefined : getSizeClass(text); @@ -93,6 +95,7 @@ export class MessageBody extends React.Component { return this.addDownloading( { return renderEmoji({ i18n, diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 21e1072f6..470c5bd2d 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -40,6 +40,7 @@ export type ConversationType = { lastMessage?: { status: 'error' | 'sending' | 'sent' | 'delivered' | 'read'; text: string; + isRss: boolean; }; phoneNumber: string; type: 'direct' | 'group'; diff --git a/tslint.json b/tslint.json index 50536f021..d875d9f6a 100644 --- a/tslint.json +++ b/tslint.json @@ -136,7 +136,15 @@ // 'as' is nicer than angle brackets. "prefer-type-cast": false, // We use || and && shortcutting because we're javascript programmers - "strict-boolean-expressions": false + "strict-boolean-expressions": false, + "react-no-dangerous-html": [ + true, + { + "file": "ts/components/conversation/Linkify.tsx", + "method": "render", + "comment": "Usage has been approved by Ryan Tharp on 2019-07-22" + } + ] }, "rulesDirectory": ["node_modules/tslint-microsoft-contrib"] } From 3d40b550115d00a16b6831c957e3c6f804189641 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Mon, 22 Jul 2019 23:49:24 -0700 Subject: [PATCH 26/71] do the deferencing correctly --- js/modules/loki_rss_api.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index 1138d8ffe..c2331c2a6 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -17,7 +17,7 @@ function xml2json(xml) { if (xml.children.length > 0) { for (let i = 0; i < xml.children.length; i += 1) { const item = xml.children.item(i); - const { nodeName } = item.nodeName; + const { nodeName } = item; if (typeof obj[nodeName] === 'undefined') { obj[nodeName] = xml2json(item); @@ -115,6 +115,11 @@ class LokiRssAPI extends EventEmitter { // (delay to after the network response intentionally) this.setupConversation(); + if (!feedObj || !feedObj.rss || !feedObj.rss.channel) { + log.error('rsserror', feedObj, feedDOM, responseXML); + return; + } + feedObj.rss.channel.item.reverse().forEach(item => { // log.debug('item', item) From bbffa89308914b805e572aecb636fb99f9ca7cec Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Tue, 30 Jul 2019 02:50:05 -0700 Subject: [PATCH 27/71] make sure we have braces --- js/models/conversations.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index d5b0c31bb..434eb4fcc 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2102,9 +2102,12 @@ const profileAvatar = this.get('profileAvatar'); if (profileAvatar !== avatarPath || currentName !== name) { // only update changed items - if (profileAvatar !== avatarPath) + if (profileAvatar !== avatarPath) { this.set({ profileAvatar: avatarPath }); - if (currentName !== name) this.set({ name }); + } + if (currentName !== name) { + this.set({ name }); + } // save await window.Signal.Data.updateConversation(this.id, this.attributes, { Conversation: Whisper.Conversation, From 22cb7cd540f212dab1714683d66c95122a84929d Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Tue, 30 Jul 2019 02:50:49 -0700 Subject: [PATCH 28/71] node/browser constant --- js/modules/loki_friend_request_status.js | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 js/modules/loki_friend_request_status.js diff --git a/js/modules/loki_friend_request_status.js b/js/modules/loki_friend_request_status.js new file mode 100644 index 000000000..c7896c0da --- /dev/null +++ b/js/modules/loki_friend_request_status.js @@ -0,0 +1,25 @@ +const friendRequestStatusEnum = Object.freeze({ + // New conversation, no messages sent or received + none: 0, + // This state is used to lock the input early while sending + pendingSend: 1, + // Friend request sent, awaiting response + requestSent: 2, + // Friend request received, awaiting user input + requestReceived: 3, + // We did it! + friends: 4, + // Friend Request sent but timed out + requestExpired: 5, +}); + +// node and browser compatibility +(function exportLocalVariable(ref) { + if (ref.constructor.name === 'Module') { + // node + module.exports = friendRequestStatusEnum; + } else { + // browser + // friendRequestStatusEnum should be already set + } +})(typeof module === 'undefined' ? this : module); From 93eb19bab50f99ba06a676dba6d1412818b1385a Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Tue, 30 Jul 2019 02:51:19 -0700 Subject: [PATCH 29/71] add space, refactor out friendRequestStatusEnum --- js/modules/loki_rss_api.js | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index c2331c2a6..18e807194 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -5,6 +5,8 @@ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); +const friendRequestStatusEnum = require('./loki_friend_request_status'); + const RSS_FEED = 'https://loki.network/feed/'; const CONVO_ID = 'rss://loki.network/feed/'; const PER_MIN = 60 * 1000; @@ -41,22 +43,6 @@ function xml2json(xml) { return {}; } -// hate duplicating this here... -const friendRequestStatusEnum = Object.freeze({ - // New conversation, no messages sent or received - none: 0, - // This state is used to lock the input early while sending - pendingSend: 1, - // Friend request sent, awaiting response - requestSent: 2, - // Friend request received, awaiting user input - requestReceived: 3, - // We did it! - friends: 4, - // Friend Request sent but timed out - requestExpired: 5, -}); - class LokiRssAPI extends EventEmitter { constructor() { super(); @@ -135,7 +121,7 @@ class LokiRssAPI extends EventEmitter { receivedAt, isRss: true, message: { - body: `

${item.title}

${item.description}`, + body: `

${item.title}

${item.description}`, attachments: [], group: { id: this.groupId, From a0e7ba252c4d5f964ea07b64be31ae5c39d26d78 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Tue, 30 Jul 2019 02:52:06 -0700 Subject: [PATCH 30/71] strip style and improving stripping regex --- ts/components/conversation/Linkify.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 975ffd668..d5514ada4 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -31,8 +31,8 @@ export class Linkify extends React.Component {
)<[^<]*)*<\/script>/gi, '') + .replace(/)<[^<]*)*<\/style>/gi, ''), }} /> ); From a67e7fcfe161b79dee286b06036122e863faf4d8 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Tue, 30 Jul 2019 03:19:10 -0700 Subject: [PATCH 31/71] guard for CI fix --- js/models/conversations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 434eb4fcc..7c0afaae1 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -197,7 +197,7 @@ return this.id.match(/^publicChat:/); }, isRss() { - return this.id.match(/^rss:/); + return this.id && this.id.match(/^rss:/); }, isBlocked() { return BlockedNumberController.isBlocked(this.id); From 9bd6b7a3640c08e191dbeb1abb2a644fd75250db Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Tue, 30 Jul 2019 03:19:22 -0700 Subject: [PATCH 32/71] lint --- ts/components/conversation/Linkify.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index d5514ada4..0f27953d7 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -31,7 +31,10 @@ export class Linkify extends React.Component {
)<[^<]*)*<\/script>/gi, '') + .replace( + /)<[^<]*)*<\/script>/gi, + '' + ) .replace(/)<[^<]*)*<\/style>/gi, ''), }} /> From d3cf055128b5e7f7a7d84d1be6666836f089ff32 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Wed, 31 Jul 2019 23:54:11 -0700 Subject: [PATCH 33/71] rss avatar guard --- js/models/conversations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/models/conversations.js b/js/models/conversations.js index 7c0afaae1..2d2515f13 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -305,6 +305,7 @@ }, async updateProfileAvatar() { + if (this.isRss()) return; const path = profileImages.getOrCreateImagePath(this.id); await this.setProfileAvatar(path); }, From d0ddcaf1fe6e34fbf4d360071f457885c1337b70 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Wed, 31 Jul 2019 23:54:57 -0700 Subject: [PATCH 34/71] update feed url, handle no items more gracefully, start on dns check --- js/modules/loki_rss_api.js | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index 18e807194..d50e326df 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -7,12 +7,35 @@ const nodeFetch = require('node-fetch'); const friendRequestStatusEnum = require('./loki_friend_request_status'); -const RSS_FEED = 'https://loki.network/feed/'; -const CONVO_ID = 'rss://loki.network/feed/'; +const RSS_FEED = 'https://loki.network/category/messenger-updates/feed/'; +const CONVO_ID = 'rss://loki.network/category/messenger-updates/feed/'; const PER_MIN = 60 * 1000; const PER_HR = 60 * PER_MIN; const RSS_POLL_EVERY = 1 * PER_HR; // once an hour +/* +const dnsUtil = require('dns'); +// how do we get our local version? +// how do we integrate with extensions.expired() +const VERSION_HOST = 'lastreleasedate.messenger.loki.network'; + +function getLastRelease(cb) { + // doesn't look to have a promise interface + dnsUtil.resolveTxt(VERSION_HOST, function handleResponse(err, records) { + if (err) { + console.error('getLastRelease error', err); + cb(); + return; + } + if (records.length) { + cb(); + } + // return first record... + cb(records[0]); + }); +} +*/ + function xml2json(xml) { try { let obj = {}; @@ -105,7 +128,10 @@ class LokiRssAPI extends EventEmitter { log.error('rsserror', feedObj, feedDOM, responseXML); return; } - + if (!feedObj.rss.channel.item) { + // no records + return; + } feedObj.rss.channel.item.reverse().forEach(item => { // log.debug('item', item) From 1536a7e5f9ea7162805bd7577bb8d0eab6656ad5 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Thu, 22 Aug 2019 17:22:31 -0700 Subject: [PATCH 35/71] support multiple RSS feeds --- js/modules/loki_rss_api.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index d50e326df..8bdc9d3b7 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -7,8 +7,6 @@ const nodeFetch = require('node-fetch'); const friendRequestStatusEnum = require('./loki_friend_request_status'); -const RSS_FEED = 'https://loki.network/category/messenger-updates/feed/'; -const CONVO_ID = 'rss://loki.network/category/messenger-updates/feed/'; const PER_MIN = 60 * 1000; const PER_HR = 60 * PER_MIN; const RSS_POLL_EVERY = 1 * PER_HR; // once an hour @@ -67,10 +65,14 @@ function xml2json(xml) { } class LokiRssAPI extends EventEmitter { - constructor() { + constructor(settings) { super(); // properties - this.groupId = CONVO_ID; + this.feedUrl = settings.RSS_FEED; + this.groupId = settings.CONVO_ID; + this.feedTitle = settings.title; + this.closeable = settings.closeable; + // non configureable options this.feedTimer = null; this.conversationSetup = false; // initial set up @@ -88,7 +90,7 @@ class LokiRssAPI extends EventEmitter { ); conversation.setFriendRequestStatus(friendRequestStatusEnum.friends); conversation.setGroupNameAndAvatar( - 'Loki.network News', + this.feedTitle, //'Loki.network News', 'images/loki/loki_icon.png' ); conversation.updateTextInputState(); @@ -100,7 +102,7 @@ class LokiRssAPI extends EventEmitter { let response; let success = true; try { - response = await nodeFetch(RSS_FEED); + response = await nodeFetch(this.feedUrl); } catch (e) { log.error('fetcherror', e); success = false; From 962469a54c427b445e7ebf9c4c83e409a013b9ee Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Thu, 22 Aug 2019 17:22:51 -0700 Subject: [PATCH 36/71] messenger and loki.network feeds --- js/background.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/js/background.js b/js/background.js index 13ae7d9c3..002ef12d5 100644 --- a/js/background.js +++ b/js/background.js @@ -228,7 +228,21 @@ window.log.warn(`Could not set up channel for ${conversation.id}`); } }); - window.lokiRssAPI = new window.LokiRssAPI(); + window.feeds = [] + window.feeds.push(new window.LokiRssAPI({ + RSS_FEED: 'https://loki.network/category/messenger-updates/feed/', + CONVO_ID: 'rss://loki.network/category/messenger-updates/feed/', + title: 'Messenger updates', + closeable: false + })); + /* + window.feeds.push(new window.LokiRssAPI({ + RSS_FEED: 'https://loki.network/feed/', + CONVO_ID: 'rss://loki.network/feed/', + title: 'Loki.network News', + closeable: true + })); + */ window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { const isPing = true; From 0a1655b03df2223e10420f2337739ccb8ffbad6a Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Thu, 22 Aug 2019 17:23:02 -0700 Subject: [PATCH 37/71] poll all feeds --- libtextsecure/message_receiver.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 42312b596..d284d924c 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -17,7 +17,7 @@ /* global localServerPort: false */ /* global lokiMessageAPI: false */ /* global lokiP2pAPI: false */ -/* global lokiRssAPI: false */ +/* global feeds: false */ /* eslint-disable more/no-then */ /* eslint-disable no-unreachable */ @@ -78,7 +78,10 @@ MessageReceiver.prototype.extend({ this.httpPollingResource.pollServer(); localLokiServer.on('message', this.handleP2pMessage.bind(this)); lokiPublicChatAPI.on('publicMessage', this.handlePublicMessage.bind(this)); - lokiRssAPI.on('rssMessage', this.handlePublicMessage.bind(this)); + // set up pollers for any RSS feeds + feeds.forEach(feed => { + feed.on('rssMessage', this.handleRssMessage.bind(this)); + }) this.startLocalServer(); // TODO: Rework this socket stuff to work with online messaging From 48f2637ff9b67636c3f6bddc8834ae4adad66f82 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Thu, 22 Aug 2019 17:51:18 -0700 Subject: [PATCH 38/71] lint --- js/background.js | 16 +++++++++------- js/modules/loki_rss_api.js | 2 +- libtextsecure/message_receiver.js | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/js/background.js b/js/background.js index 002ef12d5..fe84f03c2 100644 --- a/js/background.js +++ b/js/background.js @@ -228,13 +228,15 @@ window.log.warn(`Could not set up channel for ${conversation.id}`); } }); - window.feeds = [] - window.feeds.push(new window.LokiRssAPI({ - RSS_FEED: 'https://loki.network/category/messenger-updates/feed/', - CONVO_ID: 'rss://loki.network/category/messenger-updates/feed/', - title: 'Messenger updates', - closeable: false - })); + window.feeds = []; + window.feeds.push( + new window.LokiRssAPI({ + RSS_FEED: 'https://loki.network/category/messenger-updates/feed/', + CONVO_ID: 'rss://loki.network/category/messenger-updates/feed/', + title: 'Messenger updates', + closeable: false, + }) + ); /* window.feeds.push(new window.LokiRssAPI({ RSS_FEED: 'https://loki.network/feed/', diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index 8bdc9d3b7..438866dd1 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -90,7 +90,7 @@ class LokiRssAPI extends EventEmitter { ); conversation.setFriendRequestStatus(friendRequestStatusEnum.friends); conversation.setGroupNameAndAvatar( - this.feedTitle, //'Loki.network News', + this.feedTitle, 'images/loki/loki_icon.png' ); conversation.updateTextInputState(); diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index d284d924c..153b067c4 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -81,7 +81,7 @@ MessageReceiver.prototype.extend({ // set up pollers for any RSS feeds feeds.forEach(feed => { feed.on('rssMessage', this.handleRssMessage.bind(this)); - }) + }); this.startLocalServer(); // TODO: Rework this socket stuff to work with online messaging From 3f4b94ba7414bf88d03c4980c4e5252054c7a712 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 23 Aug 2019 13:36:36 +1000 Subject: [PATCH 39/71] Transition to initialising rss feeds in schema update --- app/sql.js | 115 ++++++++++++++++++++---------- js/background.js | 32 ++++----- js/models/conversations.js | 11 +++ js/modules/data.js | 9 +++ js/modules/loki_rss_api.js | 23 ------ libtextsecure/message_receiver.js | 6 +- 6 files changed, 113 insertions(+), 83 deletions(-) diff --git a/app/sql.js b/app/sql.js index 64fa1848a..fdae69227 100644 --- a/app/sql.js +++ b/app/sql.js @@ -97,6 +97,7 @@ module.exports = { updateConversation, removeConversation, getAllConversations, + getAllRssFeedConversations, getAllPublicConversations, getPubKeysWithFriendStatus, getAllConversationIds, @@ -784,57 +785,84 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { console.log('updateToLokiSchemaVersion1: starting...'); await instance.run('BEGIN TRANSACTION;'); - const publicChatData = { - id: 'publicChat:1@chat.lokinet.org', + await instance.run( + `ALTER TABLE messages + ADD COLUMN serverId STRING;` + ); + + const initConversation = async (data) => { + let { id, type, name, friendRequestStatus } = data; + await instance.run( + `INSERT INTO conversations ( + id, + json, + + type, + members, + name, + friendRequestStatus + ) values ( + $id, + $json, + + $type, + $members, + $name, + $friendRequestStatus + );`, + { + $id: id, + $json: objectToJSON(data), + + $type: type, + $members: null, + $name: name, + $friendRequestStatus: friendRequestStatus, + } + ); + } + + const baseData = { friendRequestStatus: 4, // Friends sealedSender: 0, sessionResetStatus: 0, swarmNodes: [], type: 'group', - server: 'https://chat.lokinet.org', - name: 'Loki Public Chat', - channelId: '1', unlockTimestamp: null, unreadCount: 0, verified: 0, version: 2, - }; - - const { id, type, name, friendRequestStatus } = publicChatData; - - await instance.run( - `ALTER TABLE messages - ADD COLUMN serverId STRING;` - ); + } - await instance.run( - `INSERT INTO conversations ( - id, - json, + const publicChatData = { + ...baseData, + id: 'publicChat:1@chat.lokinet.org', + server: 'https://chat.lokinet.org', + name: 'Loki Public Chat', + channelId: '1', + }; - type, - members, - name, - friendRequestStatus - ) values ( - $id, - $json, + const newsRssFeedData = { + ...baseData, + id: 'rss://loki.network/feed/', + rssFeed: 'https://loki.network/feed/', + closable: true, + name: 'Loki.network News', + profileAvatar: 'images/loki/loki_icon.png', + }; - $type, - $members, - $name, - $friendRequestStatus - );`, - { - $id: id, - $json: objectToJSON(publicChatData), + const updatesRssFeedData = { + ...baseData, + id: 'rss://loki.network/category/messenger-updates/feed/', + rssFeed: 'https://loki.network/category/messenger-updates/feed/', + closable: false, + name: 'Messenger updates', + profileAvatar: 'images/loki/loki_icon.png', + }; - $type: type, - $members: null, - $name: name, - $friendRequestStatus: friendRequestStatus, - } - ); + await initConversation(publicChatData); + await initConversation(newsRssFeedData); + await initConversation(updatesRssFeedData); await instance.run( `INSERT INTO loki_schema ( @@ -1606,6 +1634,17 @@ async function getAllPrivateConversations() { return map(rows, row => jsonToObject(row.json)); } +async function getAllRssFeedConversations() { + const rows = await db.all( + `SELECT json FROM conversations WHERE + type = 'group' AND + id LIKE 'rss://%' + ORDER BY id ASC;` + ); + + return map(rows, row => jsonToObject(row.json)); +} + async function getAllPublicConversations() { const rows = await db.all( `SELECT json FROM conversations WHERE diff --git a/js/background.js b/js/background.js index fe84f03c2..dab134217 100644 --- a/js/background.js +++ b/js/background.js @@ -206,6 +206,17 @@ const initAPIs = async () => { const ourKey = textsecure.storage.user.getNumber(); + const rssFeedConversations = await window.Signal.Data.getAllRssFeedConversations( + { + ConversationCollection: Whisper.ConversationCollection, + } + ); + window.feeds = []; + rssFeedConversations.forEach(conversation => { + window.feeds.push( + new window.LokiRssAPI(conversation.getRssSettings()) + ); + }); window.lokiMessageAPI = new window.LokiMessageAPI(ourKey); window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); const publicConversations = await window.Signal.Data.getAllPublicConversations( @@ -228,23 +239,6 @@ window.log.warn(`Could not set up channel for ${conversation.id}`); } }); - window.feeds = []; - window.feeds.push( - new window.LokiRssAPI({ - RSS_FEED: 'https://loki.network/category/messenger-updates/feed/', - CONVO_ID: 'rss://loki.network/category/messenger-updates/feed/', - title: 'Messenger updates', - closeable: false, - }) - ); - /* - window.feeds.push(new window.LokiRssAPI({ - RSS_FEED: 'https://loki.network/feed/', - CONVO_ID: 'rss://loki.network/feed/', - title: 'Loki.network News', - closeable: true - })); - */ window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { const isPing = true; @@ -594,7 +588,7 @@ window.log.info('Cleanup: complete'); window.log.info('listening for registration events'); - Whisper.events.on('registration_done', () => { + Whisper.events.on('registration_done', async () => { window.log.info('handling registration event'); startLocalLokiServer(); @@ -608,7 +602,7 @@ // logger: window.log, // }); - initAPIs(); + await initAPIs(); connect(true); }); diff --git a/js/models/conversations.js b/js/models/conversations.js index 2d2515f13..2fdaa0b1a 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2041,6 +2041,17 @@ getNickname() { return this.get('nickname'); }, + getRssSettings() { + if (!this.isRss()) { + return null; + } + return { + RSS_FEED: this.get('rssFeed'), + CONVO_ID: this.id, + title: this.get('name'), + closeable: this.get('closable'), + }; + }, // maybe "Backend" instead of "Source"? getPublicSource() { if (!this.isPublic()) { diff --git a/js/modules/data.js b/js/modules/data.js index cebc102cb..36be09d6f 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -118,6 +118,7 @@ module.exports = { getPubKeysWithFriendStatus, getAllConversationIds, getAllPrivateConversations, + getAllRssFeedConversations, getAllPublicConversations, getAllGroupsInvolvingId, @@ -741,6 +742,14 @@ async function getAllConversationIds() { return ids; } +async function getAllRssFeedConversations({ ConversationCollection }) { + const conversations = await channels.getAllRssFeedConversations(); + + const collection = new ConversationCollection(); + collection.add(conversations); + return collection; +} + async function getAllPublicConversations({ ConversationCollection }) { const conversations = await channels.getAllPublicConversations(); diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index 438866dd1..4a02c04af 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -79,25 +79,6 @@ class LokiRssAPI extends EventEmitter { this.getFeed(); } - setupConversation() { - // only run once - if (this.conversationSetup) return; - // wait until conversations are loaded - if (ConversationController._initialFetchComplete) { - const conversation = ConversationController.getOrCreate( - this.groupId, - 'group' - ); - conversation.setFriendRequestStatus(friendRequestStatusEnum.friends); - conversation.setGroupNameAndAvatar( - this.feedTitle, - 'images/loki/loki_icon.png' - ); - conversation.updateTextInputState(); - this.conversationSetup = true; // prevent running again - } - } - async getFeed() { let response; let success = true; @@ -122,10 +103,6 @@ class LokiRssAPI extends EventEmitter { const feedObj = xml2json(feedDOM); let receivedAt = new Date().getTime(); - // make sure conversation is set up properly - // (delay to after the network response intentionally) - this.setupConversation(); - if (!feedObj || !feedObj.rss || !feedObj.rss.channel) { log.error('rsserror', feedObj, feedDOM, responseXML); return; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 153b067c4..8530d190b 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -77,10 +77,10 @@ MessageReceiver.prototype.extend({ }); this.httpPollingResource.pollServer(); localLokiServer.on('message', this.handleP2pMessage.bind(this)); - lokiPublicChatAPI.on('publicMessage', this.handlePublicMessage.bind(this)); + lokiPublicChatAPI.on('publicMessage', this.handleUnencryptedMessage.bind(this)); // set up pollers for any RSS feeds feeds.forEach(feed => { - feed.on('rssMessage', this.handleRssMessage.bind(this)); + feed.on('rssMessage', this.handleUnencryptedMessage.bind(this)); }); this.startLocalServer(); @@ -149,7 +149,7 @@ MessageReceiver.prototype.extend({ }; this.httpPollingResource.handleMessage(message, options); }, - handlePublicMessage({ message }) { + handleUnencryptedMessage({ message }) { const ev = new Event('message'); ev.confirm = function confirmTerm() {}; ev.data = message; From 662c64f347ce621f0f2e4c0fe5a2a2b9dc9c0e42 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 23 Aug 2019 14:23:21 +1000 Subject: [PATCH 40/71] Remove ability to delete some rss conversations --- js/models/conversations.js | 4 ++++ js/views/conversation_view.js | 1 + ts/components/ConversationListItem.tsx | 4 +++- ts/components/conversation/ConversationHeader.tsx | 4 +++- ts/state/ducks/conversations.ts | 1 + 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 2fdaa0b1a..aec65b55d 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -196,6 +196,9 @@ isPublic() { return this.id.match(/^publicChat:/); }, + isClosable() { + return !this.isRss() || this.get('closable'); + }, isRss() { return this.id && this.id.match(/^rss:/); }, @@ -441,6 +444,7 @@ color, type: this.isPrivate() ? 'direct' : 'group', isMe: this.isMe(), + isClosable: this.isClosable(), isTyping: typingKeys.length > 0, lastUpdated: this.get('timestamp'), name: this.getName(), diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 4309a79e5..edea0036d 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -201,6 +201,7 @@ isVerified: this.model.isVerified(), isKeysPending: !this.model.isFriend(), isMe: this.model.isMe(), + isClosable: this.model.isClosable(), isBlocked: this.model.isBlocked(), isGroup: !this.model.isPrivate(), isOnline: this.model.isOnline(), diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 60870f6b3..f14d01856 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -21,6 +21,7 @@ export type PropsData = { type: 'group' | 'direct'; avatarPath?: string; isMe: boolean; + isClosable?: boolean; lastUpdated: number; unreadCount: number; @@ -163,6 +164,7 @@ export class ConversationListItem extends React.PureComponent { i18n, isBlocked, isMe, + isClosable, hasNickname, onDeleteContact, onDeleteMessages, @@ -191,7 +193,7 @@ export class ConversationListItem extends React.PureComponent { ) : null} {i18n('copyPublicKey')} {i18n('deleteMessages')} - {!isMe ? ( + {!isMe && isClosable ? ( {i18n('deleteContact')} ) : null} diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index b1497e21c..dfc10c197 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -26,6 +26,7 @@ interface Props { isVerified: boolean; isMe: boolean; + isClosable?: boolean; isGroup: boolean; isArchived: boolean; @@ -201,6 +202,7 @@ export class ConversationHeader extends React.Component { i18n, isBlocked, isMe, + isClosable, isGroup, isArchived, onDeleteMessages, @@ -275,7 +277,7 @@ export class ConversationHeader extends React.Component { {i18n('archiveConversation')} )} {i18n('deleteMessages')} - {!isMe ? ( + {!isMe && isClosable ? ( {i18n('deleteContact')} ) : null} diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 470c5bd2d..77379eb03 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -45,6 +45,7 @@ export type ConversationType = { phoneNumber: string; type: 'direct' | 'group'; isMe: boolean; + isClosable?: boolean; lastUpdated: number; unreadCount: number; isSelected: boolean; From 895cedc80116048e8d44f381ceff1ada44cf634f Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 23 Aug 2019 15:03:08 +1000 Subject: [PATCH 41/71] Lint --- app/sql.js | 8 +++---- js/background.js | 4 +--- js/modules/loki_friend_request_status.js | 25 ---------------------- js/modules/loki_rss_api.js | 27 +----------------------- libtextsecure/message_receiver.js | 5 ++++- 5 files changed, 10 insertions(+), 59 deletions(-) delete mode 100644 js/modules/loki_friend_request_status.js diff --git a/app/sql.js b/app/sql.js index fdae69227..23fd7d40e 100644 --- a/app/sql.js +++ b/app/sql.js @@ -790,8 +790,8 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { ADD COLUMN serverId STRING;` ); - const initConversation = async (data) => { - let { id, type, name, friendRequestStatus } = data; + const initConversation = async data => { + const { id, type, name, friendRequestStatus } = data; await instance.run( `INSERT INTO conversations ( id, @@ -820,7 +820,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { $friendRequestStatus: friendRequestStatus, } ); - } + }; const baseData = { friendRequestStatus: 4, // Friends @@ -832,7 +832,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { unreadCount: 0, verified: 0, version: 2, - } + }; const publicChatData = { ...baseData, diff --git a/js/background.js b/js/background.js index dab134217..141218b5f 100644 --- a/js/background.js +++ b/js/background.js @@ -213,9 +213,7 @@ ); window.feeds = []; rssFeedConversations.forEach(conversation => { - window.feeds.push( - new window.LokiRssAPI(conversation.getRssSettings()) - ); + window.feeds.push(new window.LokiRssAPI(conversation.getRssSettings())); }); window.lokiMessageAPI = new window.LokiMessageAPI(ourKey); window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); diff --git a/js/modules/loki_friend_request_status.js b/js/modules/loki_friend_request_status.js deleted file mode 100644 index c7896c0da..000000000 --- a/js/modules/loki_friend_request_status.js +++ /dev/null @@ -1,25 +0,0 @@ -const friendRequestStatusEnum = Object.freeze({ - // New conversation, no messages sent or received - none: 0, - // This state is used to lock the input early while sending - pendingSend: 1, - // Friend request sent, awaiting response - requestSent: 2, - // Friend request received, awaiting user input - requestReceived: 3, - // We did it! - friends: 4, - // Friend Request sent but timed out - requestExpired: 5, -}); - -// node and browser compatibility -(function exportLocalVariable(ref) { - if (ref.constructor.name === 'Module') { - // node - module.exports = friendRequestStatusEnum; - } else { - // browser - // friendRequestStatusEnum should be already set - } -})(typeof module === 'undefined' ? this : module); diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index 4a02c04af..a6f446dbe 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -1,39 +1,14 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-loop-func */ -/* global log, window, textsecure, ConversationController */ +/* global log, window, textsecure */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); -const friendRequestStatusEnum = require('./loki_friend_request_status'); - const PER_MIN = 60 * 1000; const PER_HR = 60 * PER_MIN; const RSS_POLL_EVERY = 1 * PER_HR; // once an hour -/* -const dnsUtil = require('dns'); -// how do we get our local version? -// how do we integrate with extensions.expired() -const VERSION_HOST = 'lastreleasedate.messenger.loki.network'; - -function getLastRelease(cb) { - // doesn't look to have a promise interface - dnsUtil.resolveTxt(VERSION_HOST, function handleResponse(err, records) { - if (err) { - console.error('getLastRelease error', err); - cb(); - return; - } - if (records.length) { - cb(); - } - // return first record... - cb(records[0]); - }); -} -*/ - function xml2json(xml) { try { let obj = {}; diff --git a/libtextsecure/message_receiver.js b/libtextsecure/message_receiver.js index 8530d190b..a65955bed 100644 --- a/libtextsecure/message_receiver.js +++ b/libtextsecure/message_receiver.js @@ -77,7 +77,10 @@ MessageReceiver.prototype.extend({ }); this.httpPollingResource.pollServer(); localLokiServer.on('message', this.handleP2pMessage.bind(this)); - lokiPublicChatAPI.on('publicMessage', this.handleUnencryptedMessage.bind(this)); + lokiPublicChatAPI.on( + 'publicMessage', + this.handleUnencryptedMessage.bind(this) + ); // set up pollers for any RSS feeds feeds.forEach(feed => { feed.on('rssMessage', this.handleUnencryptedMessage.bind(this)); From eec4f22b50e350cda59a99cfe4727a5e06d70b77 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Wed, 28 Aug 2019 17:08:32 +1000 Subject: [PATCH 42/71] Add crypto helpers and public error --- libloki/crypto.js | 22 ++++++++++++++++++++++ libtextsecure/errors.js | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/libloki/crypto.js b/libloki/crypto.js index 7a0c7a271..dedda4016 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -158,6 +158,27 @@ } } + async function decryptToken(ivAndCipherText64, serverPubKey64) { + const ivAndCipherText = new Uint8Array( + dcodeIO.ByteBuffer.fromBase64(ivAndCipherText64).toArrayBuffer() + ); + const iv = ivAndCipherText.slice(0, IV_LENGTH); + const cipherText = ivAndCipherText.slice(IV_LENGTH); + + const serverPubKey = new Uint8Array( + dcodeIO.ByteBuffer.fromBase64(serverPubKey64).toArrayBuffer() + ); + const { privKey } = await textsecure.storage.protocol.getIdentityKeyPair(); + const symmetricKey = libsignal.Curve.calculateAgreement( + serverPubKey, + privKey + ); + + const token = await libsignal.crypto.decrypt(symmetricKey, cipherText, iv); + const tokenString = dcodeIO.ByteBuffer.wrap(token).toString('utf8'); + return tokenString; + } + const snodeCipher = new LokiSnodeChannel(); window.libloki.crypto = { @@ -166,6 +187,7 @@ FallBackSessionCipher, FallBackDecryptionError, snodeCipher, + decryptToken, // for testing _LokiSnodeChannel: LokiSnodeChannel, _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey, diff --git a/libtextsecure/errors.js b/libtextsecure/errors.js index 7e16d4ecf..0343a836f 100644 --- a/libtextsecure/errors.js +++ b/libtextsecure/errors.js @@ -263,6 +263,16 @@ } } + function PublicTokenError(message) { + this.name = 'PublicTokenError'; + + ReplayableError.call(this, { + name: 'PublicTokenError', + message, + }); + } + inherit(ReplayableError, PublicTokenError); + function TimestampError(message) { this.name = 'TimeStampError'; @@ -305,4 +315,5 @@ window.textsecure.WrongDifficultyError = WrongDifficultyError; window.textsecure.TimestampError = TimestampError; window.textsecure.PublicChatError = PublicChatError; + window.textsecure.PublicTokenError = PublicTokenError; })(); From 1f7787ecf7dbc806e015b71cc2f6d494dc868e7b Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Wed, 28 Aug 2019 17:15:44 +1000 Subject: [PATCH 43/71] Add token retrieval functions to public chat api --- js/background.js | 15 ++-- js/modules/loki_public_chat_api.js | 107 ++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/js/background.js b/js/background.js index 141218b5f..a791a9ccc 100644 --- a/js/background.js +++ b/js/background.js @@ -224,18 +224,11 @@ ); publicConversations.forEach(conversation => { const settings = conversation.getPublicSource(); - window.log.info(`Setting up public conversation for ${conversation.id}`); - const publicChatServer = window.lokiPublicChatAPI.findOrCreateServer( - settings.server + window.lokiPublicChatAPI.registerChannel( + settings.server, + settings.channelId, + conversation.id ); - if (publicChatServer) { - publicChatServer.findOrCreateChannel( - settings.channelId, - conversation.id - ); - } else { - window.log.warn(`Could not set up channel for ${conversation.id}`); - } }); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 1acc1e884..a31a9a3d9 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -1,4 +1,4 @@ -/* global log, textsecure */ +/* global log, textsecure, libloki, Signal */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); @@ -22,6 +22,10 @@ class LokiPublicChatAPI extends EventEmitter { } return thisServer; } + registerChannel(hostport, channelId, conversationId) { + const server = this.findOrCreateServer(hostport); + server.findOrCreateChannel(channelId, conversationId); + } unregisterChannel(hostport, channelId) { let thisServer; let i = 0; @@ -46,6 +50,9 @@ class LokiPublicServerAPI { this.chatAPI = chatAPI; this.server = hostport; this.channels = []; + this.tokenPending = false; + this.tokenPromise = null; + this.baseServerUrl = `https://${this.server}`; } findOrCreateChannel(channelId, conversationId) { let thisChannel = this.channels.find( @@ -72,13 +79,102 @@ class LokiPublicServerAPI { this.channels.splice(i, 1); thisChannel.stopPolling = true; } + + async getServerToken() { + let token = await Signal.Data.getPublicServerTokenByServerName(this.server); + if (!token) { + token = await this.getNewToken(); + if (token) { + await Signal.Data.savePublicServerToken({ + server: this.server, + token, + }); + } + } + return token; + } + + async getNewToken() { + if (!this.tokenPending) { + this.tokenPending = true; + this.tokenPromise = new Promise(async res => { + const token = await this.requestToken(); + if (!token) { + res(null); + return; + } + const registered = await this.submitToken(token); + if (!registered) { + res(null); + return; + } + res(token); + }); + } + const token = await this.tokenPromise; + this.tokenPending = false; + return token; + } + + async requestToken() { + const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`); + const params = { + pubKey: this.chatAPI.ourKey, + }; + url.search = new URLSearchParams(params); + + let res; + try { + res = await nodeFetch(url); + } catch (e) { + return null; + } + if (!res.ok) { + return null; + } + const body = await res.json(); + const { cipherText64, serverPubKey64 } = body; + const token = await libloki.crypto.decryptToken( + cipherText64, + serverPubKey64 + ); + return token; + } + + async submitToken(token) { + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + pubKey: this.chatAPI.ourKey, + token, + }), + }; + + let res; + let success = true; + try { + res = await nodeFetch( + `${this.baseServerUrl}/loki/v1/submit_challenge`, + options + ); + success = res.ok; + } catch (e) { + return false; + } + return success; + } } class LokiPublicChannelAPI { constructor(serverAPI, channelId, conversationId) { this.serverAPI = serverAPI; this.channelId = channelId; - this.baseChannelUrl = `${serverAPI.server}/channels/${this.channelId}`; + this.baseChannelUrl = `${serverAPI.baseServerUrl}/channels/${ + this.channelId + }`; this.groupName = 'unknown'; this.conversationId = conversationId; this.lastGot = 0; @@ -88,6 +184,13 @@ class LokiPublicChannelAPI { this.pollForMessages(); } + getEndpoint() { + const endpoint = `https://${this.serverAPI.server}/channels/${ + this.channelId + }/messages`; + return endpoint; + } + async pollForChannel(source, endpoint) { // groupName will be loaded from server const url = new URL(this.baseChannelUrl); From fc8aa0211d3bac51297e3d955d3802f7290d7d33 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 10:59:23 +1000 Subject: [PATCH 44/71] Addressing reviews --- js/modules/loki_public_chat_api.js | 25 ++++++++----------------- libloki/crypto.js | 2 +- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index a31a9a3d9..6f2e1b916 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -50,7 +50,6 @@ class LokiPublicServerAPI { this.chatAPI = chatAPI; this.server = hostport; this.channels = []; - this.tokenPending = false; this.tokenPromise = null; this.baseServerUrl = `https://${this.server}`; } @@ -80,10 +79,10 @@ class LokiPublicServerAPI { thisChannel.stopPolling = true; } - async getServerToken() { + async getOrRefreshServerToken() { let token = await Signal.Data.getPublicServerTokenByServerName(this.server); if (!token) { - token = await this.getNewToken(); + token = await this.refreshServerToken(); if (token) { await Signal.Data.savePublicServerToken({ server: this.server, @@ -94,9 +93,8 @@ class LokiPublicServerAPI { return token; } - async getNewToken() { - if (!this.tokenPending) { - this.tokenPending = true; + async refreshServerToken() { + if (this.tokenPromise === null) { this.tokenPromise = new Promise(async res => { const token = await this.requestToken(); if (!token) { @@ -112,7 +110,7 @@ class LokiPublicServerAPI { }); } const token = await this.tokenPromise; - this.tokenPending = false; + this.tokenPromise = null; return token; } @@ -133,11 +131,7 @@ class LokiPublicServerAPI { return null; } const body = await res.json(); - const { cipherText64, serverPubKey64 } = body; - const token = await libloki.crypto.decryptToken( - cipherText64, - serverPubKey64 - ); + const token = await libloki.crypto.decryptToken(body); return token; } @@ -153,18 +147,15 @@ class LokiPublicServerAPI { }), }; - let res; - let success = true; try { - res = await nodeFetch( + const res = await nodeFetch( `${this.baseServerUrl}/loki/v1/submit_challenge`, options ); - success = res.ok; + return res.ok; } catch (e) { return false; } - return success; } } diff --git a/libloki/crypto.js b/libloki/crypto.js index dedda4016..df0b60a7f 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -158,7 +158,7 @@ } } - async function decryptToken(ivAndCipherText64, serverPubKey64) { + async function decryptToken({ ivAndCipherText64, serverPubKey64 }) { const ivAndCipherText = new Uint8Array( dcodeIO.ByteBuffer.fromBase64(ivAndCipherText64).toArrayBuffer() ); From 87e57af0908a3baec7cc48704340e644e357f2ff Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 13:19:27 +1000 Subject: [PATCH 45/71] Fix feed reader for single element results --- js/modules/loki_rss_api.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index a6f446dbe..bf4dedebb 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -86,6 +86,10 @@ class LokiRssAPI extends EventEmitter { // no records return; } + if (feedObj.rss.channel.item.constructor !== Array) { + // Treat single record as array for consistency + feedObj.rss.channel.item = [feedObj.rss.channel.item]; + } feedObj.rss.channel.item.reverse().forEach(item => { // log.debug('item', item) From 37366198fc9d36b3935a794f9bff7751489e3e78 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Wed, 28 Aug 2019 17:00:48 +1000 Subject: [PATCH 46/71] Add database tables and helper functions for public server auth --- app/sql.js | 78 +++++++++++++++++++++++++++++++++++++++++++++- js/modules/data.js | 23 ++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/app/sql.js b/app/sql.js index 23fd7d40e..7e076096e 100644 --- a/app/sql.js +++ b/app/sql.js @@ -94,11 +94,14 @@ module.exports = { saveConversation, saveConversations, getConversationById, + savePublicServerToken, + getPublicServerTokenByServerName, updateConversation, removeConversation, getAllConversations, getAllRssFeedConversations, getAllPublicConversations, + getPublicConversationsByServer, getPubKeysWithFriendStatus, getAllConversationIds, getAllPrivateConversations, @@ -790,6 +793,13 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { ADD COLUMN serverId STRING;` ); + await instance.run( + `CREATE TABLE server_tokens( + server STRING PRIMARY KEY ASC, + token TEXT + );` + ); + const initConversation = async data => { const { id, type, name, friendRequestStatus } = data; await instance.run( @@ -822,6 +832,11 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { ); }; + const lokiPublicServerData = { + server: 'chat.lokinet.org', + token: null, + }; + const baseData = { friendRequestStatus: 4, // Friends sealedSender: 0, @@ -837,11 +852,27 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { const publicChatData = { ...baseData, id: 'publicChat:1@chat.lokinet.org', - server: 'https://chat.lokinet.org', + server: 'chat.lokinet.org', name: 'Loki Public Chat', channelId: '1', }; + const { server, token } = lokiPublicServerData; + + await instance.run( + `INSERT INTO server_tokens ( + server, + token + ) values ( + $server, + $token + );`, + { + $server: server, + $token: token, + } + ); + const newsRssFeedData = { ...baseData, id: 'rss://loki.network/feed/', @@ -1590,6 +1621,38 @@ async function removeConversation(id) { ); } +async function savePublicServerToken(data) { + const { server, token } = data; + await db.run( + `INSERT OR REPLACE INTO server_tokens ( + server, + token + ) values ( + $server, + $token + )`, + { + $server: server, + $token: token, + } + ); +} + +async function getPublicServerTokenByServerName(server) { + const row = await db.get( + 'SELECT * FROM server_tokens WHERE server = $server;', + { + $server: server, + } + ); + + if (!row) { + return null; + } + + return row.token; +} + async function getConversationById(id) { const row = await db.get('SELECT * FROM conversations WHERE id = $id;', { $id: id, @@ -1656,6 +1719,19 @@ async function getAllPublicConversations() { return map(rows, row => jsonToObject(row.json)); } +async function getPublicConversationsByServer(server) { + const rows = await db.all( + `SELECT * FROM conversations WHERE + server = $server + ORDER BY id ASC;`, + { + $server: server, + } + ); + + return map(rows, row => jsonToObject(row.json)); +} + async function getAllGroupsInvolvingId(id) { const rows = await db.all( `SELECT json FROM conversations WHERE diff --git a/js/modules/data.js b/js/modules/data.js index 36be09d6f..9d9978843 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -120,6 +120,9 @@ module.exports = { getAllPrivateConversations, getAllRssFeedConversations, getAllPublicConversations, + getPublicConversationsByServer, + savePublicServerToken, + getPublicServerTokenByServerName, getAllGroupsInvolvingId, searchConversations, @@ -766,6 +769,26 @@ async function getAllPrivateConversations({ ConversationCollection }) { return collection; } +async function savePublicServerToken(data) { + await channels.savePublicServerToken(data); +} + +async function getPublicServerTokenByServerName(server) { + const token = await channels.getPublicServerTokenByServerName(server); + return token; +} + +async function getPublicConversationsByServer( + server, + { ConversationCollection } +) { + const conversations = await channels.getPublicConversationsByServer(server); + + const collection = new ConversationCollection(); + collection.add(conversations); + return collection; +} + async function getAllGroupsInvolvingId(id, { ConversationCollection }) { const conversations = await channels.getAllGroupsInvolvingId(id); From 716e142ac4bed1bf4c852ebca4a4ebd0c86809d5 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 10:49:10 +1000 Subject: [PATCH 47/71] Reviews --- app/sql.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/sql.js b/app/sql.js index 7e076096e..24835bbc4 100644 --- a/app/sql.js +++ b/app/sql.js @@ -794,7 +794,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { ); await instance.run( - `CREATE TABLE server_tokens( + `CREATE TABLE servers( server STRING PRIMARY KEY ASC, token TEXT );` @@ -851,8 +851,8 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { const publicChatData = { ...baseData, - id: 'publicChat:1@chat.lokinet.org', - server: 'chat.lokinet.org', + id: `publicChat:1@${lokiPublicServerData.server}`, + server: lokiPublicServerData.server, name: 'Loki Public Chat', channelId: '1', }; @@ -860,7 +860,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { const { server, token } = lokiPublicServerData; await instance.run( - `INSERT INTO server_tokens ( + `INSERT INTO servers ( server, token ) values ( @@ -1624,7 +1624,7 @@ async function removeConversation(id) { async function savePublicServerToken(data) { const { server, token } = data; await db.run( - `INSERT OR REPLACE INTO server_tokens ( + `INSERT OR REPLACE INTO servers ( server, token ) values ( @@ -1640,7 +1640,7 @@ async function savePublicServerToken(data) { async function getPublicServerTokenByServerName(server) { const row = await db.get( - 'SELECT * FROM server_tokens WHERE server = $server;', + 'SELECT * FROM servers WHERE server = $server;', { $server: server, } From b5fd01a468a0c34db79ae396034126d8755f0d91 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Wed, 28 Aug 2019 17:11:56 +1000 Subject: [PATCH 48/71] Add required metadata to sending pipeline and send to public channels --- js/models/conversations.js | 14 ++------- js/modules/loki_message_api.js | 48 +++++++++++++++++++++++-------- libtextsecure/outgoing_message.js | 13 +++++---- libtextsecure/sendmessage.js | 2 +- 4 files changed, 48 insertions(+), 29 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index aec65b55d..f3c47596d 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1374,8 +1374,9 @@ const options = this.getSendOptions(); options.messageType = message.get('type'); + options.isPublic = this.isPublic(); if (this.isPublic()) { - options.publicEndpoint = this.getEndpoint(); + options.channelSettings = this.getPublicSource(); } const groupNumbers = this.getRecipients(); @@ -2064,18 +2065,9 @@ return { server: this.get('server'), channelId: this.get('channelId'), + conversationId: this.get('id'), }; }, - // FIXME: remove or add public and/or "sending" hint to name... - getEndpoint() { - if (!this.isPublic()) { - return null; - } - const server = this.get('server'); - const channelId = this.get('channelId'); - const endpoint = `${server}/channels/${channelId}/messages`; - return endpoint; - }, // SIGNAL PROFILES diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 672e6c4f6..3e7767bb9 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -1,6 +1,7 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-loop-func */ -/* global log, dcodeIO, window, callWorker, lokiP2pAPI, lokiSnodeAPI, textsecure */ +/* global log, dcodeIO, window, callWorker, + lokiP2pAPI, lokiSnodeAPI, lokiPublicChatAPI, textsecure */ const _ = require('lodash'); const { rpc } = require('./loki_rpc'); @@ -78,8 +79,9 @@ class LokiMessageAPI { async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) { const { isPing = false, + isPublic = false, numConnections = DEFAULT_CONNECTIONS, - publicEndpoint = null, + channelSettings = null, } = options; // Data required to identify a message in a conversation const messageEventData = { @@ -87,10 +89,22 @@ class LokiMessageAPI { timestamp: messageTimeStamp, }; - // FIXME: should have public/sending(ish hint) in the option to make - // this more obvious... - if (publicEndpoint) { + if (isPublic) { // could we emit back to LokiPublicChannelAPI somehow? + const { server, channelId, conversationId } = channelSettings; + const serverAPI = lokiPublicChatAPI.findOrCreateServer(server); + const token = await serverAPI.getServerToken(); + if (!token) { + throw new window.textsecure.PublicChatError( + `Failed to retrieve valid token for ${conversationId}` + ); + } + const channelAPI = serverAPI.findOrCreateChannel( + channelId, + conversationId + ); + const publicEndpoint = channelAPI.getEndpoint(conversationId); + const { profile } = data; let displayName = 'Anonymous'; if (profile && profile.displayName) { @@ -109,24 +123,34 @@ class LokiMessageAPI { }, ], }; + let result; try { - const result = await nodeFetch(publicEndpoint, { + result = await nodeFetch(publicEndpoint, { method: 'post', headers: { 'Content-Type': 'application/json', - Authorization: 'Bearer loki', + Authorization: `Bearer ${token}`, }, body: JSON.stringify(payload), }); - const body = await result.json(); - messageEventData.serverId = body.data.id; - window.Whisper.events.trigger('publicMessageSent', messageEventData); - return; } catch (e) { throw new window.textsecure.PublicChatError( - 'Failed to send public chat message.' + `Failed to send public chat message: ${e}` ); } + const body = await result.json(); + if (!result.ok) { + if (result.status === 401) { + // TODO: Handle token timeout + } + const error = body.meta.error_message; + throw new window.textsecure.PublicChatError( + `Failed to send public chat message: ${error}` + ); + } + messageEventData.serverId = body.data.id; + window.Whisper.events.trigger('publicMessageSent', messageEventData); + return; } const data64 = dcodeIO.ByteBuffer.wrap(data).toString('base64'); diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index a0ab663a4..996983590 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -49,11 +49,13 @@ function OutgoingMessage( online, messageType, isPing, - publicEndpoint, + isPublic, + channelSettings, } = options || {}; this.numberInfo = numberInfo; - this.publicEndpoint = publicEndpoint; + this.isPublic = isPublic; + this.channelSettings = channelSettings; this.senderCertificate = senderCertificate; this.online = online; this.messageType = messageType || 'outgoing'; @@ -201,8 +203,9 @@ OutgoingMessage.prototype = { numConnections: NUM_SEND_CONNECTIONS, isPing: this.isPing, }; - if (this.publicEndpoint) { - options.publicEndpoint = this.publicEndpoint; + options.isPublic = this.isPublic; + if (this.isPublic) { + options.channelSettings = this.channelSettings; } await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); } catch (e) { @@ -270,7 +273,7 @@ OutgoingMessage.prototype = { }, doSendMessage(number, deviceIds, recurse) { const ciphers = {}; - if (this.publicEndpoint) { + if (this.isPublic) { return this.transmitMessage( number, this.message.dataMessage, diff --git a/libtextsecure/sendmessage.js b/libtextsecure/sendmessage.js index 649e668e7..b5b608f20 100644 --- a/libtextsecure/sendmessage.js +++ b/libtextsecure/sendmessage.js @@ -943,7 +943,7 @@ MessageSender.prototype = { ) { const me = textsecure.storage.user.getNumber(); let numbers = groupNumbers.filter(number => number !== me); - if (options.publicEndpoint) { + if (options.isPublic) { numbers = [groupId]; } const profile = textsecure.storage.impl.getLocalProfile(); From 700ed5d2a89e4689a3ca1ae5d26b6f6c2cf2ff95 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 14:15:36 +1000 Subject: [PATCH 49/71] Some refactoring from reviews, and include the token in the message pipeline instead of getting in message_api --- app/sql.js | 32 +++++++++++++++--------------- js/models/conversations.js | 19 +++++++++++++++++- js/modules/data.js | 6 +++--- js/modules/loki_message_api.js | 17 ++++------------ js/modules/loki_public_chat_api.js | 18 ++++++++++------- libloki/crypto.js | 29 +++++++++++++-------------- libtextsecure/outgoing_message.js | 6 +++--- 7 files changed, 69 insertions(+), 58 deletions(-) diff --git a/app/sql.js b/app/sql.js index 24835bbc4..77bde9694 100644 --- a/app/sql.js +++ b/app/sql.js @@ -95,7 +95,7 @@ module.exports = { saveConversations, getConversationById, savePublicServerToken, - getPublicServerTokenByServerName, + getPublicServerTokenByServerUrl, updateConversation, removeConversation, getAllConversations, @@ -795,7 +795,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { await instance.run( `CREATE TABLE servers( - server STRING PRIMARY KEY ASC, + serverUrl STRING PRIMARY KEY ASC, token TEXT );` ); @@ -833,7 +833,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { }; const lokiPublicServerData = { - server: 'chat.lokinet.org', + serverUrl: 'https://chat.lokinet.org', token: null, }; @@ -851,24 +851,24 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { const publicChatData = { ...baseData, - id: `publicChat:1@${lokiPublicServerData.server}`, - server: lokiPublicServerData.server, + id: 'publicChat:1@chat.lokinet.org', + server: lokiPublicServerData.serverUrl, name: 'Loki Public Chat', channelId: '1', }; - const { server, token } = lokiPublicServerData; + const { serverUrl, token } = lokiPublicServerData; await instance.run( `INSERT INTO servers ( - server, + serverUrl, token ) values ( - $server, + $serverUrl, $token );`, { - $server: server, + $serverUrl: serverUrl, $token: token, } ); @@ -1622,27 +1622,27 @@ async function removeConversation(id) { } async function savePublicServerToken(data) { - const { server, token } = data; + const { serverUrl, token } = data; await db.run( `INSERT OR REPLACE INTO servers ( - server, + serverUrl, token ) values ( - $server, + $serverUrl, $token )`, { - $server: server, + $serverUrl: serverUrl, $token: token, } ); } -async function getPublicServerTokenByServerName(server) { +async function getPublicServerTokenByServerUrl(serverUrl) { const row = await db.get( - 'SELECT * FROM servers WHERE server = $server;', + 'SELECT * FROM servers WHERE serverUrl = $serverUrl;', { - $server: server, + $serverUrl: serverUrl, } ); diff --git a/js/models/conversations.js b/js/models/conversations.js index f3c47596d..4b674503e 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -11,6 +11,7 @@ clipboard, BlockedNumberController, lokiP2pAPI, + lokiPublicChatAPI, JobQueue */ @@ -1376,7 +1377,7 @@ options.messageType = message.get('type'); options.isPublic = this.isPublic(); if (this.isPublic()) { - options.channelSettings = this.getPublicSource(); + options.publicSendData = await this.getPublicSendData(); } const groupNumbers = this.getRecipients(); @@ -2068,6 +2069,22 @@ conversationId: this.get('id'), }; }, + async getPublicSendData() { + const serverAPI = lokiPublicChatAPI.findOrCreateServer( + this.get('server') + ); + // Can be null if fails + const token = await serverAPI.getOrRefreshServerToken(); + const channelAPI = serverAPI.findOrCreateChannel( + this.get('channelId'), + this.id + ); + const publicEndpoint = channelAPI.getEndpoint(); + return { + publicEndpoint, + token, + }; + }, // SIGNAL PROFILES diff --git a/js/modules/data.js b/js/modules/data.js index 9d9978843..80c21bdee 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -122,7 +122,7 @@ module.exports = { getAllPublicConversations, getPublicConversationsByServer, savePublicServerToken, - getPublicServerTokenByServerName, + getPublicServerTokenByServerUrl, getAllGroupsInvolvingId, searchConversations, @@ -773,8 +773,8 @@ async function savePublicServerToken(data) { await channels.savePublicServerToken(data); } -async function getPublicServerTokenByServerName(server) { - const token = await channels.getPublicServerTokenByServerName(server); +async function getPublicServerTokenByServerUrl(serverUrl) { + const token = await channels.getPublicServerTokenByServerUrl(serverUrl); return token; } diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 3e7767bb9..556069bb0 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -1,7 +1,6 @@ /* eslint-disable no-await-in-loop */ /* eslint-disable no-loop-func */ -/* global log, dcodeIO, window, callWorker, - lokiP2pAPI, lokiSnodeAPI, lokiPublicChatAPI, textsecure */ +/* global log, dcodeIO, window, callWorker, lokiP2pAPI, lokiSnodeAPI, textsecure */ const _ = require('lodash'); const { rpc } = require('./loki_rpc'); @@ -81,7 +80,7 @@ class LokiMessageAPI { isPing = false, isPublic = false, numConnections = DEFAULT_CONNECTIONS, - channelSettings = null, + publicSendData = null, } = options; // Data required to identify a message in a conversation const messageEventData = { @@ -90,20 +89,12 @@ class LokiMessageAPI { }; if (isPublic) { - // could we emit back to LokiPublicChannelAPI somehow? - const { server, channelId, conversationId } = channelSettings; - const serverAPI = lokiPublicChatAPI.findOrCreateServer(server); - const token = await serverAPI.getServerToken(); + const { token, publicEndpoint } = publicSendData; if (!token) { throw new window.textsecure.PublicChatError( - `Failed to retrieve valid token for ${conversationId}` + `Failed to retrieve valid token for ${publicEndpoint}` ); } - const channelAPI = serverAPI.findOrCreateChannel( - channelId, - conversationId - ); - const publicEndpoint = channelAPI.getEndpoint(conversationId); const { profile } = data; let displayName = 'Anonymous'; diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 6f2e1b916..89d0fea7c 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -46,12 +46,11 @@ class LokiPublicChatAPI extends EventEmitter { } class LokiPublicServerAPI { - constructor(chatAPI, hostport) { + constructor(chatAPI, url) { this.chatAPI = chatAPI; - this.server = hostport; this.channels = []; this.tokenPromise = null; - this.baseServerUrl = `https://${this.server}`; + this.baseServerUrl = url; } findOrCreateChannel(channelId, conversationId) { let thisChannel = this.channels.find( @@ -80,12 +79,14 @@ class LokiPublicServerAPI { } async getOrRefreshServerToken() { - let token = await Signal.Data.getPublicServerTokenByServerName(this.server); + let token = await Signal.Data.getPublicServerTokenByServerUrl( + this.baseServerUrl + ); if (!token) { token = await this.refreshServerToken(); if (token) { await Signal.Data.savePublicServerToken({ - server: this.server, + serverUrl: this.baseServerUrl, token, }); } @@ -176,7 +177,7 @@ class LokiPublicChannelAPI { } getEndpoint() { - const endpoint = `https://${this.serverAPI.server}/channels/${ + const endpoint = `${this.serverAPI.baseServerUrl}/channels/${ this.channelId }/messages`; return endpoint; @@ -255,7 +256,10 @@ class LokiPublicChannelAPI { let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; - if (adnMessage.annotations.length) { + if (adnMessage.is_deleted) { + return; + } + if (adnMessage.annotations !== []) { const noteValue = adnMessage.annotations[0].value; ({ from, timestamp, source } = noteValue); } diff --git a/libloki/crypto.js b/libloki/crypto.js index df0b60a7f..f8ad8d7f2 100644 --- a/libloki/crypto.js +++ b/libloki/crypto.js @@ -34,8 +34,8 @@ async function DHDecrypt(symmetricKey, ivAndCiphertext) { const iv = ivAndCiphertext.slice(0, IV_LENGTH); - const cipherText = ivAndCiphertext.slice(IV_LENGTH); - return libsignal.crypto.decrypt(symmetricKey, cipherText, iv); + const ciphertext = ivAndCiphertext.slice(IV_LENGTH); + return libsignal.crypto.decrypt(symmetricKey, ciphertext, iv); } class FallBackSessionCipher { @@ -131,18 +131,18 @@ return this._ephemeralPubKeyHex; } - async decrypt(snodeAddress, ivAndCipherTextBase64) { - const ivAndCipherText = dcodeIO.ByteBuffer.wrap( - ivAndCipherTextBase64, + async decrypt(snodeAddress, ivAndCiphertextBase64) { + const ivAndCiphertext = dcodeIO.ByteBuffer.wrap( + ivAndCiphertextBase64, 'base64' ).toArrayBuffer(); const symmetricKey = await this._getSymmetricKey(snodeAddress); try { - const decrypted = await DHDecrypt(symmetricKey, ivAndCipherText); + const decrypted = await DHDecrypt(symmetricKey, ivAndCiphertext); const decoder = new TextDecoder(); return decoder.decode(decrypted); } catch (e) { - return ivAndCipherText; + return ivAndCiphertext; } } @@ -153,17 +153,15 @@ plainText = textEncoder.encode(plainText); } const symmetricKey = await this._getSymmetricKey(snodeAddress); - const cipherText = await DHEncrypt(symmetricKey, plainText); - return dcodeIO.ByteBuffer.wrap(cipherText).toString('base64'); + const ciphertext = await DHEncrypt(symmetricKey, plainText); + return dcodeIO.ByteBuffer.wrap(ciphertext).toString('base64'); } } - async function decryptToken({ ivAndCipherText64, serverPubKey64 }) { - const ivAndCipherText = new Uint8Array( - dcodeIO.ByteBuffer.fromBase64(ivAndCipherText64).toArrayBuffer() + async function decryptToken({ cipherText64, serverPubKey64 }) { + const ivAndCiphertext = new Uint8Array( + dcodeIO.ByteBuffer.fromBase64(cipherText64).toArrayBuffer() ); - const iv = ivAndCipherText.slice(0, IV_LENGTH); - const cipherText = ivAndCipherText.slice(IV_LENGTH); const serverPubKey = new Uint8Array( dcodeIO.ByteBuffer.fromBase64(serverPubKey64).toArrayBuffer() @@ -174,7 +172,8 @@ privKey ); - const token = await libsignal.crypto.decrypt(symmetricKey, cipherText, iv); + const token = await DHDecrypt(symmetricKey, ivAndCiphertext); + const tokenString = dcodeIO.ByteBuffer.wrap(token).toString('utf8'); return tokenString; } diff --git a/libtextsecure/outgoing_message.js b/libtextsecure/outgoing_message.js index 996983590..ca4142313 100644 --- a/libtextsecure/outgoing_message.js +++ b/libtextsecure/outgoing_message.js @@ -50,12 +50,12 @@ function OutgoingMessage( messageType, isPing, isPublic, - channelSettings, + publicSendData, } = options || {}; this.numberInfo = numberInfo; this.isPublic = isPublic; - this.channelSettings = channelSettings; + this.publicSendData = publicSendData; this.senderCertificate = senderCertificate; this.online = online; this.messageType = messageType || 'outgoing'; @@ -205,7 +205,7 @@ OutgoingMessage.prototype = { }; options.isPublic = this.isPublic; if (this.isPublic) { - options.channelSettings = this.channelSettings; + options.publicSendData = this.publicSendData; } await lokiMessageAPI.sendMessage(pubKey, data, timestamp, ttl, options); } catch (e) { From 6392c779523dc494b033b10baf276c56f2d685e0 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 14:35:01 +1000 Subject: [PATCH 50/71] Fix database helpers --- app/sql.js | 9 ++++++--- js/modules/data.js | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/sql.js b/app/sql.js index 77bde9694..bd1e4f1e6 100644 --- a/app/sql.js +++ b/app/sql.js @@ -790,7 +790,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { await instance.run( `ALTER TABLE messages - ADD COLUMN serverId STRING;` + ADD COLUMN serverId INTEGER;` ); await instance.run( @@ -2060,11 +2060,14 @@ async function removeMessage(id) { ); } -async function getMessageByServerId(serverId) { +async function getMessageByServerId(serverId, conversationId) { const row = await db.get( - 'SELECT * FROM messages WHERE serverId = $serverId;', + `SELECT * FROM messages WHERE + serverId = $serverId AND + conversationId = $conversationId;`, { $serverId: serverId, + $conversationId: conversationId, } ); diff --git a/js/modules/data.js b/js/modules/data.js index 80c21bdee..c410f9464 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -908,8 +908,8 @@ async function _removeMessages(ids) { await channels.removeMessage(ids); } -async function getMessageByServerId(id, { Message }) { - const message = await channels.getMessageByServerId(id); +async function getMessageByServerId(serverId, conversationId, { Message }) { + const message = await channels.getMessageByServerId(serverId, conversationId); if (!message) { return null; } From 5a70d88747aa7bf661ca5ec5f1e3ba1781fb574e Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 14:35:42 +1000 Subject: [PATCH 51/71] Add removeMessage function to conversation --- js/models/conversations.js | 10 ++++++++++ js/models/messages.js | 3 --- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index 4b674503e..218202f84 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2288,6 +2288,16 @@ }); }, + removeMessage(messageId) { + const message = this.messageCollection.models.find( + msg => msg.id === messageId + ); + if (message) { + message.trigger('unload'); + this.messageCollection.remove(messageId); + } + }, + deleteMessages() { Whisper.events.trigger('showConfirmationDialog', { message: i18n('deleteConversationConfirmation'), diff --git a/js/models/messages.js b/js/models/messages.js index b79307473..ce16c06d5 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -357,9 +357,6 @@ onDestroy() { this.cleanup(); }, - deleteMessage() { - this.trigger('delete', this); - }, async cleanup() { MessageController.unregister(this.id); this.unload(); From 3332cb240e0269de04f5d06608bf6f4e07bd9a28 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 14:37:04 +1000 Subject: [PATCH 52/71] Add background event listener --- js/background.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/js/background.js b/js/background.js index a791a9ccc..c30a385d0 100644 --- a/js/background.js +++ b/js/background.js @@ -487,6 +487,28 @@ } }); + Whisper.events.on( + 'deletePublicMessage', + async ({ messageServerId, conversationId }) => { + const message = await window.Signal.Data.getMessageByServerId( + messageServerId, + conversationId, + { + Message: Whisper.Message, + } + ); + if (message) { + const conversation = ConversationController.get(conversationId); + if (conversation) { + conversation.removeMessage(message.id); + } + await window.Signal.Data.removeMessage(message.id, { + Message: Whisper.Message, + }); + } + } + ); + Whisper.events.on('setupAsNewDevice', () => { const { appView } = window.owsDesktopApp; if (appView) { @@ -1418,6 +1440,7 @@ let messageData = { source: data.source, sourceDevice: data.sourceDevice, + serverId: data.serverId, sent_at: data.timestamp, received_at: data.receivedAt || Date.now(), conversationId: data.source, From 7d0b3002460645630ce951165e341862f157454a Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Thu, 29 Aug 2019 14:37:21 +1000 Subject: [PATCH 53/71] Add deletion polling --- js/modules/loki_public_chat_api.js | 75 ++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 89d0fea7c..f76f2e47d 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -1,9 +1,10 @@ -/* global log, textsecure, libloki, Signal */ +/* global log, textsecure, libloki, Signal, Whisper */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); const GROUPCHAT_POLL_EVERY = 1000; // 1 second +const DELETION_POLL_EVERY = 60000; // 1 minute // singleton to relay events to libtextsecure/message_receiver class LokiPublicChatAPI extends EventEmitter { @@ -174,6 +175,7 @@ class LokiPublicChannelAPI { log.info(`registered LokiPublicChannel ${channelId}`); // start polling this.pollForMessages(); + this.pollForDeletions(); } getEndpoint() { @@ -206,20 +208,56 @@ class LokiPublicChannelAPI { // read all messages from 0 to current // delete local copies if server state has changed to delete // run every minute - const url = new URL(this.baseChannelUrl); - let res; - let success = true; - try { - res = await nodeFetch(url); - } catch (e) { - success = false; - } + const pollAgain = () => { + setTimeout(() => { + this.pollForDeletions(); + }, DELETION_POLL_EVERY); + }; - const response = await res.json(); - if (response.meta.code !== 200) { - success = false; + let numChecked = 0; + const url = new URL(`${this.baseChannelUrl}/messages`); + const params = { + include_annotations: 1, + count: -200, + }; + let beforeId = 0; + + while (numChecked < 2000) { + params.before_id = beforeId; + url.search = new URLSearchParams(params); + let res; + try { + // eslint-disable-next-line no-await-in-loop + res = await nodeFetch(url); + } catch (e) { + pollAgain(); + return; + } + + // eslint-disable-next-line no-await-in-loop + const response = await res.json(); + if (response.meta.code !== 200) { + pollAgain(); + return; + } + numChecked += response.data.length; + // eslint-disable-next-line no-loop-func + response.data.reverse().forEach(adnMessage => { + if (beforeId === 0 || adnMessage.id < beforeId) { + beforeId = adnMessage.id; + } + if (adnMessage.is_deleted) { + Whisper.events.trigger('deletePublicMessage', { + messageServerId: adnMessage.id, + conversationId: this.conversationId, + }); + } + }); + if (response.data.length < 200) { + break; + } } - return success; + pollAgain(); } async pollForMessages() { @@ -227,6 +265,7 @@ class LokiPublicChannelAPI { const params = { include_annotations: 1, count: -20, + include_deleted: false, }; if (this.lastGot) { params.since_id = this.lastGot; @@ -264,6 +303,16 @@ class LokiPublicChannelAPI { ({ from, timestamp, source } = noteValue); } + if ( + !from || + !timestamp || + !source || + !adnMessage.id || + !adnMessage.text + ) { + return; // Invalid message + } + const messageData = { serverId: adnMessage.id, friendRequest: false, From d5e6ffb1f92d7193a27d01062ec434a5adbe1bfc Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Thu, 29 Aug 2019 18:41:54 -0700 Subject: [PATCH 54/71] rename deletePublicMessage => deleteLocalPublicMessage --- js/background.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/background.js b/js/background.js index c30a385d0..705b936ea 100644 --- a/js/background.js +++ b/js/background.js @@ -488,7 +488,7 @@ }); Whisper.events.on( - 'deletePublicMessage', + 'deleteLocalPublicMessage', async ({ messageServerId, conversationId }) => { const message = await window.Signal.Data.getMessageByServerId( messageServerId, From 807b32fac0ed620e144f1b0df49d10ec0d67b179 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Thu, 29 Aug 2019 18:41:54 -0700 Subject: [PATCH 55/71] remove unneeded $ on badge --- ts/components/conversation/Message.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index e9489ed96..7fdebbadc 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -257,7 +257,7 @@ export class Message extends React.PureComponent { `module-message__metadata__${badgeType}--${direction}` )} > -  • ${badgeText} +  • {badgeText} ) : null} {expirationLength && expirationTimestamp ? ( From 33572cac1fdb6090ac7279c924288762d92ffd7e Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Thu, 29 Aug 2019 18:41:54 -0700 Subject: [PATCH 56/71] use newer delete system every 5s, deleteMessage(), store token in serverAPI, serverRequest refactor, registerChannel now returns the channel found/created, change baseChannelUrl base --- js/modules/loki_public_chat_api.js | 165 +++++++++++++++++------------ 1 file changed, 100 insertions(+), 65 deletions(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index f76f2e47d..f2ea54166 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -1,10 +1,11 @@ -/* global log, textsecure, libloki, Signal, Whisper */ +/* global log, textsecure, libloki, Signal, Whisper, Headers */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); -const GROUPCHAT_POLL_EVERY = 1000; // 1 second -const DELETION_POLL_EVERY = 60000; // 1 minute +// Can't be less than 1200 if we have unauth'd requests +const GROUPCHAT_POLL_EVERY = 1500; // 1.5s +const DELETION_POLL_EVERY = 5000; // 1 second // singleton to relay events to libtextsecure/message_receiver class LokiPublicChatAPI extends EventEmitter { @@ -15,17 +16,18 @@ class LokiPublicChatAPI extends EventEmitter { this.servers = []; } findOrCreateServer(hostport) { - log.info(`LokiPublicChatAPI looking for ${hostport}`); let thisServer = this.servers.find(server => server.server === hostport); if (!thisServer) { + log.info(`LokiPublicChatAPI creating ${hostport}`); thisServer = new LokiPublicServerAPI(this, hostport); this.servers.push(thisServer); } return thisServer; } + // rename to findOrCreateChannel? registerChannel(hostport, channelId, conversationId) { const server = this.findOrCreateServer(hostport); - server.findOrCreateChannel(channelId, conversationId); + return server.findOrCreateChannel(channelId, conversationId); } unregisterChannel(hostport, channelId) { let thisServer; @@ -52,12 +54,18 @@ class LokiPublicServerAPI { this.channels = []; this.tokenPromise = null; this.baseServerUrl = url; + const ref = this; + (async function justToEnableAsyncToGetToken() { + ref.token = await ref.getOrRefreshServerToken(); + log.info(`set token ${ref.token}`); + })(); } findOrCreateChannel(channelId, conversationId) { let thisChannel = this.channels.find( channel => channel.channelId === channelId ); if (!thisChannel) { + log.info(`LokiPublicChatAPI creating channel ${conversationId}`); thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId); this.channels.push(thisChannel); } @@ -165,9 +173,7 @@ class LokiPublicChannelAPI { constructor(serverAPI, channelId, conversationId) { this.serverAPI = serverAPI; this.channelId = channelId; - this.baseChannelUrl = `${serverAPI.baseServerUrl}/channels/${ - this.channelId - }`; + this.baseChannelUrl = `channels/${this.channelId}`; this.groupName = 'unknown'; this.conversationId = conversationId; this.lastGot = 0; @@ -175,16 +181,62 @@ class LokiPublicChannelAPI { log.info(`registered LokiPublicChannel ${channelId}`); // start polling this.pollForMessages(); + this.deleteLastId = 1; this.pollForDeletions(); } getEndpoint() { - const endpoint = `${this.serverAPI.baseServerUrl}/channels/${ - this.channelId + const endpoint = `${this.serverAPI.baseServerUrl}/${ + this.baseChannelUrl }/messages`; return endpoint; } + // we'll pass token for now + async serverRequest(endpoint, params, method) { + const url = new URL(`${this.serverAPI.baseServerUrl}/${endpoint}`); + url.search = new URLSearchParams(params); + let res; + let { token } = this.serverAPI; + if (!token) { + token = await this.serverAPI.getOrRefreshServerToken(); + if (!token) { + log.error('NO TOKEN'); + return { + err: 'noToken', + }; + } + } + try { + // eslint-disable-next-line no-await-in-loop + const options = { + headers: new Headers({ + Authorization: `Bearer ${this.serverAPI.token}`, + }), + }; + if (method) { + options.method = method; + } + res = await nodeFetch(url, options || undefined); + } catch (e) { + log.info(`e ${e}`); + return { + err: e, + }; + } + // eslint-disable-next-line no-await-in-loop + const response = await res.json(); + if (response.meta.code !== 200) { + return { + err: 'statusCode', + response, + }; + } + return { + response, + }; + } + async pollForChannel(source, endpoint) { // groupName will be loaded from server const url = new URL(this.baseChannelUrl); @@ -214,54 +266,51 @@ class LokiPublicChannelAPI { }, DELETION_POLL_EVERY); }; - let numChecked = 0; - const url = new URL(`${this.baseChannelUrl}/messages`); const params = { - include_annotations: 1, - count: -200, + count: 200, }; - let beforeId = 0; - while (numChecked < 2000) { - params.before_id = beforeId; - url.search = new URLSearchParams(params); - let res; - try { - // eslint-disable-next-line no-await-in-loop - res = await nodeFetch(url); - } catch (e) { - pollAgain(); - return; - } + // full scan + let more = true; + while (more) { + params.since_id = this.deleteLastId; + const res = await this.serverRequest( + `loki/v1/channel/${this.channelId}/deletes`, + params + ); - // eslint-disable-next-line no-await-in-loop - const response = await res.json(); - if (response.meta.code !== 200) { - pollAgain(); - return; - } - numChecked += response.data.length; // eslint-disable-next-line no-loop-func - response.data.reverse().forEach(adnMessage => { - if (beforeId === 0 || adnMessage.id < beforeId) { - beforeId = adnMessage.id; - } - if (adnMessage.is_deleted) { - Whisper.events.trigger('deletePublicMessage', { - messageServerId: adnMessage.id, - conversationId: this.conversationId, - }); - } + res.response.data.reverse().forEach(deleteEntry => { + Whisper.events.trigger('deleteLocalPublicMessage', { + messageServerId: deleteEntry.message_id, + conversationId: this.conversationId, + }); }); - if (response.data.length < 200) { + if (res.response.data.length < 200) { break; } + this.deleteLastId = res.response.meta.max_id; + ({ more } = res.response); } pollAgain(); } + async deleteMessage(serverId) { + const params = {}; + const res = await this.serverRequest( + `${this.baseChannelUrl}/messages/${serverId}`, + params, + 'DELETE' + ); + if (!res.err && res.response) { + log.info(`deleted ${serverId} on ${this.baseChannelUrl}`); + return true; + } + log.warn(`failed to delete ${serverId} on ${this.baseChannelUrl}`); + return false; + } + async pollForMessages() { - const url = new URL(`${this.baseChannelUrl}/messages`); const params = { include_annotations: 1, count: -20, @@ -270,28 +319,14 @@ class LokiPublicChannelAPI { if (this.lastGot) { params.since_id = this.lastGot; } - url.search = new URLSearchParams(params); - - let res; - let success = true; - try { - res = await nodeFetch(url); - } catch (e) { - success = false; - } - - const response = await res.json(); - if (this.stopPolling) { - // Stop after latest await possible - return; - } - if (response.meta.code !== 200) { - success = false; - } + const res = await this.serverRequest( + `${this.baseChannelUrl}/messages`, + params + ); - if (success) { + if (!res.err && res.response) { let receivedAt = new Date().getTime(); - response.data.reverse().forEach(adnMessage => { + res.response.data.reverse().forEach(adnMessage => { let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; From 1b4105a3d0c1ac7cef4d643ac4bd4eaca1a86e6a Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 30 Aug 2019 14:58:40 +1000 Subject: [PATCH 57/71] Fix db stuff and remove unneeded message function --- app/sql.js | 9 ++++++--- js/models/messages.js | 6 +++--- js/modules/data.js | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/sql.js b/app/sql.js index 77bde9694..bd1e4f1e6 100644 --- a/app/sql.js +++ b/app/sql.js @@ -790,7 +790,7 @@ async function updateToLokiSchemaVersion1(currentVersion, instance) { await instance.run( `ALTER TABLE messages - ADD COLUMN serverId STRING;` + ADD COLUMN serverId INTEGER;` ); await instance.run( @@ -2060,11 +2060,14 @@ async function removeMessage(id) { ); } -async function getMessageByServerId(serverId) { +async function getMessageByServerId(serverId, conversationId) { const row = await db.get( - 'SELECT * FROM messages WHERE serverId = $serverId;', + `SELECT * FROM messages WHERE + serverId = $serverId AND + conversationId = $conversationId;`, { $serverId: serverId, + $conversationId: conversationId, } ); diff --git a/js/models/messages.js b/js/models/messages.js index b79307473..1c79f2858 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -357,9 +357,6 @@ onDestroy() { this.cleanup(); }, - deleteMessage() { - this.trigger('delete', this); - }, async cleanup() { MessageController.unregister(this.id); this.unload(); @@ -1243,6 +1240,9 @@ Message: Whisper.Message, }); }, + getServerId() { + return this.get('serverId'); + }, async setServerId(serverId) { if (_.isEqual(this.get('serverId'), serverId)) return; diff --git a/js/modules/data.js b/js/modules/data.js index 80c21bdee..c410f9464 100644 --- a/js/modules/data.js +++ b/js/modules/data.js @@ -908,8 +908,8 @@ async function _removeMessages(ids) { await channels.removeMessage(ids); } -async function getMessageByServerId(id, { Message }) { - const message = await channels.getMessageByServerId(id); +async function getMessageByServerId(serverId, conversationId, { Message }) { + const message = await channels.getMessageByServerId(serverId, conversationId); if (!message) { return null; } From e4ecc5b389944ab56bbcfdfe65e10b4cdcb9aa3d Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 30 Aug 2019 15:01:16 +1000 Subject: [PATCH 58/71] A prepare for message deletion --- _locales/en/messages.json | 4 ++++ js/background.js | 23 +++++++++++++++++++++++ js/models/conversations.js | 25 +++++++++++++++++++++++++ ts/components/conversation/Message.tsx | 2 +- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index daa221739..729cc1181 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -948,6 +948,10 @@ "delete": { "message": "Delete" }, + "deletePublicWarning": { + "message": + "Are you sure? Clicking 'delete' will permanently remove this message for everyone in this channel." + }, "deleteWarning": { "message": "Are you sure? Clicking 'delete' will permanently remove this message from this device only." diff --git a/js/background.js b/js/background.js index a791a9ccc..705b936ea 100644 --- a/js/background.js +++ b/js/background.js @@ -487,6 +487,28 @@ } }); + Whisper.events.on( + 'deleteLocalPublicMessage', + async ({ messageServerId, conversationId }) => { + const message = await window.Signal.Data.getMessageByServerId( + messageServerId, + conversationId, + { + Message: Whisper.Message, + } + ); + if (message) { + const conversation = ConversationController.get(conversationId); + if (conversation) { + conversation.removeMessage(message.id); + } + await window.Signal.Data.removeMessage(message.id, { + Message: Whisper.Message, + }); + } + } + ); + Whisper.events.on('setupAsNewDevice', () => { const { appView } = window.owsDesktopApp; if (appView) { @@ -1418,6 +1440,7 @@ let messageData = { source: data.source, sourceDevice: data.sourceDevice, + serverId: data.serverId, sent_at: data.timestamp, received_at: data.receivedAt || Date.now(), conversationId: data.source, diff --git a/js/models/conversations.js b/js/models/conversations.js index 4b674503e..e28762909 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2288,6 +2288,31 @@ }); }, + async deletePublicMessage(message) { + const serverAPI = lokiPublicChatAPI.findOrCreateServer( + this.get('server') + ); + const channelAPI = serverAPI.findOrCreateChannel( + this.get('channelId'), + this.id + ); + const success = await channelAPI.deleteMessage(message.getServerId()); + if (success) { + this.removeMessage(message.id); + } + return success; + }, + + removeMessage(messageId) { + const message = this.messageCollection.models.find( + msg => msg.id === messageId + ); + if (message) { + message.trigger('unload'); + this.messageCollection.remove(messageId); + } + }, + deleteMessages() { Whisper.events.trigger('showConfirmationDialog', { message: i18n('deleteConversationConfirmation'), diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index e9489ed96..7fdebbadc 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -257,7 +257,7 @@ export class Message extends React.PureComponent { `module-message__metadata__${badgeType}--${direction}` )} > -  • ${badgeText} +  • {badgeText} ) : null} {expirationLength && expirationTimestamp ? ( From 351fa09ad6c9eb6de46083625ebac8d7f6e85ddf Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 30 Aug 2019 15:02:17 +1000 Subject: [PATCH 59/71] Only show delete option if have mod status and show different modal for mod deletion --- js/models/conversations.js | 17 +++++++++++++++++ js/models/messages.js | 2 ++ js/views/conversation_view.js | 23 +++++++++++++++++++++++ ts/components/conversation/Message.tsx | 20 ++++++++++++-------- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index e28762909..fc6f4b85f 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2085,6 +2085,23 @@ token, }; }, + getModStatus() { + if (!this.isPublic()) { + return false; + } + return this.get('modStatus'); + }, + async setModStatus(newStatus) { + if (!this.isPublic()) { + return; + } + if (this.get('modStatus') !== newStatus) { + this.set({ modStatus: newStatus }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } + }, // SIGNAL PROFILES diff --git a/js/models/messages.js b/js/models/messages.js index 1c79f2858..cc82e1a0c 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -672,6 +672,8 @@ isP2p: !!this.get('isP2p'), isPublic: !!this.get('isPublic'), isRss: !!this.get('isRss'), + isDeletable: + !this.get('isPublic') || this.getConversation().getModStatus(), onCopyText: () => this.copyText(), onReply: () => this.trigger('reply', this), diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index edea0036d..055a829a6 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -1291,6 +1291,29 @@ }, deleteMessage(message) { + if (this.model.isPublic()) { + const dialog = new Whisper.ConfirmationDialogView({ + message: i18n('deletePublicWarning'), + okText: i18n('delete'), + resolve: async () => { + const success = await this.model.deletePublicMessage(message); + if (!success) { + // Message failed to delete from server, show error? + return; + } + await window.Signal.Data.removeMessage(message.id, { + Message: Whisper.Message, + }); + message.trigger('unload'); + this.resetPanel(); + this.updateHeader(); + }, + }); + + this.$el.prepend(dialog.el); + dialog.focusCancel(); + return; + } const dialog = new Whisper.ConfirmationDialogView({ message: i18n('deleteWarning'), okText: i18n('delete'), diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 7fdebbadc..dba0d52de 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -48,6 +48,7 @@ interface LinkPreviewType { export interface Props { disableMenu?: boolean; + isDeletable: boolean; text?: string; textPending?: boolean; id?: string; @@ -819,6 +820,7 @@ export class Message extends React.PureComponent { onCopyText, direction, status, + isDeletable, onDelete, onDownload, onReply, @@ -876,14 +878,16 @@ export class Message extends React.PureComponent { {i18n('retrySend')} ) : null} - - {i18n('deleteMessage')} - + {isDeletable ? ( + + {i18n('deleteMessage')} + + ) : null} ); } From 8d77d6fd79d74cd8e02b27e9c9425c1015037f7e Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 30 Aug 2019 15:07:45 +1000 Subject: [PATCH 60/71] All the API updates, enable mod status retrieval and allow for message deletion --- js/background.js | 3 +- js/modules/loki_public_chat_api.js | 222 +++++++++++++++++++++-------- 2 files changed, 163 insertions(+), 62 deletions(-) diff --git a/js/background.js b/js/background.js index 705b936ea..799d8f8f3 100644 --- a/js/background.js +++ b/js/background.js @@ -224,11 +224,12 @@ ); publicConversations.forEach(conversation => { const settings = conversation.getPublicSource(); - window.lokiPublicChatAPI.registerChannel( + const channel = window.lokiPublicChatAPI.findOrCreateChannel( settings.server, settings.channelId, conversation.id ); + channel.refreshModStatus(); }); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 89d0fea7c..32bec06d1 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -1,43 +1,46 @@ -/* global log, textsecure, libloki, Signal */ +/* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); -const GROUPCHAT_POLL_EVERY = 1000; // 1 second +// Can't be less than 1200 if we have unauth'd requests +const GROUPCHAT_POLL_EVERY = 1500; // 1.5s +const DELETION_POLL_EVERY = 5000; // 1 second // singleton to relay events to libtextsecure/message_receiver class LokiPublicChatAPI extends EventEmitter { constructor(ourKey) { super(); this.ourKey = ourKey; - this.lastGot = {}; this.servers = []; } - findOrCreateServer(hostport) { - log.info(`LokiPublicChatAPI looking for ${hostport}`); - let thisServer = this.servers.find(server => server.server === hostport); + findOrCreateServer(serverUrl) { + let thisServer = this.servers.find( + server => server.baseServerUrl === serverUrl + ); if (!thisServer) { - thisServer = new LokiPublicServerAPI(this, hostport); + log.info(`LokiPublicChatAPI creating ${serverUrl}`); + thisServer = new LokiPublicServerAPI(this, serverUrl); this.servers.push(thisServer); } return thisServer; } - registerChannel(hostport, channelId, conversationId) { - const server = this.findOrCreateServer(hostport); - server.findOrCreateChannel(channelId, conversationId); + findOrCreateChannel(serverUrl, channelId, conversationId) { + const server = this.findOrCreateServer(serverUrl); + return server.findOrCreateChannel(channelId, conversationId); } - unregisterChannel(hostport, channelId) { + unregisterChannel(serverUrl, channelId) { let thisServer; let i = 0; for (; i < this.servers.length; i += 1) { - if (this.servers[i].server === hostport) { + if (this.servers[i].server === serverUrl) { thisServer = this.servers[i]; break; } } if (!thisServer) { - log.warn(`Tried to unregister from nonexistent server ${hostport}`); + log.warn(`Tried to unregister from nonexistent server ${serverUrl}`); return; } thisServer.unregisterChannel(channelId); @@ -57,6 +60,7 @@ class LokiPublicServerAPI { channel => channel.channelId === channelId ); if (!thisChannel) { + log.info(`LokiPublicChatAPI creating channel ${conversationId}`); thisChannel = new LokiPublicChannelAPI(this, channelId, conversationId); this.channels.push(thisChannel); } @@ -79,6 +83,9 @@ class LokiPublicServerAPI { } async getOrRefreshServerToken() { + if (this.token) { + return this.token; + } let token = await Signal.Data.getPublicServerTokenByServerUrl( this.baseServerUrl ); @@ -91,6 +98,7 @@ class LokiPublicServerAPI { }); } } + this.token = token; return token; } @@ -164,9 +172,7 @@ class LokiPublicChannelAPI { constructor(serverAPI, channelId, conversationId) { this.serverAPI = serverAPI; this.channelId = channelId; - this.baseChannelUrl = `${serverAPI.baseServerUrl}/channels/${ - this.channelId - }`; + this.baseChannelUrl = `channels/${this.channelId}`; this.groupName = 'unknown'; this.conversationId = conversationId; this.lastGot = 0; @@ -174,85 +180,169 @@ class LokiPublicChannelAPI { log.info(`registered LokiPublicChannel ${channelId}`); // start polling this.pollForMessages(); + this.deleteLastId = 1; + this.pollForDeletions(); + } + + async refreshModStatus() { + const url = new URL(`${this.serverAPI.baseServerUrl}/loki/v1/user_info`); + const token = await this.serverAPI.getOrRefreshServerToken(); + let modStatus = false; + try { + const result = await nodeFetch(url, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + if (result.ok) { + const response = await result.json(); + if (response.data.moderator_status) { + modStatus = response.data.moderator_status; + } + } + } catch (e) { + modStatus = false; + } + const conversation = ConversationController.get(this.conversationId); + await conversation.setModStatus(modStatus); + } + + async deleteMessage(messageServerId) { + // TODO: Allow deletion of your own messages without moderator status + const url = new URL( + `${ + this.serverAPI.baseServerUrl + }/loki/v1/moderation/message/${messageServerId}` + ); + const token = await this.serverAPI.getOrRefreshServerToken(); + try { + const result = await nodeFetch(url, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + // 200 for successful delete + // 404 for trying to delete a message that doesn't exist + // 410 for successful moderator delete + const validResults = [404, 410]; + if (result.ok || validResults.includes(result.status)) { + return true; + } + } catch (e) { + log.warn( + `Failed to delete message from public server with ID ${messageServerId}` + ); + } + return false; } getEndpoint() { - const endpoint = `${this.serverAPI.baseServerUrl}/channels/${ - this.channelId + const endpoint = `${this.serverAPI.baseServerUrl}/${ + this.baseChannelUrl }/messages`; return endpoint; } - async pollForChannel(source, endpoint) { - // groupName will be loaded from server - const url = new URL(this.baseChannelUrl); + async serverRequest(endpoint, params, method) { + const url = new URL(`${this.serverAPI.baseServerUrl}/${endpoint}`); + url.search = new URLSearchParams(params); let res; - let success = true; + const token = await this.serverAPI.getOrRefreshServerToken(); + if (!token) { + log.error('NO TOKEN'); + return { + err: 'noToken', + }; + } try { - res = await nodeFetch(url); + // eslint-disable-next-line no-await-in-loop + const options = { + headers: new Headers({ + Authorization: `Bearer ${token}`, + }), + }; + if (method) { + options.method = method; + } + res = await nodeFetch(url, options || undefined); } catch (e) { - success = false; + log.info(`e ${e}`); + return { + err: e, + }; } - + // eslint-disable-next-line no-await-in-loop const response = await res.json(); if (response.meta.code !== 200) { - success = false; + return { + err: 'statusCode', + response, + }; } - // update this.groupId - return endpoint || success; + return { + response, + }; } async pollForDeletions() { // read all messages from 0 to current // delete local copies if server state has changed to delete // run every minute - const url = new URL(this.baseChannelUrl); - let res; - let success = true; - try { - res = await nodeFetch(url); - } catch (e) { - success = false; - } + const pollAgain = () => { + setTimeout(() => { + this.pollForDeletions(); + }, DELETION_POLL_EVERY); + }; - const response = await res.json(); - if (response.meta.code !== 200) { - success = false; + const params = { + count: 200, + }; + + // full scan + let more = true; + while (more) { + params.since_id = this.deleteLastId; + const res = await this.serverRequest( + `loki/v1/channel/${this.channelId}/deletes`, + params + ); + + // eslint-disable-next-line no-loop-func + res.response.data.reverse().forEach(deleteEntry => { + Whisper.events.trigger('deleteLocalPublicMessage', { + messageServerId: deleteEntry.message_id, + conversationId: this.conversationId, + }); + }); + if (res.response.data.length < 200) { + break; + } + this.deleteLastId = res.response.meta.max_id; + ({ more } = res.response); } - return success; + pollAgain(); } async pollForMessages() { - const url = new URL(`${this.baseChannelUrl}/messages`); const params = { include_annotations: 1, count: -20, + include_deleted: false, }; if (this.lastGot) { params.since_id = this.lastGot; } - url.search = new URLSearchParams(params); - - let res; - let success = true; - try { - res = await nodeFetch(url); - } catch (e) { - success = false; - } - - const response = await res.json(); - if (this.stopPolling) { - // Stop after latest await possible - return; - } - if (response.meta.code !== 200) { - success = false; - } + const res = await this.serverRequest( + `${this.baseChannelUrl}/messages`, + params + ); - if (success) { + if (!res.err && res.response) { let receivedAt = new Date().getTime(); - response.data.reverse().forEach(adnMessage => { + res.response.data.reverse().forEach(adnMessage => { let timestamp = new Date(adnMessage.created_at).getTime(); let from = adnMessage.user.username; let source; @@ -264,6 +354,16 @@ class LokiPublicChannelAPI { ({ from, timestamp, source } = noteValue); } + if ( + !from || + !timestamp || + !source || + !adnMessage.id || + !adnMessage.text + ) { + return; // Invalid message + } + const messageData = { serverId: adnMessage.id, friendRequest: false, From ba72f55b03cbdfef962b4c6436fcb687afff0bce Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Fri, 30 Aug 2019 11:56:59 +1000 Subject: [PATCH 61/71] Fix missing react key error --- ts/components/conversation/Linkify.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 0f27953d7..6562b6ed8 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -25,10 +25,12 @@ export class Linkify extends React.Component { public render() { const { text, renderNonLink, isRss } = this.props; const results: Array = []; + let count = 1; if (isRss && text.indexOf(' { const matchData = linkify.match(text) || []; let last = 0; - let count = 1; // We have to do this, because renderNonLink is not required in our Props object, // but it is always provided via defaultProps. From 3178d672953d2b35915ec03b7894968da34ef237 Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Fri, 30 Aug 2019 01:38:45 -0700 Subject: [PATCH 62/71] allow yourself to request deletion of messages --- js/models/messages.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/models/messages.js b/js/models/messages.js index cc82e1a0c..3102ee915 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -673,7 +673,9 @@ isPublic: !!this.get('isPublic'), isRss: !!this.get('isRss'), isDeletable: - !this.get('isPublic') || this.getConversation().getModStatus(), + !this.get('isPublic') || + this.getConversation().getModStatus() || + this.getSource() === this.OUR_NUMBER, onCopyText: () => this.copyText(), onReply: () => this.trigger('reply', this), From 133f2a565664bd24bc76f61931fe85dd521b572c Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Fri, 30 Aug 2019 01:39:34 -0700 Subject: [PATCH 63/71] replace publicSendData with LokiPublicChannelAPI instance --- js/models/conversations.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/js/models/conversations.js b/js/models/conversations.js index fc6f4b85f..9774ef97c 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1376,7 +1376,7 @@ const options = this.getSendOptions(); options.messageType = message.get('type'); options.isPublic = this.isPublic(); - if (this.isPublic()) { + if (options.isPublic) { options.publicSendData = await this.getPublicSendData(); } @@ -2073,17 +2073,11 @@ const serverAPI = lokiPublicChatAPI.findOrCreateServer( this.get('server') ); - // Can be null if fails - const token = await serverAPI.getOrRefreshServerToken(); const channelAPI = serverAPI.findOrCreateChannel( this.get('channelId'), this.id ); - const publicEndpoint = channelAPI.getEndpoint(); - return { - publicEndpoint, - token, - }; + return channelAPI; }, getModStatus() { if (!this.isPublic()) { From dfd6039c8384407c88bfa938d78e846077e694bd Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Fri, 30 Aug 2019 01:42:05 -0700 Subject: [PATCH 64/71] use LokiPublicChannelAPI --- js/modules/loki_message_api.js | 53 ++++++---------------------------- 1 file changed, 9 insertions(+), 44 deletions(-) diff --git a/js/modules/loki_message_api.js b/js/modules/loki_message_api.js index 556069bb0..227541929 100644 --- a/js/modules/loki_message_api.js +++ b/js/modules/loki_message_api.js @@ -4,7 +4,6 @@ const _ = require('lodash'); const { rpc } = require('./loki_rpc'); -const nodeFetch = require('node-fetch'); const DEFAULT_CONNECTIONS = 3; const MAX_ACCEPTABLE_FAILURES = 1; @@ -89,57 +88,23 @@ class LokiMessageAPI { }; if (isPublic) { - const { token, publicEndpoint } = publicSendData; - if (!token) { - throw new window.textsecure.PublicChatError( - `Failed to retrieve valid token for ${publicEndpoint}` - ); - } - const { profile } = data; let displayName = 'Anonymous'; if (profile && profile.displayName) { ({ displayName } = profile); } - const payload = { - text: data.body, - annotations: [ - { - type: 'network.loki.messenger.publicChat', - value: { - timestamp: messageTimeStamp, - from: displayName, - source: this.ourKey, - }, - }, - ], - }; - let result; - try { - result = await nodeFetch(publicEndpoint, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(payload), - }); - } catch (e) { - throw new window.textsecure.PublicChatError( - `Failed to send public chat message: ${e}` - ); - } - const body = await result.json(); - if (!result.ok) { - if (result.status === 401) { - // TODO: Handle token timeout - } - const error = body.meta.error_message; + const res = await publicSendData.sendMessage( + data.body, + messageTimeStamp, + displayName, + this.ourKey + ); + if (res === false) { throw new window.textsecure.PublicChatError( - `Failed to send public chat message: ${error}` + 'Failed to send public chat message' ); } - messageEventData.serverId = body.data.id; + messageEventData.serverId = res; window.Whisper.events.trigger('publicMessageSent', messageEventData); return; } From 56a4a31ca2a80f9600f52ee8e82cfeb6df72fd2d Mon Sep 17 00:00:00 2001 From: Ryan Tharp Date: Fri, 30 Aug 2019 01:48:01 -0700 Subject: [PATCH 65/71] sendMessage(), getOrRefreshServerToken supports forceRefresh, make modStatus a property, additional comments --- js/modules/loki_public_chat_api.js | 139 ++++++++++++++++++++++------- 1 file changed, 108 insertions(+), 31 deletions(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 6bb819430..eb4fe6c7f 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -14,6 +14,8 @@ class LokiPublicChatAPI extends EventEmitter { this.ourKey = ourKey; this.servers = []; } + + // server getter/factory findOrCreateServer(serverUrl) { let thisServer = this.servers.find( server => server.baseServerUrl === serverUrl @@ -25,10 +27,14 @@ class LokiPublicChatAPI extends EventEmitter { } return thisServer; } + + // channel getter/factory findOrCreateChannel(serverUrl, channelId, conversationId) { const server = this.findOrCreateServer(serverUrl); return server.findOrCreateChannel(channelId, conversationId); } + + // deallocate resources server uses unregisterChannel(serverUrl, channelId) { let thisServer; let i = 0; @@ -60,6 +66,8 @@ class LokiPublicServerAPI { log.info(`set token ${ref.token}`); })(); } + + // channel getter/factory findOrCreateChannel(channelId, conversationId) { let thisChannel = this.channels.find( channel => channel.channelId === channelId @@ -71,6 +79,8 @@ class LokiPublicServerAPI { } return thisChannel; } + + // deallocate resources channel uses unregisterChannel(channelId) { let thisChannel; let i = 0; @@ -87,13 +97,17 @@ class LokiPublicServerAPI { thisChannel.stopPolling = true; } - async getOrRefreshServerToken() { - if (this.token) { - return this.token; + // get active token for this server + async getOrRefreshServerToken(forceRefresh = false) { + let token; + if (!forceRefresh) { + if (this.token) { + return this.token; + } + token = await Signal.Data.getPublicServerTokenByServerUrl( + this.baseServerUrl + ); } - let token = await Signal.Data.getPublicServerTokenByServerUrl( - this.baseServerUrl - ); if (!token) { token = await this.refreshServerToken(); if (token) { @@ -107,27 +121,36 @@ class LokiPublicServerAPI { return token; } + // get active token from server (but only allow one request at a time) async refreshServerToken() { + // if currently not in progress if (this.tokenPromise === null) { + // set lock this.tokenPromise = new Promise(async res => { + // request the oken const token = await this.requestToken(); if (!token) { res(null); return; } + // activate the token const registered = await this.submitToken(token); if (!registered) { res(null); return; } + // resolve promise to release lock res(token); }); } + // wait until we have it set const token = await this.tokenPromise; + // clear lock this.tokenPromise = null; return token; } + // request an token from the server async requestToken() { const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`); const params = { @@ -149,6 +172,7 @@ class LokiPublicServerAPI { return token; } + // activate token async submitToken(token) { const options = { method: 'POST', @@ -193,12 +217,14 @@ class LokiPublicChannelAPI { this.pollForDeletions(); } + // make a request to the server async serverRequest(endpoint, options = {}) { + const { params = {}, method, objBody, forceFreshToken = false } = options; const url = new URL(`${this.serverAPI.baseServerUrl}/${endpoint}`); - if (options.params) { + if (params) { url.search = new URLSearchParams(params); } - let res; + let result; let { token } = this.serverAPI; if (!token) { token = await this.serverAPI.getOrRefreshServerToken(); @@ -210,38 +236,61 @@ class LokiPublicChannelAPI { } } try { - const fetchOptions = { - headers: new Headers({ - Authorization: `Bearer ${this.serverAPI.token}`, - }), + const fetchOptions = {}; + const headers = { + Authorization: `Bearer ${this.serverAPI.token}`, }; - if (options.method) { - fetchOptions.method = options.method; + if (method) { + fetchOptions.method = method; } - res = await nodeFetch(url, fetchOptions || undefined); + if (objBody) { + headers['Content-Type'] = 'application/json'; + fetchOptions.body = JSON.stringify(objBody); + } + fetchOptions.headers = new Headers(headers); + result = await nodeFetch(url, fetchOptions || undefined); } catch (e) { log.info(`e ${e}`); return { err: e, }; } - const response = await res.json(); + let response = null; + try { + response = await result.json(); + } catch (e) { + log.info(`serverRequest json arpse ${e}`); + return { + err: e, + statusCode: result.status, + }; + } // if it's a response style with a meta - if (res.status !== 200) { + if (result.status !== 200) { + if (!forceFreshToken && response.meta.code === 401) { + // copy options because lint complains if we modify this directly + const updatedOptions = options; + // force it this time + updatedOptions.forceFreshToken = true; + // retry with updated options + return this.serverRequest(endpoint, updatedOptions); + } return { err: 'statusCode', + statusCode: result.status, response, }; } return { - statusCode: res.status, + statusCode: result.status, response, }; } + // get moderator status async refreshModStatus() { - const res = serverRequest('/loki/v1/user_info'); + const res = this.serverRequest('loki/v1/user_info'); // if no problems and we have data if (!res.err && res.response && res.response.data) { this.modStatus = res.response.data.moderator_status; @@ -251,9 +300,12 @@ class LokiPublicChannelAPI { await conversation.setModStatus(this.modStatus); } + // delete a message on the server async deleteMessage(serverId) { const res = await this.serverRequest( - this.modStatus?`loki/v1/moderation/message/${messageServerId}`:`${this.baseChannelUrl}/messages/${serverId}`, + this.modStatus + ? `loki/v1/moderation/message/${serverId}` + : `${this.baseChannelUrl}/messages/${serverId}`, { method: 'DELETE' } ); if (!res.err && res.response) { @@ -264,6 +316,7 @@ class LokiPublicChannelAPI { return false; } + // used for sending messages getEndpoint() { const endpoint = `${this.serverAPI.baseServerUrl}/${ this.baseChannelUrl @@ -272,7 +325,7 @@ class LokiPublicChannelAPI { } // update room details - async pollForChannel(source, endpoint) { + async pollForChannel() { // groupName will be loaded from server const url = new URL(this.baseChannelUrl); let res; @@ -290,9 +343,6 @@ class LokiPublicChannelAPI { Authorization: `Bearer ${token}`, }), }; - if (method) { - options.method = method; - } res = await nodeFetch(url, options || undefined); } catch (e) { log.info(`e ${e}`); @@ -322,20 +372,21 @@ class LokiPublicChannelAPI { // start loop let more = true; - // eslint-disable-next-line no-await-in-loop 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 } ); - // Process rresult + // Process results res.response.data.reverse().forEach(deleteEntry => { - // Escalate it up to the subsystem that can check to see if this has been processsed + // Escalate it up to the subsystem that can check to see if this has + // been processed Whisper.events.trigger('deleteLocalPublicMessage', { messageServerId: deleteEntry.message_id, conversationId: this.conversationId, @@ -368,10 +419,9 @@ class LokiPublicChannelAPI { if (this.lastGot) { params.since_id = this.lastGot; } - const res = await this.serverRequest( - `${this.baseChannelUrl}/messages`, - { params } - ); + const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, { + params, + }); if (!res.err && res.response) { let receivedAt = new Date().getTime(); @@ -442,6 +492,33 @@ class LokiPublicChannelAPI { this.pollForMessages(); }, GROUPCHAT_POLL_EVERY); } + + // create a message in the channel + async sendMessage(text, messageTimeStamp, displayName, pubKey) { + const payload = { + text, + annotations: [ + { + type: 'network.loki.messenger.publicChat', + value: { + timestamp: messageTimeStamp, + // will deprecated + from: displayName, + // will deprecated + source: pubKey, + }, + }, + ], + }; + const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, { + method: 'POST', + objBody: payload, + }); + if (!res.err && res.response) { + return res.response.data.id; + } + return false; + } } module.exports = LokiPublicChatAPI; From 616e952f8ef678eb23cac44bc29e46875cb7a1d6 Mon Sep 17 00:00:00 2001 From: Beaudan Date: Thu, 8 Aug 2019 15:09:54 +1000 Subject: [PATCH 66/71] Add right click option to public chat messages to copy pubkey of sender --- js/models/messages.js | 12 ++++++++++++ ts/components/conversation/Message.tsx | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/js/models/messages.js b/js/models/messages.js index 3102ee915..d9027cf9c 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -678,6 +678,7 @@ this.getSource() === this.OUR_NUMBER, onCopyText: () => this.copyText(), + onCopyPubKey: () => this.copyPubKey(), onReply: () => this.trigger('reply', this), onRetrySend: () => this.retrySend(), onShowDetail: () => this.trigger('show-message-detail', this), @@ -964,6 +965,17 @@ }; }, + copyPubKey() { + if (this.isIncoming()) { + clipboard.writeText(this.get('source')); + } else { + clipboard.writeText(this.OUR_NUMBER); + } + window.Whisper.events.trigger('showToast', { + message: i18n('copiedPublicKey'), + }); + }, + copyText() { clipboard.writeText(this.get('body')); window.Whisper.events.trigger('showToast', { diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index dba0d52de..8ed5fa4f3 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -97,6 +97,7 @@ export interface Props { onRetrySend?: () => void; onDownload?: (isDangerous: boolean) => void; onDelete?: () => void; + onCopyPubKey?: () => void; onShowDetail: () => void; } @@ -826,6 +827,8 @@ export class Message extends React.PureComponent { onReply, onRetrySend, onShowDetail, + onCopyPubKey, + isPublic, i18n, } = this.props; @@ -888,6 +891,9 @@ export class Message extends React.PureComponent { {i18n('deleteMessage')} ) : null} + {isPublic ? ( + {i18n('copyPublicKey')} + ) : null} ); } From 8066affd2c564eac2273c686b6e0937317326871 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 2 Sep 2019 11:24:00 +1000 Subject: [PATCH 67/71] Move initialisation of public and rss conversations to fix early loading error. Rename function to be more readable --- js/background.js | 22 ---------------------- js/conversation_controller.js | 11 +++++++++++ js/models/conversations.js | 2 +- js/models/messages.js | 2 +- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/js/background.js b/js/background.js index 799d8f8f3..8950b48e1 100644 --- a/js/background.js +++ b/js/background.js @@ -206,31 +206,9 @@ const initAPIs = async () => { const ourKey = textsecure.storage.user.getNumber(); - const rssFeedConversations = await window.Signal.Data.getAllRssFeedConversations( - { - ConversationCollection: Whisper.ConversationCollection, - } - ); window.feeds = []; - rssFeedConversations.forEach(conversation => { - window.feeds.push(new window.LokiRssAPI(conversation.getRssSettings())); - }); window.lokiMessageAPI = new window.LokiMessageAPI(ourKey); window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey); - const publicConversations = await window.Signal.Data.getAllPublicConversations( - { - ConversationCollection: Whisper.ConversationCollection, - } - ); - publicConversations.forEach(conversation => { - const settings = conversation.getPublicSource(); - const channel = window.lokiPublicChatAPI.findOrCreateChannel( - settings.server, - settings.channelId, - conversation.id - ); - channel.refreshModStatus(); - }); window.lokiP2pAPI = new window.LokiP2pAPI(ourKey); window.lokiP2pAPI.on('pingContact', pubKey => { const isPing = true; diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 50b3aae33..3383e6e24 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -247,6 +247,17 @@ conversation.resetPendingSend(), conversation.setFriendRequestExpiryTimeout(), ]); + if (conversation.isPublic()) { + const settings = conversation.getPublicSource(); + const channel = window.lokiPublicChatAPI.findOrCreateChannel( + settings.server, + settings.channelId, + conversation.id + ); + promises.push(channel.refreshModStatus()); + } else if (conversation.isRss()) { + window.feeds.push(new window.LokiRssAPI(conversation.getRssSettings())); + } }); await Promise.all(promises); diff --git a/js/models/conversations.js b/js/models/conversations.js index 9774ef97c..5987daed2 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2079,7 +2079,7 @@ ); return channelAPI; }, - getModStatus() { + isModerator() { if (!this.isPublic()) { return false; } diff --git a/js/models/messages.js b/js/models/messages.js index d9027cf9c..e584b95a6 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -674,7 +674,7 @@ isRss: !!this.get('isRss'), isDeletable: !this.get('isPublic') || - this.getConversation().getModStatus() || + this.getConversation().isModerator() || this.getSource() === this.OUR_NUMBER, onCopyText: () => this.copyText(), From ef8b515d70b426d28f7065c13b53b8f20c2f1845 Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 2 Sep 2019 11:48:25 +1000 Subject: [PATCH 68/71] Make sure we only initialise special conversations after both registration and storage load have complete --- js/background.js | 36 +++++++++++++++++++++++++++-------- js/conversation_controller.js | 11 ----------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/js/background.js b/js/background.js index 8950b48e1..9b27d064c 100644 --- a/js/background.js +++ b/js/background.js @@ -204,6 +204,31 @@ window.log.info('Storage fetch'); storage.fetch(); + const initSpecialConversations = async () => { + const rssFeedConversations = await window.Signal.Data.getAllRssFeedConversations( + { + ConversationCollection: Whisper.ConversationCollection, + } + ); + rssFeedConversations.forEach(conversation => { + window.feeds.push(new window.LokiRssAPI(conversation.getRssSettings())); + }); + const publicConversations = await window.Signal.Data.getAllPublicConversations( + { + ConversationCollection: Whisper.ConversationCollection, + } + ); + publicConversations.forEach(conversation => { + const settings = conversation.getPublicSource(); + const channel = window.lokiPublicChatAPI.findOrCreateChannel( + settings.server, + settings.channelId, + conversation.id + ); + channel.refreshModStatus(); + }); + } + const initAPIs = async () => { const ourKey = textsecure.storage.user.getNumber(); window.feeds = []; @@ -246,11 +271,6 @@ } first = false; - if (Whisper.Registration.isDone()) { - startLocalLokiServer(); - await initAPIs(); - } - const currentPoWDifficulty = storage.get('PoWDifficulty', null); if (!currentPoWDifficulty) { storage.put('PoWDifficulty', window.getDefaultPoWDifficulty()); @@ -583,8 +603,6 @@ Whisper.events.on('registration_done', async () => { window.log.info('handling registration event'); - startLocalLokiServer(); - // listeners Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); // window.Signal.RefreshSenderCertificate.initialize({ @@ -594,7 +612,6 @@ // logger: window.log, // }); - await initAPIs(); connect(true); }); @@ -868,6 +885,9 @@ Whisper.Notifications.disable(); // avoid notification flood until empty // initialize the socket and start listening for messages + startLocalLokiServer(); + await initAPIs(); + await initSpecialConversations(); messageReceiver = new textsecure.MessageReceiver( USERNAME, PASSWORD, diff --git a/js/conversation_controller.js b/js/conversation_controller.js index 3383e6e24..50b3aae33 100644 --- a/js/conversation_controller.js +++ b/js/conversation_controller.js @@ -247,17 +247,6 @@ conversation.resetPendingSend(), conversation.setFriendRequestExpiryTimeout(), ]); - if (conversation.isPublic()) { - const settings = conversation.getPublicSource(); - const channel = window.lokiPublicChatAPI.findOrCreateChannel( - settings.server, - settings.channelId, - conversation.id - ); - promises.push(channel.refreshModStatus()); - } else if (conversation.isRss()) { - window.feeds.push(new window.LokiRssAPI(conversation.getRssSettings())); - } }); await Promise.all(promises); From 27976a053ca9c078cf60bdbcbba91e47c2c5a9ab Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 2 Sep 2019 12:27:33 +1000 Subject: [PATCH 69/71] Save the last retrieved message for public chats on the conversation --- js/background.js | 2 +- js/models/conversations.js | 18 ++++++++++++++++++ js/modules/loki_public_chat_api.js | 12 +++++++++--- js/modules/loki_rss_api.js | 1 - js/modules/loki_snode_api.js | 1 + 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/js/background.js b/js/background.js index 9b27d064c..85f53af2f 100644 --- a/js/background.js +++ b/js/background.js @@ -227,7 +227,7 @@ ); channel.refreshModStatus(); }); - } + }; const initAPIs = async () => { const ourKey = textsecure.storage.user.getNumber(); diff --git a/js/models/conversations.js b/js/models/conversations.js index 5987daed2..0829a5a8e 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -2079,6 +2079,24 @@ ); return channelAPI; }, + getLastRetrievedMessage() { + if (!this.isPublic()) { + return null; + } + const lastMessageId = this.get('lastPublicMessage') || 0; + return lastMessageId; + }, + async setLastRetrievedMessage(newLastMessageId) { + if (!this.isPublic()) { + return; + } + if (this.get('lastPublicMessage') !== newLastMessageId) { + this.set({ lastPublicMessage: newLastMessageId }); + await window.Signal.Data.updateConversation(this.id, this.attributes, { + Conversation: Whisper.Conversation, + }); + } + }, isModerator() { if (!this.isPublic()) { return false; diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index eb4fe6c7f..59214f65e 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -205,7 +205,7 @@ class LokiPublicChannelAPI { this.baseChannelUrl = `channels/${this.channelId}`; this.groupName = 'unknown'; this.conversationId = conversationId; - this.lastGot = 0; + this.lastGot = null; this.stopPolling = false; this.modStatus = false; this.deleteLastId = 1; @@ -416,9 +416,14 @@ class LokiPublicChannelAPI { count: -20, include_deleted: false, }; - if (this.lastGot) { - params.since_id = this.lastGot; + const conversation = ConversationController.get(this.conversationId); + if (!conversation) { + log.warn('Trying to poll for non-existing public conversation'); + this.lastGot = 0; + } else if (!this.lastGot) { + this.lastGot = conversation.getLastRetrievedMessage(); } + params.since_id = this.lastGot; const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, { params, }); @@ -485,6 +490,7 @@ class LokiPublicChannelAPI { this.lastGot = !this.lastGot ? adnMessage.id : Math.max(this.lastGot, adnMessage.id); + conversation.setLastRetrievedMessage(this.lastGot); }); } diff --git a/js/modules/loki_rss_api.js b/js/modules/loki_rss_api.js index bf4dedebb..ee5486d53 100644 --- a/js/modules/loki_rss_api.js +++ b/js/modules/loki_rss_api.js @@ -49,7 +49,6 @@ class LokiRssAPI extends EventEmitter { this.closeable = settings.closeable; // non configureable options this.feedTimer = null; - this.conversationSetup = false; // initial set up this.getFeed(); } diff --git a/js/modules/loki_snode_api.js b/js/modules/loki_snode_api.js index 49e00c773..d46b2755d 100644 --- a/js/modules/loki_snode_api.js +++ b/js/modules/loki_snode_api.js @@ -89,6 +89,7 @@ class LokiSnodeAPI { async initialiseRandomPool(seedNodes = [...window.seedNodeList]) { const params = { limit: 20, + active_only: true, fields: { public_ip: true, storage_port: true, From 38e81fd95ab7fbbfd06e29f38e0021ba56b0d28e Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 2 Sep 2019 13:32:40 +1000 Subject: [PATCH 70/71] Lower default PoW and add export to build-release yarn call --- config/default.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/default.json b/config/default.json index c6ead5686..8740d6b93 100644 --- a/config/default.json +++ b/config/default.json @@ -4,7 +4,7 @@ "cdnUrl": "random.snode", "contentProxyUrl": "", "localServerPort": "8081", - "defaultPoWDifficulty": "100", + "defaultPoWDifficulty": "1", "seedNodeList": [ { "ip": "storage.testnetseed1.loki.network", diff --git a/package.json b/package.json index 306574796..28161da5a 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build", "generate": "yarn icon-gen && yarn grunt", "build": "build --config.extraMetadata.environment=$SIGNAL_ENV", - "build-release": "SIGNAL_ENV=production && npm run build -- --config.directories.output=release", + "build-release": "export SIGNAL_ENV=production && npm run build -- --config.directories.output=release", "sign-release": "node ts/updater/generateSignature.js", "build-module-protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js", "clean-module-protobuf": "rm -f ts/protobuf/compiled.d.ts ts/protobuf/compiled.js", From cccf533abc30ff6318ea244b6a4bfd1fe1d1372e Mon Sep 17 00:00:00 2001 From: Beaudan Brown Date: Mon, 2 Sep 2019 13:58:45 +1000 Subject: [PATCH 71/71] Save conversation after all messages are processed --- js/modules/loki_public_chat_api.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/modules/loki_public_chat_api.js b/js/modules/loki_public_chat_api.js index 59214f65e..7a959c316 100644 --- a/js/modules/loki_public_chat_api.js +++ b/js/modules/loki_public_chat_api.js @@ -490,8 +490,8 @@ class LokiPublicChannelAPI { this.lastGot = !this.lastGot ? adnMessage.id : Math.max(this.lastGot, adnMessage.id); - conversation.setLastRetrievedMessage(this.lastGot); }); + conversation.setLastRetrievedMessage(this.lastGot); } setTimeout(() => {