diff --git a/ts/session/protocols/SessionProtocol.ts b/ts/session/protocols/SessionProtocol.ts index c95629c2c..56bd08dc8 100644 --- a/ts/session/protocols/SessionProtocol.ts +++ b/ts/session/protocols/SessionProtocol.ts @@ -1,11 +1,125 @@ import { SessionResetMessage } from '../messages/outgoing'; // import { MessageSender } from '../sending'; -// These two Maps should never be accessed directly but only -// through `_update*SessionTimestamp()`, `_get*SessionRequest()` or `_has'SessionRequest()` +/** + * 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: Map; + +/** + * 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: Map; +/** + * 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(); + + +/** ======= 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(); +} + +/** + * 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 { + if (hasSession(device) || hasSentSessionRequest(device)) { + return Promise.resolve(); + } + + const preKeyBundle = await window.libloki.storage.getPreKeyBundleForContact(device); + const sessionReset = new SessionResetMessage({ + preKeyBundle, + timestamp: Date.now(), + }); + + return sendSessionRequests(sessionReset, device); +} + +/** */ +export async function sendSessionRequests( + message: SessionResetMessage, + device: string +): Promise { + 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); + // }); +} + +/** + * 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 async function shouldProcessSessionRequest( + device: string, + messageTimestamp: number +): 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()); +} + +/** ======= local / utility functions ======= */ + + /** * We only need to fetch once from the database, because we are the only one writing to it */ @@ -18,11 +132,15 @@ async function _fetchFromDBIfNeeded(): Promise { } async function _writeToDBSentSessions(): Promise { - // TODO actually write to DB + const data = { id: 'sentSessionsTimestamp', value: JSON.stringify(sentSessionsTimestamp) }; + + await window.Signal.Data.createOrUpdateItem(data); } async function _writeToDBProcessedSessions(): Promise { - // TODO actually write to DB + const data = { id: 'processedSessionsTimestamp', value: JSON.stringify(processedSessionsTimestamp) }; + + await window.Signal.Data.createOrUpdateItem(data); } @@ -59,9 +177,6 @@ async function _updateProcessedSessionTimestamp(device: string, timestamp: numbe } } -export function hasSession(device: string): boolean { - return false; // TODO: Implement -} /** * This is a utility function to avoid duplicate code between `_getProcessedSessionRequest()` and `_getSentSessionRequest()` @@ -73,60 +188,15 @@ async function _getSessionRequest(device: string, map: Map): Pro } async function _getSentSessionRequest(device: string): Promise { - return _getSessionRequest(device, processedSessionsTimestamp); -} - -async function _getProcessedSessionRequest(device: string): Promise { return _getSessionRequest(device, sentSessionsTimestamp); } -export async function hasSentSessionRequest(device: string): Promise { - const hasSent = await _getSessionRequest(device, sentSessionsTimestamp); - - return !!hasSent; -} - -export async function sendSessionRequestIfNeeded( - device: string -): Promise { - if (hasSession(device) || hasSentSessionRequest(device)) { - return Promise.resolve(); - } - - // TODO: Call sendSessionRequest with SessionReset - return Promise.reject(new Error('Need to implement this function')); -} - -export async function sendSessionRequests( - message: SessionResetMessage, - device: string -): Promise { - - // Optimistically store timestamp of when session request was sent - await _updateSentSessionTimestamp(device, Date.now()); - - // await MessageSender.send() - - // TODO: Send out the request via MessageSender - // TODO: On failure, unset the timestamp - return Promise.resolve(); -} - -export async function sessionEstablished(device: string) { - // remove our existing sent timestamp for that device - return _updateSentSessionTimestamp(device, undefined); +async function _getProcessedSessionRequest(device: string): Promise { + return _getSessionRequest(device, processedSessionsTimestamp); } -export async function shouldProcessSessionRequest( - device: string, - messageTimestamp: number -): Promise { - const existingSentTimestamp = await _getSentSessionRequest(device) || 0; - const existingProcessedTimestamp = await _getProcessedSessionRequest(device) || 0; - - return messageTimestamp > existingSentTimestamp && messageTimestamp > existingProcessedTimestamp; -} +async function _hasSentSessionRequest(device: string): Promise { + await _fetchFromDBIfNeeded(); -export async function onSessionRequestProcessed(device: string) { - return _updateProcessedSessionTimestamp(device, Date.now()); + return sentSessionsTimestamp.has(device); }