diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts index ebeac9f37..eae8fcbad 100644 --- a/ts/session/protocols/SessionProtocol.ts +++ b/ts/session/protocols/SessionProtocol.ts @@ -1,19 +1,64 @@ -// TODO: Need to flesh out these functions -// Structure of this can be changed for example sticking this all in a class -// The reason i haven't done it is to avoid having instances of the protocol, rather you should be able to call the functions directly +import { SessionResetMessage } from '../messages/outgoing'; +// import { MessageSender } from '../sending'; -import { OutgoingContentMessage } from '../messages/outgoing'; +interface StringToNumberMap { + [key: string]: number; +} + +/** + * This map olds the sent session timestamps, i.e. session requests message effectively sent to the recipient. + * It is backed by a database entry so it's loaded from db on startup. + * This map should not be used directly, but instead through + * `_updateSendSessionTimestamp()`, `_getSendSessionRequest()` or `_hasSendSessionRequest()` + */ +let sentSessionsTimestamp: StringToNumberMap; + +/** + * This map olds the processed session timestamps, i.e. when we received a session request and handled it. + * It is backed by a database entry so it's loaded from db on startup. + * This map should not be used directly, but instead through + * `_updateProcessedSessionTimestamp()`, `_getProcessedSessionRequest()` or `_hasProcessedSessionRequest()` + */ +let processedSessionsTimestamp: StringToNumberMap; + +/** + * This map olds the timestamp on which a sent session reset is triggered for a specific device. + * Once the message is sent or failed to sent, this device is removed from here. + * This is a memory only map. Which means that on app restart it's starts empty. + */ +const pendingSendSessionsTimestamp: Set = new Set(); -export function hasSession(device: string): boolean { - return false; // TODO: Implement +/** ======= exported functions ======= */ + +/** Returns true if we already have a session with that device */ +export async function hasSession(device: string): Promise { + // Session does not use the concept of a deviceId, thus it's always 1 + const address = new window.libsignal.SignalProtocolAddress(device, 1); + const sessionCipher = new window.libsignal.SessionCipher( + window.textsecure.storage.protocol, + address + ); + + return sessionCipher.hasOpenSession(); } -export function hasSentSessionRequest(device: string): boolean { - // TODO: need a way to keep track of if we've sent a session request - // My idea was to use the timestamp of when it was sent but there might be another better approach - return false; +/** + * Returns true if we sent a session request to that device already OR + * if a session request to that device is right now being sent. + */ +export async function hasSentSessionRequest(device: string): Promise { + const pendingSend = pendingSendSessionsTimestamp.has(device); + const hasSent = await _hasSentSessionRequest(device); + + return pendingSend || hasSent; } +/** + * Triggers a SessionResetMessage to be sent if: + * - we do not already have a session and + * - we did not sent a session request already to that device and + * - we do not have a session request currently being send to that device + */ export async function sendSessionRequestIfNeeded( device: string ): Promise { @@ -21,37 +66,186 @@ export async function sendSessionRequestIfNeeded( return Promise.resolve(); } - // TODO: Call sendSessionRequest with SessionReset - return Promise.reject(new Error('Need to implement this function')); + const preKeyBundle = await window.libloki.storage.getPreKeyBundleForContact( + device + ); + const sessionReset = new SessionResetMessage({ + preKeyBundle, + timestamp: Date.now(), + }); + + return sendSessionRequests(sessionReset, device); } -// TODO: Replace OutgoingContentMessage with SessionReset -export async function sendSessionRequest( - message: OutgoingContentMessage +/** */ +export async function sendSessionRequests( + message: SessionResetMessage, + device: string ): Promise { - // TODO: Optimistically store timestamp of when session request was sent - // TODO: Send out the request via MessageSender - // TODO: On failure, unset the timestamp - return Promise.resolve(); + const timestamp = Date.now(); + + // mark the session as being pending send with current timestamp + // so we know we already triggered a new session with that device + pendingSendSessionsTimestamp.add(device); + // const rawMessage = toRawMessage(message); + // // TODO: Send out the request via MessageSender + + // return MessageSender.send(rawMessage) + // .then(async () => { + // await _updateSentSessionTimestamp(device, timestamp); + // pendingSendSessionsTimestamp.delete(device); + // }) + // .catch(() => { + // pendingSendSessionsTimestamp.delete(device); + // }); } -export function sessionEstablished(device: string) { - // TODO: this is called when we receive an encrypted message from the other user - // Maybe it should be renamed to something else - // TODO: This should make `hasSentSessionRequest` return `false` +/** + * Called when a session is establish so we store on database this info. + */ +export async function onSessionEstablished(device: string) { + // remove our existing sent timestamp for that device + return _updateSentSessionTimestamp(device, undefined); } -export function shouldProcessSessionRequest( +export async function shouldProcessSessionRequest( device: string, messageTimestamp: number -): boolean { - // TODO: Need to do the following here - // messageTimestamp > session request sent timestamp && messageTimestamp > session request processed timestamp - return false; +): Promise { + const existingSentTimestamp = (await _getSentSessionRequest(device)) || 0; + const existingProcessedTimestamp = + (await _getProcessedSessionRequest(device)) || 0; + + return ( + messageTimestamp > existingSentTimestamp && + messageTimestamp > existingProcessedTimestamp + ); +} + +export async function onSessionRequestProcessed(device: string) { + return _updateProcessedSessionTimestamp(device, Date.now()); } -export function sessionRequestProcessed(device: string) { - // TODO: this is called when we process the session request - // This should store the processed timestamp - // Again naming is crap so maybe some other name is better +/** ======= local / utility functions ======= */ + +/** + * We only need to fetch once from the database, because we are the only one writing to it + */ +async function _fetchFromDBIfNeeded(): Promise { + if (!sentSessionsTimestamp) { + const sentItem = await window.Signal.Data.getItemById( + 'sentSessionsTimestamp' + ); + if (sentItem) { + sentSessionsTimestamp = sentItem.value; + } else { + sentSessionsTimestamp = {}; + } + + const processedItem = await window.Signal.Data.getItemById( + 'processedSessionsTimestamp' + ); + if (processedItem) { + processedSessionsTimestamp = processedItem.value; + } else { + processedSessionsTimestamp = {}; + } + } +} + +async function _writeToDBSentSessions(): Promise { + const data = { + id: 'sentSessionsTimestamp', + value: JSON.stringify(sentSessionsTimestamp), + }; + + await window.Signal.Data.createOrUpdateItem(data); +} + +async function _writeToDBProcessedSessions(): Promise { + const data = { + id: 'processedSessionsTimestamp', + value: JSON.stringify(processedSessionsTimestamp), + }; + + await window.Signal.Data.createOrUpdateItem(data); +} + +/** + * This is a utility function to avoid duplicated code of _updateSentSessionTimestamp and _updateProcessedSessionTimestamp + */ +async function _updateSessionTimestamp( + device: string, + timestamp: number | undefined, + map: StringToNumberMap +): Promise { + await _fetchFromDBIfNeeded(); + if (!timestamp) { + if (!!map[device]) { + delete map.device; + + return true; + } + + return false; + } + map[device] = timestamp; + + return true; +} + +/** + * + * @param device the device id + * @param timestamp undefined to remove the key/value pair, otherwise updates the sent timestamp and write to DB + */ +async function _updateSentSessionTimestamp( + device: string, + timestamp: number | undefined +): Promise { + if (_updateSessionTimestamp(device, timestamp, sentSessionsTimestamp)) { + await _writeToDBSentSessions(); + } +} + +/** + * timestamp undefined to remove the key/value pair, otherwise updates the processed timestamp and writes to DB + */ +async function _updateProcessedSessionTimestamp( + device: string, + timestamp: number | undefined +): Promise { + if (_updateSessionTimestamp(device, timestamp, processedSessionsTimestamp)) { + await _writeToDBProcessedSessions(); + } +} + +/** + * This is a utility function to avoid duplicate code between `_getProcessedSessionRequest()` and `_getSentSessionRequest()` + */ +async function _getSessionRequest( + device: string, + map: StringToNumberMap +): Promise { + await _fetchFromDBIfNeeded(); + + return map[device]; +} + +async function _getSentSessionRequest( + device: string +): Promise { + return _getSessionRequest(device, sentSessionsTimestamp); +} + +async function _getProcessedSessionRequest( + device: string +): Promise { + return _getSessionRequest(device, processedSessionsTimestamp); +} + +async function _hasSentSessionRequest(device: string): Promise { + await _fetchFromDBIfNeeded(); + + return !!sentSessionsTimestamp[device]; } diff --git a/ts/session/sending/MessageQueue.ts b/ts/session/sending/MessageQueue.ts index b927fbb79..909eb6b7d 100644 --- a/ts/session/sending/MessageQueue.ts +++ b/ts/session/sending/MessageQueue.ts @@ -5,9 +5,14 @@ import { MessageQueueInterfaceEvents, } from './MessageQueueInterface'; import { +<<<<<<< HEAD ContentMessage, OpenGroupMessage, SessionResetMessage, +======= + ContentMessage as OutgoingContentMessage, + OpenGroupMessage, +>>>>>>> 935ac8d8f911616731c20aa5b45b79bea6895731 } from '../messages/outgoing'; import { PendingMessageCache } from './PendingMessageCache'; import { JobQueue, TypedEventEmitter } from '../utils'; diff --git a/ts/session/sending/MessageQueueInterface.ts b/ts/session/sending/MessageQueueInterface.ts index 0a74f0164..182eb1e46 100644 --- a/ts/session/sending/MessageQueueInterface.ts +++ b/ts/session/sending/MessageQueueInterface.ts @@ -1,7 +1,12 @@ import { +<<<<<<< HEAD OpenGroupMessage, ContentMessage, SyncMessage, +======= + ContentMessage as OutgoingContentMessage, + OpenGroupMessage, +>>>>>>> 935ac8d8f911616731c20aa5b45b79bea6895731 } from '../messages/outgoing'; import { RawMessage } from '../types/RawMessage'; import { TypedEventEmitter } from '../utils'; diff --git a/ts/test/session/protocols/SessionProtocol_test.ts b/ts/test/session/protocols/SessionProtocol_test.ts new file mode 100644 index 000000000..8cd26ddfc --- /dev/null +++ b/ts/test/session/protocols/SessionProtocol_test.ts @@ -0,0 +1,9 @@ +// import { expect } from 'chai'; +// import { SessionProtocol } from '../../../session/protocols'; + +// describe('SessionProtocol', () => { +// it('has ', () => { + +// }); + +// });