<commit_before>/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";

/**
 * @module crypto/DeviceList
 *
 * Manages the list of other users' devices
 */

import Promise from 'bluebird';

import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';


/* State transition diagram for DeviceList._deviceTrackingStatus
 *
 *                                |
 *     stopTrackingDeviceList     V
 *   +---------------------> NOT_TRACKED
 *   |                            |
 *   +<--------------------+      | startTrackingDeviceList
 *   |                     |      V
 *   |   +-------------> PENDING_DOWNLOAD <--------------------+-+
 *   |   |                      ^ |                            | |
 *   |   | restart     download | |  start download            | | invalidateUserDeviceList
 *   |   | client        failed | |                            | |
 *   |   |                      | V                            | |
 *   |   +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
 *   |                    |       |                              |
 *   +<-------------------+       |  download successful         |
 *   ^                            V                              |
 *   +----------------------- UP_TO_DATE ------------------------+
 */


// constants for DeviceList._deviceTrackingStatus
const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;

/**
 * @alias module:crypto/DeviceList
 */
export default class DeviceList {
    constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
        this._cryptoStore = cryptoStore;
        this._sessionStore = sessionStore;

        // userId -> {
        //     deviceId -> {
        //         [device info]
        //     }
        // }
        this._devices = {};

        // which users we are tracking device status for.
        // userId -> TRACKING_STATUS_*
        this._deviceTrackingStatus = {}; // loaded from storage in load()

        // The 'next_batch' sync token at the point the data was writen,
        // ie. a token representing the point immediately after the
        // moment represented by the snapshot in the db.
        this._syncToken = null;

        this._serialiser = new DeviceListUpdateSerialiser(
            baseApis, olmDevice, this._devices,
        );

        // userId -> promise
        this._keyDownloadsInProgressByUser = {};

        // Set whenever changes are made other than setting the sync token
        this._dirty = false;

        // Promise resolved when device data is saved
        this._savePromise = null;
    }

    /**
     * Load the device tracking state from storage
     */
    async load() {
        let shouldDeleteSessionStore = false;
        await this._cryptoStore.doTxn(
            // migrate from session store if there's data there and not here
            'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
                this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
                    if (deviceData === null) {
                        console.log("Migrating e2e device data...");
                        this._devices = this._sessionStore.getAllEndToEndDevices() || {};
                        this._deviceTrackingStatus = (
                            this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
                        );
                        this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
                        this._cryptoStore.storeEndToEndDeviceData({
                            devices: this._devices,
                            trackingStatus: this._deviceTrackingStatus,
                            syncToken: this._syncToken,
                        }, txn);
                        shouldDeleteSessionStore = true;
                    } else {
                        this._devices = deviceData ? deviceData.devices : {},
                        this._deviceTrackingStatus = deviceData ?
                            deviceData.trackingStatus : {};
                        this._syncToken = deviceData ? deviceData.syncToken : null;
                    }
                });
            },
        );

        if (shouldDeleteSessionStore) {
            // migrated data is now safely persisted: remove from old store
            this._sessionStore.removeEndToEndDeviceData();
        }

        for (const u of Object.keys(this._deviceTrackingStatus)) {
            // if a download was in progress when we got shut down, it isn't any more.
            if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
                this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
            }
        }
    }

    /**
     * Save the device tracking state to storage, if any changes are
     * pending other than updating the sync token
     *
     * The actual save will be delayed by a short amount of time to
     * aggregate multiple writes to the database.
     *
     * @return {Promise<bool>} true if the data was saved, false if
     *     it was not (eg. because no changes were pending). The promise
     *     will only resolve once the data is saved, so may take some time
     *     to resolve.
     */
    async saveIfDirty() {
        if (!this._dirty) return Promise.resolve(false);

        if (this._savePromise === null) {
            // Delay saves for a bit so we can aggregate multiple saves that happen
            // in quick succession (eg. when a whole room's devices are marked as known)
            this._savePromise = Promise.delay(500).then(() => {
                console.log('Saving device tracking data at token ' + this._syncToken);
                // null out savePromise now (after the delay but before the write),
                // otherwise we could return the existing promise when the save has
                // actually already happened. Likewsie for the dirty flag.
                this._savePromise = null;
                this._dirty = false;
                return this._cryptoStore.doTxn(
                    'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
                        this._cryptoStore.storeEndToEndDeviceData({
                            devices: this._devices,
                            trackingStatus: this._deviceTrackingStatus,
                            syncToken: this._syncToken,
                        }, txn);
                    },
                );
            }).then(() => {
                return true;
            });
        }
        return this._savePromise;
    }

    /**
     * Gets the sync token last set with setSyncToken
     *
     * @return {string} The sync token
     */
    getSyncToken() {
        return this._syncToken;
    }

    /**
     * Sets the sync token that the app will pass as the 'since' to the /sync
     * endpoint next time it syncs.<commit_msg>Comment typo<commit_after>/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";

/**
 * @module crypto/DeviceList
 *
 * Manages the list of other users' devices
 */

import Promise from 'bluebird';

import DeviceInfo from './deviceinfo';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';


/* State transition diagram for DeviceList._deviceTrackingStatus
 *
 *                                |
 *     stopTrackingDeviceList     V
 *   +---------------------> NOT_TRACKED
 *   |                            |
 *   +<--------------------+      | startTrackingDeviceList
 *   |                     |      V
 *   |   +-------------> PENDING_DOWNLOAD <--------------------+-+
 *   |   |                      ^ |                            | |
 *   |   | restart     download | |  start download            | | invalidateUserDeviceList
 *   |   | client        failed | |                            | |
 *   |   |                      | V                            | |
 *   |   +------------ DOWNLOAD_IN_PROGRESS -------------------+ |
 *   |                    |       |                              |
 *   +<-------------------+       |  download successful         |
 *   ^                            V                              |
 *   +----------------------- UP_TO_DATE ------------------------+
 */


// constants for DeviceList._deviceTrackingStatus
const TRACKING_STATUS_NOT_TRACKED = 0;
const TRACKING_STATUS_PENDING_DOWNLOAD = 1;
const TRACKING_STATUS_DOWNLOAD_IN_PROGRESS = 2;
const TRACKING_STATUS_UP_TO_DATE = 3;

/**
 * @alias module:crypto/DeviceList
 */
export default class DeviceList {
    constructor(baseApis, cryptoStore, sessionStore, olmDevice) {
        this._cryptoStore = cryptoStore;
        this._sessionStore = sessionStore;

        // userId -> {
        //     deviceId -> {
        //         [device info]
        //     }
        // }
        this._devices = {};

        // which users we are tracking device status for.
        // userId -> TRACKING_STATUS_*
        this._deviceTrackingStatus = {}; // loaded from storage in load()

        // The 'next_batch' sync token at the point the data was writen,
        // ie. a token representing the point immediately after the
        // moment represented by the snapshot in the db.
        this._syncToken = null;

        this._serialiser = new DeviceListUpdateSerialiser(
            baseApis, olmDevice, this._devices,
        );

        // userId -> promise
        this._keyDownloadsInProgressByUser = {};

        // Set whenever changes are made other than setting the sync token
        this._dirty = false;

        // Promise resolved when device data is saved
        this._savePromise = null;
    }

    /**
     * Load the device tracking state from storage
     */
    async load() {
        let shouldDeleteSessionStore = false;
        await this._cryptoStore.doTxn(
            // migrate from session store if there's data there and not here
            'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
                this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
                    if (deviceData === null) {
                        console.log("Migrating e2e device data...");
                        this._devices = this._sessionStore.getAllEndToEndDevices() || {};
                        this._deviceTrackingStatus = (
                            this._sessionStore.getEndToEndDeviceTrackingStatus() || {}
                        );
                        this._syncToken = this._sessionStore.getEndToEndDeviceSyncToken();
                        this._cryptoStore.storeEndToEndDeviceData({
                            devices: this._devices,
                            trackingStatus: this._deviceTrackingStatus,
                            syncToken: this._syncToken,
                        }, txn);
                        shouldDeleteSessionStore = true;
                    } else {
                        this._devices = deviceData ? deviceData.devices : {},
                        this._deviceTrackingStatus = deviceData ?
                            deviceData.trackingStatus : {};
                        this._syncToken = deviceData ? deviceData.syncToken : null;
                    }
                });
            },
        );

        if (shouldDeleteSessionStore) {
            // migrated data is now safely persisted: remove from old store
            this._sessionStore.removeEndToEndDeviceData();
        }

        for (const u of Object.keys(this._deviceTrackingStatus)) {
            // if a download was in progress when we got shut down, it isn't any more.
            if (this._deviceTrackingStatus[u] == TRACKING_STATUS_DOWNLOAD_IN_PROGRESS) {
                this._deviceTrackingStatus[u] = TRACKING_STATUS_PENDING_DOWNLOAD;
            }
        }
    }

    /**
     * Save the device tracking state to storage, if any changes are
     * pending other than updating the sync token
     *
     * The actual save will be delayed by a short amount of time to
     * aggregate multiple writes to the database.
     *
     * @return {Promise<bool>} true if the data was saved, false if
     *     it was not (eg. because no changes were pending). The promise
     *     will only resolve once the data is saved, so may take some time
     *     to resolve.
     */
    async saveIfDirty() {
        if (!this._dirty) return Promise.resolve(false);

        if (this._savePromise === null) {
            // Delay saves for a bit so we can aggregate multiple saves that happen
            // in quick succession (eg. when a whole room's devices are marked as known)
            this._savePromise = Promise.delay(500).then(() => {
                console.log('Saving device tracking data at token ' + this._syncToken);
                // null out savePromise now (after the delay but before the write),
                // otherwise we could return the existing promise when the save has
                // actually already happened. Likewise for the dirty flag.
                this._savePromise = null;
                this._dirty = false;
                return this._cryptoStore.doTxn(
                    'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
                        this._cryptoStore.storeEndToEndDeviceData({
                            devices: this._devices,
                            trackingStatus: this._deviceTrackingStatus,
                            syncToken: this._syncToken,
                        }, txn);
                    },
                );
            }).then(() => {
                return true;
            });
        }
        return this._savePromise;
    }

    /**
     * Gets the sync token last set with setSyncToken
     *
     * @return {string} The sync token
     */
    getSyncToken() {
        return this._syncToken;
    }

    /**
     * Sets the sync token that the app will pass as the 'since' to the /sync
     * endpoint next time it syncs.
{'fraction_non_alphanumeric': 0.10281050483775422, 'fraction_numerical': 0.00263279141709998, 'mean_word_length': 1.5122354497354498, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1648513', 'n_tokens_mistral': 3773, 'n_tokens_neox': 3619, 'n_words': 1116}