diff --git a/js/background.js b/js/background.js index 081ce55bc..3631ae88f 100644 --- a/js/background.js +++ b/js/background.js @@ -747,11 +747,6 @@ Whisper.Notifications.enable(); }, window.CONSTANTS.NOTIFICATION_ENABLE_TIMEOUT_SECONDS * 1000); - // TODO: Investigate the case where we reconnect - const ourKey = window.libsession.Utils.UserUtils.getOurPubKeyStrFromCache(); - window.SwarmPolling.addPubkey(ourKey); - window.SwarmPolling.start(); - window.NewReceiver.queueAllCached(); initAPIs(); diff --git a/preload.js b/preload.js index 064f224c6..ca15f6608 100644 --- a/preload.js +++ b/preload.js @@ -410,14 +410,6 @@ window.DataMessageReceiver = require('./ts/receiver/dataMessage'); window.NewSnodeAPI = require('./ts/session/snode_api/serviceNodeAPI'); window.SnodePool = require('./ts/session/snode_api/snodePool'); -if (process.env.USE_STUBBED_NETWORK) { - const { SwarmPollingStub } = require('./ts/session/snode_api/swarmPollingStub'); - window.SwarmPolling = new SwarmPollingStub(); -} else { - const { SwarmPolling } = require('./ts/session/snode_api/swarmPolling'); - window.SwarmPolling = new SwarmPolling(); -} - // eslint-disable-next-line no-extend-native,func-names Promise.prototype.ignore = function() { // eslint-disable-next-line more/no-then diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index e8f59ca44..98d0cdde0 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -33,6 +33,7 @@ import { cleanUpOldDecryptedMedias } from '../../session/crypto/DecryptedAttachm import { OpenGroupManagerV2 } from '../../opengroup/opengroupV2/OpenGroupManagerV2'; import { loadDefaultRooms } from '../../opengroup/opengroupV2/ApiUtil'; import { forceRefreshRandomSnodePool } from '../../session/snode_api/snodePool'; +import { SwarmPolling } from '../../session/snode_api/swarmPolling'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -176,6 +177,11 @@ const doAppStartUp = (dispatch: Dispatch) => { void triggerSyncIfIfNeeded(); void loadDefaultRooms(); + + // TODO: Investigate the case where we reconnect + const ourKey = UserUtils.getOurPubKeyStrFromCache(); + SwarmPolling.getInstance().addPubkey(ourKey); + SwarmPolling.getInstance().start(); }; /** diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index c5793c784..6b12bbc9f 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -33,6 +33,7 @@ import { MessageController } from '../session/messages'; import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage'; import { queueAllCachedFromSource } from './receiver'; import { actions as conversationActions } from '../state/ducks/conversations'; +import { SwarmPolling } from '../session/snode_api/swarmPolling'; export const distributingClosedGroupEncryptionKeyPairs = new Map(); @@ -237,13 +238,32 @@ export async function handleNewClosedGroup( await addClosedGroupEncryptionKeyPair(groupId, ecKeyPair.toHexKeyPair()); // start polling for this new group - window.SwarmPolling.addGroupId(PubKey.cast(groupId)); + SwarmPolling.getInstance().addGroupId(PubKey.cast(groupId)); await removeFromCache(envelope); // trigger decrypting of all this group messages we did not decrypt successfully yet. await queueAllCachedFromSource(groupId); } +/** + * + * @param isKicked if true, we mark the reason for leaving as a we got kicked + */ +export async function markGroupAsLeftOrKicked( + groupPublicKey: string, + groupConvo: ConversationModel, + isKicked: boolean +) { + await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); + + if (isKicked) { + groupConvo.set('isKickedFromGroup', true); + } else { + groupConvo.set('left', true); + } + SwarmPolling.getInstance().removePubkey(groupPublicKey); +} + async function handleUpdateClosedGroup( envelope: EnvelopePlus, groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, @@ -273,17 +293,14 @@ async function handleUpdateClosedGroup( await removeFromCache(envelope); return; } - await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); - // Disable typing: - convo.set('isKickedFromGroup', true); - window.SwarmPolling.removePubkey(groupPublicKey); + await markGroupAsLeftOrKicked(groupPublicKey, convo, true); } else { if (convo.get('isKickedFromGroup')) { // Enable typing: convo.set('isKickedFromGroup', false); convo.set('left', false); // Subscribe to this group id - window.SwarmPolling.addGroupId(new PubKey(groupPublicKey)); + SwarmPolling.getInstance().addGroupId(new PubKey(groupPublicKey)); } } @@ -492,7 +509,7 @@ async function performIfValid( } else if (groupUpdate.type === Type.MEMBERS_REMOVED) { await handleClosedGroupMembersRemoved(envelope, groupUpdate, convo); } else if (groupUpdate.type === Type.MEMBER_LEFT) { - await handleClosedGroupMemberLeft(envelope, groupUpdate, convo); + await handleClosedGroupMemberLeft(envelope, convo); } else if (groupUpdate.type === Type.ENCRYPTION_KEY_PAIR_REQUEST) { if (window.lokiFeatureFlags.useRequestEncryptionKeyPair) { await handleClosedGroupEncryptionKeyPairRequest(envelope, groupUpdate, convo); @@ -555,7 +572,7 @@ async function handleClosedGroupMembersAdded( } if (await areWeAdmin(convo)) { - await sendLatestKeyPairToUsers(envelope, convo, convo.id, membersNotAlreadyPresent); + await sendLatestKeyPairToUsers(convo, convo.id, membersNotAlreadyPresent); } const members = [...oldMembers, ...membersNotAlreadyPresent]; @@ -616,10 +633,7 @@ async function handleClosedGroupMembersRemoved( const ourPubKey = UserUtils.getOurPubKeyFromCache(); const wasCurrentUserRemoved = !membersAfterUpdate.includes(ourPubKey.key); if (wasCurrentUserRemoved) { - await removeAllClosedGroupEncryptionKeyPairs(groupPubKey); - // Disable typing: - convo.set('isKickedFromGroup', true); - window.SwarmPolling.removePubkey(groupPubKey); + await markGroupAsLeftOrKicked(groupPublicKey, convo, true); } // Generate and distribute a new encryption key pair if needed if (await areWeAdmin(convo)) { @@ -651,19 +665,103 @@ async function handleClosedGroupMembersRemoved( await removeFromCache(envelope); } -async function handleClosedGroupMemberLeft( +function isUserAZombie(convo: ConversationModel, user: PubKey) { + return convo.get('zombies').includes(user.key); +} + +/** + * Returns true if the user was not a zombie and so was added to the zombies. + * No commit() are called + */ +function addMemberToZombies( envelope: EnvelopePlus, - groupUpdate: SignalService.DataMessage.ClosedGroupControlMessage, + userToAdd: PubKey, + convo: ConversationModel +): boolean { + const zombies = convo.get('zombies'); + const isAlreadyZombie = isUserAZombie(convo, userToAdd); + + if (isAlreadyZombie) { + return false; + } + console.warn('Marking user ', userToAdd.key, ' as a zombie'); + convo.set('zombies', [...zombies, userToAdd.key]); + return true; +} + +/** + * + * Returns true if the user was not a zombie and so was not removed from the zombies. + * Note: no commit() are made + */ +function removeMemberFromZombies( + envelope: EnvelopePlus, + userToAdd: PubKey, convo: ConversationModel +): boolean { + const zombies = convo.get('zombies'); + const isAlreadyAZombie = isUserAZombie(convo, userToAdd); + + if (!isAlreadyAZombie) { + return false; + } + convo.set( + 'zombies', + zombies.filter(z => z !== userToAdd.key) + ); + return true; +} + +async function handleClosedGroupAdminMemberLeft( + groupPublicKey: string, + isCurrentUserAdmin: boolean, + convo: ConversationModel, + envelope: EnvelopePlus +) { + // if the admin was remove and we are the admin, it can only be voluntary + await markGroupAsLeftOrKicked(groupPublicKey, convo, !isCurrentUserAdmin); + + convo.set('members', []); + // everybody left ! this is how we disable a group when the admin left + const groupDiff: ClosedGroup.GroupDiff = { + leavingMembers: convo.get('members'), + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp)); + convo.updateLastMessage(); + + await convo.commit(); + await removeFromCache(envelope); +} + +async function handleClosedGroupLeftOurself( + groupPublicKey: string, + convo: ConversationModel, + envelope: EnvelopePlus ) { + await markGroupAsLeftOrKicked(groupPublicKey, convo, false); + const groupDiff: ClosedGroup.GroupDiff = { + leavingMembers: [envelope.senderIdentity], + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp)); + convo.updateLastMessage(); + // remove ourself from the list of members + convo.set( + 'members', + convo.get('members').filter(m => !UserUtils.isUsFromCache(m)) + ); + + await convo.commit(); + await removeFromCache(envelope); +} + +async function handleClosedGroupMemberLeft(envelope: EnvelopePlus, convo: ConversationModel) { const sender = envelope.senderIdentity; const groupPublicKey = envelope.source; const didAdminLeave = convo.get('groupAdmins')?.includes(sender) || false; // If the admin leaves the group is disbanded // otherwise, we remove the sender from the list of current members in this group const oldMembers = convo.get('members') || []; - const leftMemberWasPresent = oldMembers.includes(sender); - const members = didAdminLeave ? [] : oldMembers.filter(s => s !== sender); + const newMembers = oldMembers.filter(s => s !== sender); // Show log if we sent this message ourself (from another device or not) if (UserUtils.isUsFromCache(sender)) { @@ -671,56 +769,42 @@ async function handleClosedGroupMemberLeft( } const ourPubkey = UserUtils.getOurPubKeyStrFromCache(); - // Generate and distribute a new encryption key pair if needed + // if the admin leaves, the group is disabled for every members const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourPubkey) || false; - if (isCurrentUserAdmin && !!members.length) { - await ClosedGroup.generateAndSendNewEncryptionKeyPair(groupPublicKey, members); - } if (didAdminLeave) { - window.SwarmPolling.removePubkey(groupPublicKey); - - await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); - // Disable typing - // if the admin was remove and we are the admin, it can only be voluntary - if (isCurrentUserAdmin) { - convo.set('left', true); - } else { - convo.set('isKickedFromGroup', true); - } + await handleClosedGroupAdminMemberLeft(groupPublicKey, isCurrentUserAdmin, convo, envelope); + return; } - const didWeLeaveFromAnotherDevice = !members.includes(ourPubkey); - if (didWeLeaveFromAnotherDevice) { - await removeAllClosedGroupEncryptionKeyPairs(groupPublicKey); - // Disable typing: - convo.set('left', true); - window.SwarmPolling.removePubkey(groupPublicKey); + // if we are no longer a member, we LEFT from another device + if (!newMembers.includes(ourPubkey)) { + // stop polling, remove all stored pubkeys and make sure the UI does not let us write messages + await handleClosedGroupLeftOurself(groupPublicKey, convo, envelope); + return; } - // Only add update message if we have something to show - if (leftMemberWasPresent) { - const groupDiff: ClosedGroup.GroupDiff = { - leavingMembers: didAdminLeave ? oldMembers : [sender], - }; - await ClosedGroup.addUpdateMessage( - convo, - groupDiff, - 'incoming', - _.toNumber(envelope.timestamp) - ); - convo.updateLastMessage(); + // if we are the admin, and there are still some members after the member left, we send a new keypair + // to the remaining members + if (isCurrentUserAdmin && !!newMembers.length) { + await ClosedGroup.generateAndSendNewEncryptionKeyPair(groupPublicKey, newMembers); } - convo.set('members', members); - + // Another member left, not us, not the admin, just another member. + // But this member was in the list of members (as performIfValid checks for that) + const groupDiff: ClosedGroup.GroupDiff = { + leavingMembers: [sender], + }; + await ClosedGroup.addUpdateMessage(convo, groupDiff, 'incoming', _.toNumber(envelope.timestamp)); + convo.updateLastMessage(); + await addMemberToZombies(envelope, PubKey.cast(sender), convo); + convo.set('members', newMembers); await convo.commit(); await removeFromCache(envelope); } async function sendLatestKeyPairToUsers( - envelope: EnvelopePlus, groupConvo: ConversationModel, groupPubKey: string, targetUsers: Array @@ -778,7 +862,7 @@ async function handleClosedGroupEncryptionKeyPairRequest( await removeFromCache(envelope); return; } - await sendLatestKeyPairToUsers(envelope, groupConvo, groupPublicKey, [sender]); + await sendLatestKeyPairToUsers(groupConvo, groupPublicKey, [sender]); return removeFromCache(envelope); } @@ -850,7 +934,7 @@ export async function createClosedGroup(groupName: string, members: Array { window.log.info(`Leaving message sent ${groupId}. Removing everything related to this group.`); - await removeAllClosedGroupEncryptionKeyPairs(groupId); + await markGroupAsLeftOrKicked(groupId, convo, false); }); } diff --git a/ts/session/snode_api/swarmPolling.ts b/ts/session/snode_api/swarmPolling.ts index 8a798a4b6..4d406c923 100644 --- a/ts/session/snode_api/swarmPolling.ts +++ b/ts/session/snode_api/swarmPolling.ts @@ -41,16 +41,24 @@ export function processMessage(message: string, options: any = {}) { } export class SwarmPolling { + private static instance: SwarmPolling; private pubkeys: Array; private groupPubkeys: Array; private readonly lastHashes: { [key: string]: PubkeyToHash }; - constructor() { + private constructor() { this.pubkeys = []; this.groupPubkeys = []; this.lastHashes = {}; } + public static getInstance() { + if (!SwarmPolling.instance) { + SwarmPolling.instance = new SwarmPolling(); + } + return SwarmPolling.instance; + } + public start(): void { this.loadGroupIds(); void this.pollForAllKeys(); diff --git a/ts/session/snode_api/swarmPollingStub.ts b/ts/session/snode_api/swarmPollingStub.ts deleted file mode 100644 index c05ab8c48..000000000 --- a/ts/session/snode_api/swarmPollingStub.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { processMessage, SwarmPolling } from './swarmPolling'; -import { default as insecureNodeFetch } from 'node-fetch'; -import { PubKey } from '../types'; - -export class SwarmPollingStub extends SwarmPolling { - private readonly baseUrl = 'http://localhost:3000'; - - protected async pollOnceForKey(pubkey: PubKey, isGroup: boolean) { - const pubkeyStr = pubkey.key ? pubkey.key : pubkey; - - const get = { - method: 'GET', - }; - - // insecureNodeFetch but this is a stub - const res = await insecureNodeFetch(`${this.baseUrl}/messages?pubkey=${pubkeyStr}`, get); - - try { - const json = await res.json(); - - const options = isGroup ? { conversationId: pubkeyStr } : {}; - - json.messages.forEach((m: any) => { - processMessage(m.data, options); - }); - } catch (e) { - window.log.error('invalid json: ', e); - } - } -} diff --git a/ts/window.d.ts b/ts/window.d.ts index b9e6ce240..5577fa796 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -4,7 +4,6 @@ import { SignalInterface } from '../../js/modules/signal'; import { Libloki } from '../libloki'; import { LokiPublicChatFactoryInterface } from '../js/modules/loki_public_chat_api'; import { LokiAppDotNetServerInterface } from '../js/modules/loki_app_dot_net_api'; -import { SwarmPolling } from './session/snode_api/swarmPolling'; import { LibTextsecure } from '../libtextsecure'; import { ConfirmationDialogParams } from '../background'; @@ -83,7 +82,6 @@ declare global { versionInfo: any; getStoragePubKey: (key: string) => string; getConversations: () => ConversationCollection; - SwarmPolling: SwarmPolling; SnodePool: { getSnodesFor: (string) => any; };