diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 12155cc8a..e2a65a4d8 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -1,5 +1,5 @@ /* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController, -clearTimeout, MessageController, libsignal, StringView, window, _ */ +clearTimeout, MessageController, libsignal, StringView, window, _, lokiFileServerAPI */ const EventEmitter = require('events'); const nodeFetch = require('node-fetch'); const { URL, URLSearchParams } = require('url'); @@ -383,8 +383,7 @@ class LokiAppDotNetServerAPI { if (pubKeys.length > 200) { log.warn('Too many pubKeys given to getUsersAnnotations!'); } - console.log('getUsersAnnotations', pubKeys) - const res = await this.serverRequest(`users`, { + const res = await this.serverRequest('users', { method: 'GET', params: { ids: pubKeys.join(','), @@ -445,6 +444,10 @@ class LokiPublicChannelAPI { // Cache for duplicate checking this.lastMessagesCache = []; + // Multidevice states + this.slavePrimaryMap = {}; + this.primaryUserProfileName = {}; + // end properties log.info(`registered LokiPublicChannel ${channelId}`); @@ -750,158 +753,290 @@ class LokiPublicChannelAPI { if (!res.err && res.response) { let receivedAt = new Date().getTime(); - const pubKeys = [] - const pendingMessages = [] - res.response.data.reverse().forEach(async adnMessage => { - // still update our last received if deleted, not signed or not valid - this.lastGot = !this.lastGot - ? adnMessage.id - : Math.max(this.lastGot, adnMessage.id); - - if ( - !adnMessage.id || - !adnMessage.user || - !adnMessage.user.username || // pubKey lives in the username field - !adnMessage.user.name || // profileName lives in the name field - !adnMessage.text || - adnMessage.is_deleted - ) { - return; // Invalid or delete message - } - - const messengerData = await this.getMessengerData(adnMessage); - if (messengerData === false) { - return; - } + const pubKeys = []; + let pendingMessages = []; + pendingMessages = await Promise.all( + res.response.data.reverse().map(async adnMessage => { + // still update our last received if deleted, not signed or not valid + this.lastGot = !this.lastGot + ? adnMessage.id + : Math.max(this.lastGot, adnMessage.id); + + if ( + !adnMessage.id || + !adnMessage.user || + !adnMessage.user.username || // pubKey lives in the username field + !adnMessage.user.name || // profileName lives in the name field + !adnMessage.text || + adnMessage.is_deleted + ) { + return; // Invalid or delete message + } - const { timestamp, quote } = messengerData; - if (!timestamp) { - return; // Invalid message - } + const messengerData = await this.getMessengerData(adnMessage); + if (messengerData === false) { + return; + } - // Duplicate check - const isDuplicate = message => { - // The username in this case is the users pubKey - const sameUsername = message.username === adnMessage.user.username; - const sameText = message.text === adnMessage.text; - // Don't filter out messages that are too far apart from each other - const timestampsSimilar = - Math.abs(message.timestamp - timestamp) <= - PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES; - - return sameUsername && sameText && timestampsSimilar; - }; + const { timestamp, quote } = messengerData; + if (!timestamp) { + return; // Invalid message + } - // Filter out any messages that we got previously - if (this.lastMessagesCache.some(isDuplicate)) { - return; // Duplicate message - } + // Duplicate check + const isDuplicate = message => { + // The username in this case is the users pubKey + const sameUsername = message.username === adnMessage.user.username; + const sameText = message.text === adnMessage.text; + // Don't filter out messages that are too far apart from each other + const timestampsSimilar = + Math.abs(message.timestamp - timestamp) <= + PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES; + + return sameUsername && sameText && timestampsSimilar; + }; - // FIXME: maybe move after the de-multidev-decode - // Add the message to the lastMessage cache and keep the last 5 recent messages - this.lastMessagesCache = [ - ...this.lastMessagesCache, - { - username: adnMessage.user.username, - text: adnMessage.text, - timestamp, - }, - ].splice(-5); - - const from = adnMessage.user.name; // profileName - console.log('checking', adnMessage.user.username, 'in', pubKeys) - if (pubKeys.indexOf(adnMessage.user.username) == -1) { - console.log('pushing pubkey', adnMessage.user.username) - pubKeys.push(adnMessage.user.username) - console.log('pubKeys', pubKeys) - } + // Filter out any messages that we got previously + if (this.lastMessagesCache.some(isDuplicate)) { + return; // Duplicate message + } - const messageData = { - serverId: adnMessage.id, - clientVerified: true, - friendRequest: false, - source: adnMessage.user.username, - sourceDevice: 1, - timestamp, - serverTimestamp: timestamp, - receivedAt, - isPublic: true, - message: { - body: adnMessage.text, - attachments: [], - group: { - id: this.conversationId, - type: textsecure.protobuf.GroupContext.Type.DELIVER, + // FIXME: maybe move after the de-multidev-decode + // Add the message to the lastMessage cache and keep the last 5 recent messages + this.lastMessagesCache = [ + ...this.lastMessagesCache, + { + username: adnMessage.user.username, + text: adnMessage.text, + timestamp, }, - flags: 0, - expireTimer: 0, - profileKey: null, + ].splice(-5); + + const from = adnMessage.user.name; // profileName + if (pubKeys.indexOf(`@${adnMessage.user.username}`) === -1) { + pubKeys.push(`@${adnMessage.user.username}`); + } + + const messageData = { + serverId: adnMessage.id, + clientVerified: true, + friendRequest: false, + source: adnMessage.user.username, + sourceDevice: 1, timestamp, - received_at: receivedAt, - sent_at: timestamp, - quote, - contact: [], - preview: [], - profile: { - displayName: from, + 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, + contact: [], + preview: [], + profile: { + displayName: from, + }, }, - }, - }; - receivedAt += 1; // Ensure different arrival times - - /* - this.serverAPI.chatAPI.emit('publicMessage', { - message: messageData, - }); - */ - console.log('pushing back message from', adnMessage.user.username) - pendingMessages.push(messageData) + }; + receivedAt += 1; // Ensure different arrival times - // now process any user meta data updates - // - update their conversation with a potentially new avatar - }); + // now process any user meta data updates + // - update their conversation with a potentially new avatar + return messageData; + }) + ); this.conversation.setLastRetrievedMessage(this.lastGot); - // - console.log('pendingMessages', pendingMessages.length) if (pendingMessages.length) { - const slavePrimaryMap = {}; + this.slavePrimaryMap = {}; - console.log('premultiDeviceResults', pubKeys) + // console.log('premultiDeviceResults', pubKeys) if (pubKeys.length) { - const multiDeviceResults = await lokiFileServerAPI.getDeviceMappingForUsers(pubKeys); - console.log('multiDeviceResults', multiDeviceResults) - // no user or isPrimary means not multidevice, send event now - + const multiDeviceResults = await lokiFileServerAPI.getDeviceMappingForUsers( + pubKeys + ); + // console.log('multiDeviceResults', multiDeviceResults); + + multiDeviceResults.forEach(user => { + if (user.annotations) { + user.annotations.forEach(note => { + if (note.type === 'network.loki.messenger.devicemapping') { + // console.log('devmap note', note); + // is slave? + if (note.value.isPrimary === '0') { + const { authorisations } = note.value; + if (Array.isArray(authorisations)) { + authorisations.forEach(auth => { + // console.log('auth', auth); + // FIXME: verify secondary sig + if (1) { + // add map to slavePrimaryMap + if ( + this.slavePrimaryMap[user.username] && + this.slavePrimaryMap[user.username] !== + auth.primaryDevicePubKey + ) { + log.warn( + `file server user annotation primaryKey mismatch, had ${ + this.slavePrimaryMap[user.username] + } now ${auth.primaryDevicePubKey} for ${ + user.username + }` + ); + return; + } + this.slavePrimaryMap[user.username] = + auth.primaryDevicePubKey; + } + }); + } + } + } + }); + } + }); + log.info('updated slavePrimaryMap', this.slavePrimaryMap); const primaryPubKeys = []; + const slaveMessages = {}; // go through multiDeviceResults and get primary Pubkey - // verify secondary sig - // add map to slavePrimaryMap + pendingMessages.forEach(messageData => { + // why am I getting these? + if (messageData === undefined) { + log.warn('invalid pendingMessages'); + return; + } + if (this.slavePrimaryMap[messageData.source]) { + const primaryPubKey = this.slavePrimaryMap[messageData.source]; + // add to lookup (if we don't already have it) + if (primaryPubKeys.indexOf(`@${primaryPubKey}`) === -1) { + primaryPubKeys.push(`@${primaryPubKey}`); + } + // delay sending the message + if (slaveMessages[messageData.source] === undefined) { + slaveMessages[messageData.source] = [messageData]; + } else { + slaveMessages[messageData.source].push(messageData); + } + } else { + // no user or isPrimary means not multidevice, send event now + this.serverAPI.chatAPI.emit('publicMessage', { + message: messageData, + }); + } + }); + pendingMessages = []; // free memory const verifiedPrimaryPKs = []; // get a list of all of primary pubKeys to verify the secondaryDevice - const primaryDeviceResults = await lokiFileServerAPI.getDeviceMappingForUsers(primaryPubKeys); - // go through primaryDeviceResults and verify primary sig - // if not verified remove from slavePrimaryMap + const primaryDeviceResults = await lokiFileServerAPI.getDeviceMappingForUsers( + primaryPubKeys + ); + // go through primaryDeviceResults + primaryDeviceResults.forEach(user => { + if (user.annotations) { + user.annotations.forEach(note => { + if (note.type === 'network.loki.messenger.devicemapping') { + if (note.value.isPrimary) { + const { authorisations } = note.value; + let found = false; + if (Array.isArray(authorisations)) { + authorisations.forEach(auth => { + // FIXME: verify primary sig + if ( + verifiedPrimaryPKs.indexOf( + `@${auth.primaryDevicePubKey}` + ) === -1 + ) { + verifiedPrimaryPKs.push( + `@${auth.primaryDevicePubKey}` + ); + } + found = true; + }); + } + if (found) { + return; + } + // if not verified remove from slavePrimaryMap + } + // not primary or verified + /* + Object.keys(slavePrimaryMap).forEach(slaveKey => { + if (slavePrimaryMap[slaveKey] == + }) + */ + } + }); + } + }); // get final list of verified chat server profile names - const verifiedDeviceResults = await this.getDeviceMappingForUsers(verifiedPrimaryPKs); + const verifiedDeviceResults = await this.serverAPI.getUsersAnnotations( + pubKeys + ); + // console.log('verifiedDeviceResults', verifiedDeviceResults) + // go through verifiedDeviceResults - // find messages for original slave key using slavePrimaryMap + this.primaryUserProfileName = {}; + verifiedDeviceResults.forEach(user => { + this.primaryUserProfileName[user.username] = user.name; + }); + Object.keys(slaveMessages).forEach(slaveKey => { + const primaryPubKey = this.slavePrimaryMap[slaveKey]; + slaveMessages[slaveKey].forEach(messageDataP => { + const messageData = messageDataP; // for linter + if (this.slavePrimaryMap[messageData.source]) { + // rewrite source, profile + messageData.source = primaryPubKey; + messageData.message.profile.displayName = this.primaryUserProfileName[ + primaryPubKey + ]; + } + // console.log('messageData', messageData) + this.serverAPI.chatAPI.emit('publicMessage', { + message: messageData, + }); + }); + }); } - pendingMessages.forEach(messageData => { - if (slavePrimaryMap[messageData.source]) { - // rewrite source, profile - messageData.source = slavePrimaryMap.username - messageData.message.profile.displayName = slavePrimaryMap.name - } - this.serverAPI.chatAPI.emit('publicMessage', { - message: messageData, + // console.log('pendingMessages len', pendingMessages.length); + // console.log('pendingMessages', pendingMessages); + // find messages for original slave key using slavePrimaryMap + if (pendingMessages.length) { + pendingMessages.forEach(messageDataP => { + const messageData = messageDataP; // for linter + // why am I getting these? + if (messageData === undefined) { + log.warn(`invalid pendingMessages ${pendingMessages}`); + return; + } + if (this.slavePrimaryMap[messageData.source]) { + // rewrite source, profile + const primaryPubKey = this.slavePrimaryMap[messageData.source]; + log.info('Rewriting', messageData.source, 'to', primaryPubKey); + messageData.source = primaryPubKey; + messageData.message.profile.displayName = this.primaryUserProfileName[ + primaryPubKey + ]; + } + this.serverAPI.chatAPI.emit('publicMessage', { + message: messageData, + }); }); - }) - pendingMessages = [] + } + pendingMessages = []; } } }