"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.deleteOfflineNodes = exports.sendMonitoringMails = exports.retrieveNodeInformation = exports.disable = exports.confirm = exports.getByMacs = exports.getAll = exports.parseNodesJson = exports.parseNode = exports.parseTimestamp = void 0;
const lodash_1 = __importDefault(require("lodash"));
const moment_1 = __importDefault(require("moment"));
const request_1 = __importDefault(require("request"));
const config_1 = require("../config");
const database_1 = require("../db/database");
const DatabaseUtil = __importStar(require("../utils/databaseUtil"));
const errorTypes_1 = __importDefault(require("../utils/errorTypes"));
const logger_1 = __importDefault(require("../logger"));
const MailService = __importStar(require("../services/mailService"));
const NodeService = __importStar(require("../services/nodeService"));
const Resources = __importStar(require("../utils/resources"));
const strings_1 = require("../utils/strings");
const urlBuilder_1 = require("../utils/urlBuilder");
const constraints_1 = __importDefault(require("../validation/constraints"));
const validator_1 = require("../validation/validator");
const types_1 = require("../types");
const MONITORING_STATE_MACS_CHUNK_SIZE = 100;
const NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE = 20;
const MONITORING_MAILS_DB_BATCH_SIZE = 50;
/**
 * Defines the intervals emails are sent if a node is offline
 */
const MONITORING_OFFLINE_MAILS_SCHEDULE = {
    1: { amount: 3, unit: 'hours' },
    2: { amount: 1, unit: 'days' },
    3: { amount: 7, unit: 'days' }
};
const DELETE_OFFLINE_NODES_AFTER_DURATION = {
    amount: 100,
    unit: 'days'
};
let previousImportTimestamp = null;
async function insertNodeInformation(nodeData, node) {
    logger_1.default
        .tag('monitoring', 'information-retrieval')
        .debug('Node is new in monitoring, creating data: %s', nodeData.mac);
    await database_1.db.run('INSERT INTO node_state ' +
        '(hostname, mac, site, domain, monitoring_state, state, last_seen, import_timestamp, last_status_mail_sent, last_status_mail_type) ' +
        'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', [
        node.hostname,
        node.mac,
        nodeData.site,
        nodeData.domain,
        node.monitoringState,
        nodeData.state,
        nodeData.lastSeen.unix(),
        nodeData.importTimestamp.unix(),
        null,
        null // new node so we haven't send a mail yet
    ]);
}
async function updateNodeInformation(nodeData, node, row) {
    logger_1.default
        .tag('monitoring', 'informacallbacktion-retrieval')
        .debug('Node is known in monitoring: %s', nodeData.mac);
    if (!moment_1.default(row.import_timestamp).isBefore(nodeData.importTimestamp)) {
        logger_1.default
            .tag('monitoring', 'information-retrieval')
            .debug('No new data for node, skipping: %s', nodeData.mac);
        return;
    }
    logger_1.default
        .tag('monitoring', 'information-retrieval')
        .debug('New data for node, updating: %s', nodeData.mac);
    await database_1.db.run('UPDATE node_state ' +
        'SET ' +
        'hostname = ?, ' +
        'site = ?, ' +
        'domain = ?, ' +
        'monitoring_state = ?, ' +
        'state = ?, ' +
        'last_seen = ?, ' +
        'import_timestamp = ?, ' +
        'modified_at = ? ' +
        'WHERE id = ? AND mac = ?', [
        node.hostname,
        nodeData.site || row.site,
        nodeData.domain || row.domain,
        node.monitoringState,
        nodeData.state,
        nodeData.lastSeen.unix(),
        nodeData.importTimestamp.unix(),
        moment_1.default().unix(),
        row.id,
        node.mac
    ]);
}
async function storeNodeInformation(nodeData, node) {
    logger_1.default.tag('monitoring', 'information-retrieval').debug('Storing status for node: %s', nodeData.mac);
    const row = await database_1.db.get('SELECT * FROM node_state WHERE mac = ?', [node.mac]);
    if (lodash_1.default.isUndefined(row)) {
        return await insertNodeInformation(nodeData, node);
    }
    else {
        return await updateNodeInformation(nodeData, node, row);
    }
}
const isValidMac = validator_1.forConstraint(constraints_1.default.node.mac, false);
function parseTimestamp(timestamp) {
    if (!lodash_1.default.isString(timestamp)) {
        return moment_1.default.invalid();
    }
    return moment_1.default.utc(timestamp);
}
exports.parseTimestamp = parseTimestamp;
// TODO: Use sparkson for JSON parsing.
function parseNode(importTimestamp, nodeData) {
    if (!lodash_1.default.isPlainObject(nodeData)) {
        throw new Error('Unexpected node type: ' + (typeof nodeData));
    }
    if (!lodash_1.default.isPlainObject(nodeData.nodeinfo)) {
        throw new Error('Unexpected nodeinfo type: ' + (typeof nodeData.nodeinfo));
    }
    const nodeId = nodeData.nodeinfo.node_id;
    if (!nodeId || !lodash_1.default.isString(nodeId)) {
        throw new Error(`Invalid node id of type "${typeof nodeId}": ${nodeId}`);
    }
    if (!lodash_1.default.isPlainObject(nodeData.nodeinfo.network)) {
        throw new Error('Node ' + nodeId + ': Unexpected nodeinfo.network type: ' + (typeof nodeData.nodeinfo.network));
    }
    if (!isValidMac(nodeData.nodeinfo.network.mac)) {
        throw new Error('Node ' + nodeId + ': Invalid MAC: ' + nodeData.nodeinfo.network.mac);
    }
    const mac = strings_1.normalizeMac(nodeData.nodeinfo.network.mac);
    if (!lodash_1.default.isPlainObject(nodeData.flags)) {
        throw new Error('Node ' + nodeId + ': Unexpected flags type: ' + (typeof nodeData.flags));
    }
    if (!lodash_1.default.isBoolean(nodeData.flags.online)) {
        throw new Error('Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online));
    }
    const isOnline = nodeData.flags.online;
    const lastSeen = parseTimestamp(nodeData.lastseen);
    if (!lastSeen.isValid()) {
        throw new Error('Node ' + nodeId + ': Invalid lastseen timestamp: ' + nodeData.lastseen);
    }
    let site = null;
    if (lodash_1.default.isPlainObject(nodeData.nodeinfo.system) && lodash_1.default.isString(nodeData.nodeinfo.system.site_code)) {
        site = nodeData.nodeinfo.system.site_code;
    }
    let domain = null;
    if (lodash_1.default.isPlainObject(nodeData.nodeinfo.system) && lodash_1.default.isString(nodeData.nodeinfo.system.domain_code)) {
        domain = nodeData.nodeinfo.system.domain_code;
    }
    return {
        mac: mac,
        importTimestamp: importTimestamp,
        state: isOnline ? types_1.NodeState.ONLINE : types_1.NodeState.OFFLINE,
        lastSeen: lastSeen,
        site: site || '<unknown-site>',
        domain: domain || '<unknown-domain>'
    };
}
exports.parseNode = parseNode;
// TODO: Use sparkson for JSON parsing.
function parseNodesJson(body) {
    logger_1.default.tag('monitoring', 'information-retrieval').debug('Parsing nodes.json...');
    const json = JSON.parse(body);
    if (!lodash_1.default.isPlainObject(json)) {
        throw new Error(`Expecting a JSON object as the nodes.json root, but got: ${typeof json}`);
    }
    const expectedVersion = 2;
    if (json.version !== expectedVersion) {
        throw new Error(`Unexpected nodes.json version "${json.version}". Expected: "${expectedVersion}"`);
    }
    const result = {
        importTimestamp: parseTimestamp(json.timestamp),
        nodes: [],
        failedNodesCount: 0,
        totalNodesCount: 0,
    };
    if (!result.importTimestamp.isValid()) {
        throw new Error('Invalid timestamp: ' + json.timestamp);
    }
    if (!lodash_1.default.isArray(json.nodes)) {
        throw new Error('Invalid nodes array type: ' + (typeof json.nodes));
    }
    for (const nodeData of json.nodes) {
        result.totalNodesCount += 1;
        try {
            const parsedNode = parseNode(result.importTimestamp, nodeData);
            logger_1.default.tag('monitoring', 'parsing-nodes-json').debug(`Parsing node successful: ${parsedNode.mac}`);
            result.nodes.push(parsedNode);
        }
        catch (error) {
            result.failedNodesCount += 1;
            logger_1.default.tag('monitoring', 'parsing-nodes-json').error("Could not parse node.", error, nodeData);
        }
    }
    return result;
}
exports.parseNodesJson = parseNodesJson;
async function updateSkippedNode(id, node) {
    return await database_1.db.run('UPDATE node_state ' +
        'SET hostname = ?, monitoring_state = ?, modified_at = ?' +
        'WHERE id = ?', [
        node ? node.hostname : '', node ? node.monitoringState : '', moment_1.default().unix(),
        id
    ]);
}
async function sendMonitoringMailsBatched(name, mailType, findBatchFun) {
    logger_1.default.tag('monitoring', 'mail-sending').debug('Sending "%s" mails...', name);
    while (true) {
        logger_1.default.tag('monitoring', 'mail-sending').debug('Sending next batch...');
        const nodeStates = await findBatchFun();
        if (lodash_1.default.isEmpty(nodeStates)) {
            logger_1.default.tag('monitoring', 'mail-sending').debug('Done sending "%s" mails.', name);
            return;
        }
        for (const nodeState of nodeStates) {
            const mac = nodeState.mac;
            logger_1.default.tag('monitoring', 'mail-sending').debug('Loading node data for: %s', mac);
            const result = await NodeService.getNodeDataWithSecretsByMac(mac);
            if (!result) {
                logger_1.default
                    .tag('monitoring', 'mail-sending')
                    .debug('Node not found. Skipping sending of "' + name + '" mail: ' + mac);
                await updateSkippedNode(nodeState.id);
                continue;
            }
            const { node, nodeSecrets } = result;
            if (!(node.monitoring && node.monitoringConfirmed)) {
                logger_1.default
                    .tag('monitoring', 'mail-sending')
                    .debug('Monitoring disabled, skipping "%s" mail for: %s', name, mac);
                await updateSkippedNode(nodeState.id);
                continue;
            }
            const monitoringToken = nodeSecrets.monitoringToken;
            if (!monitoringToken) {
                logger_1.default
                    .tag('monitoring', 'mail-sending')
                    .error('Node has no monitoring token. Cannot send mail "%s" for: %s', name, mac);
                await updateSkippedNode(nodeState.id);
                continue;
            }
            logger_1.default
                .tag('monitoring', 'mail-sending')
                .info('Sending "%s" mail for: %s', name, mac);
            await MailService.enqueue(config_1.config.server.email.from, node.nickname + ' <' + node.email + '>', mailType, {
                node: node,
                lastSeen: nodeState.last_seen,
                disableUrl: urlBuilder_1.monitoringDisableUrl(monitoringToken)
            });
            logger_1.default
                .tag('monitoring', 'mail-sending')
                .debug('Updating node state: ', mac);
            const now = moment_1.default().unix();
            await database_1.db.run('UPDATE node_state ' +
                'SET hostname = ?, monitoring_state = ?, modified_at = ?, last_status_mail_sent = ?, last_status_mail_type = ?' +
                'WHERE id = ?', [
                node.hostname, node.monitoringState, now, now, mailType,
                nodeState.id
            ]);
        }
    }
}
async function sendOnlineAgainMails(startTime) {
    await sendMonitoringMailsBatched('online again', 'monitoring-online-again', async () => await database_1.db.all('SELECT * FROM node_state ' +
        'WHERE modified_at < ? AND state = ? AND last_status_mail_type IN (' +
        '\'monitoring-offline-1\', \'monitoring-offline-2\', \'monitoring-offline-3\'' +
        ')' +
        'ORDER BY id ASC LIMIT ?', [
        startTime.unix(),
        'ONLINE',
        MONITORING_MAILS_DB_BATCH_SIZE
    ]));
}
async function sendOfflineMails(startTime, mailNumber) {
    await sendMonitoringMailsBatched('offline ' + mailNumber, 'monitoring-offline-' + mailNumber, async () => {
        const previousType = mailNumber === 1 ? 'monitoring-online-again' : ('monitoring-offline-' + (mailNumber - 1));
        // the first time the first offline mail is send, there was no mail before
        const allowNull = mailNumber === 1 ? ' OR last_status_mail_type IS NULL' : '';
        const schedule = MONITORING_OFFLINE_MAILS_SCHEDULE[mailNumber];
        const scheduledTimeBefore = moment_1.default().subtract(schedule.amount, schedule.unit);
        return await database_1.db.all('SELECT * FROM node_state ' +
            'WHERE modified_at < ? AND state = ? AND (last_status_mail_type = ?' + allowNull + ') AND ' +
            'last_seen <= ? AND (last_status_mail_sent <= ? OR last_status_mail_sent IS NULL) ' +
            'ORDER BY id ASC LIMIT ?', [
            startTime.unix(),
            'OFFLINE',
            previousType,
            scheduledTimeBefore.unix(),
            scheduledTimeBefore.unix(),
            MONITORING_MAILS_DB_BATCH_SIZE
        ]);
    });
}
function doRequest(url) {
    return new Promise((resolve, reject) => {
        request_1.default(url, function (err, response, body) {
            if (err) {
                return reject(err);
            }
            resolve({ response, body });
        });
    });
}
async function withUrlsData(urls) {
    const results = [];
    for (const url of urls) {
        logger_1.default.tag('monitoring', 'information-retrieval').debug('Retrieving nodes.json: %s', url);
        const { response, body } = await doRequest(url);
        if (response.statusCode !== 200) {
            throw new Error('Could not download nodes.json from ' + url + ': ' +
                response.statusCode + ' - ' + response.statusMessage);
        }
        results.push(await parseNodesJson(body));
    }
    return results;
}
async function retrieveNodeInformationForUrls(urls) {
    const datas = await withUrlsData(urls);
    let maxTimestamp = datas[0].importTimestamp;
    let minTimestamp = maxTimestamp;
    let failedParsingNodesCount = 0;
    let totalNodesCount = 0;
    for (const data of datas) {
        if (data.importTimestamp.isAfter(maxTimestamp)) {
            maxTimestamp = data.importTimestamp;
        }
        if (data.importTimestamp.isBefore(minTimestamp)) {
            minTimestamp = data.importTimestamp;
        }
        failedParsingNodesCount += data.failedNodesCount;
        totalNodesCount += data.totalNodesCount;
    }
    if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) {
        logger_1.default
            .tag('monitoring', 'information-retrieval')
            .debug('No new data, skipping. Current timestamp: %s, previous timestamp: %s', maxTimestamp.format(), previousImportTimestamp.format());
        return {
            failedParsingNodesCount,
            totalNodesCount,
        };
    }
    previousImportTimestamp = maxTimestamp;
    // We do not parallelize here as the sqlite will start slowing down and blocking with too many
    // parallel queries. This has resulted in blocking other requests too and thus in a major slowdown.
    const allNodes = lodash_1.default.flatMap(datas, data => data.nodes);
    // Get rid of duplicates from different nodes.json files. Always use the one with the newest
    const sortedNodes = lodash_1.default.orderBy(allNodes, [node => node.lastSeen.unix()], ['desc']);
    const uniqueNodes = lodash_1.default.uniqBy(sortedNodes, function (node) {
        return node.mac;
    });
    for (const nodeData of uniqueNodes) {
        logger_1.default.tag('monitoring', 'information-retrieval').debug('Importing: %s', nodeData.mac);
        const result = await NodeService.getNodeDataByMac(nodeData.mac);
        if (!result) {
            logger_1.default
                .tag('monitoring', 'information-retrieval')
                .debug('Unknown node, skipping: %s', nodeData.mac);
            continue;
        }
        await storeNodeInformation(nodeData, result);
        logger_1.default
            .tag('monitoring', 'information-retrieval')
            .debug('Updating / deleting node data done: %s', nodeData.mac);
    }
    logger_1.default
        .tag('monitoring', 'information-retrieval')
        .debug('Marking missing nodes as offline.');
    // Mark nodes as offline that haven't been imported in this run.
    await database_1.db.run('UPDATE node_state ' +
        'SET state = ?, modified_at = ?' +
        'WHERE import_timestamp < ?', [
        types_1.NodeState.OFFLINE, moment_1.default().unix(),
        minTimestamp.unix()
    ]);
    return {
        failedParsingNodesCount,
        totalNodesCount,
    };
}
async function getAll(restParams) {
    const sortFields = [
        'id',
        'hostname',
        'mac',
        'site',
        'domain',
        'monitoring_state',
        'state',
        'last_seen',
        'import_timestamp',
        'last_status_mail_type',
        'last_status_mail_sent',
        'created_at',
        'modified_at'
    ];
    const filterFields = [
        'hostname',
        'mac',
        'monitoring_state',
        'state',
        'last_status_mail_type'
    ];
    const where = Resources.whereCondition(restParams, filterFields);
    const row = await database_1.db.get('SELECT count(*) AS total FROM node_state WHERE ' + where.query, lodash_1.default.concat([], where.params));
    const total = row.total;
    const filter = Resources.filterClause(restParams, 'id', sortFields, filterFields);
    const monitoringStates = await database_1.db.all('SELECT * FROM node_state WHERE ' + filter.query, lodash_1.default.concat([], filter.params));
    return { monitoringStates, total };
}
exports.getAll = getAll;
async function getByMacs(macs) {
    if (lodash_1.default.isEmpty(macs)) {
        return {};
    }
    const nodeStateByMac = {};
    for (const subMacs of lodash_1.default.chunk(macs, MONITORING_STATE_MACS_CHUNK_SIZE)) {
        const inCondition = DatabaseUtil.inCondition('mac', subMacs);
        const rows = await database_1.db.all('SELECT * FROM node_state WHERE ' + inCondition.query, lodash_1.default.concat([], inCondition.params));
        for (const row of rows) {
            nodeStateByMac[row.mac] = row;
        }
    }
    return nodeStateByMac;
}
exports.getByMacs = getByMacs;
async function confirm(token) {
    const { node, nodeSecrets } = await NodeService.getNodeDataWithSecretsByMonitoringToken(token);
    if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) {
        throw { data: 'Invalid token.', type: errorTypes_1.default.badRequest };
    }
    if (node.monitoringConfirmed) {
        return node;
    }
    node.monitoringConfirmed = true;
    const { node: newNode } = await NodeService.internalUpdateNode(node.token, node, nodeSecrets);
    return newNode;
}
exports.confirm = confirm;
async function disable(token) {
    const { node, nodeSecrets } = await NodeService.getNodeDataWithSecretsByMonitoringToken(token);
    if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) {
        throw { data: 'Invalid token.', type: errorTypes_1.default.badRequest };
    }
    node.monitoring = false;
    node.monitoringConfirmed = false;
    nodeSecrets.monitoringToken = '';
    const { node: newNode } = await NodeService.internalUpdateNode(node.token, node, nodeSecrets);
    return newNode;
}
exports.disable = disable;
async function retrieveNodeInformation() {
    const urls = config_1.config.server.map.nodesJsonUrl;
    if (lodash_1.default.isEmpty(urls)) {
        throw new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl');
    }
    return await retrieveNodeInformationForUrls(urls);
}
exports.retrieveNodeInformation = retrieveNodeInformation;
async function sendMonitoringMails() {
    logger_1.default.tag('monitoring', 'mail-sending').debug('Sending monitoring mails...');
    const startTime = moment_1.default();
    try {
        await sendOnlineAgainMails(startTime);
    }
    catch (error) {
        // only logging an continuing with next type
        logger_1.default
            .tag('monitoring', 'mail-sending')
            .error('Error sending "online again" mails.', error);
    }
    for (let mailNumber = 1; mailNumber <= 3; mailNumber++) {
        try {
            await sendOfflineMails(startTime, mailNumber);
        }
        catch (error) {
            // only logging an continuing with next type
            logger_1.default
                .tag('monitoring', 'mail-sending')
                .error('Error sending "offline ' + mailNumber + '" mails.', error);
        }
    }
}
exports.sendMonitoringMails = sendMonitoringMails;
async function deleteOfflineNodes() {
    logger_1.default
        .tag('nodes', 'delete-offline')
        .info('Deleting offline nodes older than ' +
        DELETE_OFFLINE_NODES_AFTER_DURATION.amount + ' ' +
        DELETE_OFFLINE_NODES_AFTER_DURATION.unit);
    const deleteBefore = moment_1.default().subtract(DELETE_OFFLINE_NODES_AFTER_DURATION.amount, DELETE_OFFLINE_NODES_AFTER_DURATION.unit).unix();
    await deleteNeverOnlineNodesBefore(deleteBefore);
    await deleteNodesOfflineSinceBefore(deleteBefore);
}
exports.deleteOfflineNodes = deleteOfflineNodes;
async function deleteNeverOnlineNodesBefore(deleteBefore) {
    logger_1.default
        .tag('nodes', 'delete-never-online')
        .info('Deleting nodes that were never online created befor ' +
        deleteBefore);
    const deletionCandidates = await NodeService.findNodesModifiedBefore(deleteBefore);
    logger_1.default
        .tag('nodes', 'delete-never-online')
        .info('Number of nodes created before ' +
        deleteBefore +
        ': ' +
        deletionCandidates.length);
    const deletionCandidateMacs = lodash_1.default.map(deletionCandidates, node => node.mac);
    const chunks = lodash_1.default.chunk(deletionCandidateMacs, NEVER_ONLINE_NODES_DELETION_CHUNK_SIZE);
    logger_1.default
        .tag('nodes', 'delete-never-online')
        .info('Number of chunks to check for deletion: ' +
        chunks.length);
    for (const macs of chunks) {
        logger_1.default
            .tag('nodes', 'delete-never-online')
            .info('Checking chunk of ' +
            macs.length +
            ' MACs for deletion.');
        const placeholders = lodash_1.default.join(lodash_1.default.map(macs, () => '?'), ',');
        const rows = await database_1.db.all(`SELECT * FROM node_state WHERE mac IN (${placeholders})`, macs);
        logger_1.default
            .tag('nodes', 'delete-never-online')
            .info('Of the chunk of ' +
            macs.length +
            ' MACs there were ' +
            rows.length +
            ' nodes found in monitoring database. Those should be skipped.');
        const seenMacs = lodash_1.default.map(rows, (row) => row.mac);
        const neverSeenMacs = lodash_1.default.difference(macs, seenMacs);
        logger_1.default
            .tag('nodes', 'delete-never-online')
            .info('Of the chunk of ' +
            macs.length +
            ' MACs there are ' +
            neverSeenMacs +
            ' nodes that were never online. Those will be deleted.');
        for (const neverSeenMac of neverSeenMacs) {
            await deleteNodeByMac(neverSeenMac);
        }
    }
}
async function deleteNodesOfflineSinceBefore(deleteBefore) {
    const rows = await database_1.db.all('SELECT * FROM node_state WHERE state = ? AND last_seen < ?', [
        'OFFLINE',
        deleteBefore
    ]);
    for (const row of rows) {
        await deleteNodeByMac(row.mac);
    }
}
async function deleteNodeByMac(mac) {
    logger_1.default.tag('nodes', 'delete-offline').debug('Deleting node ' + mac);
    let node;
    try {
        node = await NodeService.getNodeDataByMac(mac);
    }
    catch (error) {
        // Only log error. We try to delete the nodes state anyways.
        logger_1.default.tag('nodes', 'delete-offline').error('Could not find node to delete: ' + mac, error);
    }
    if (node && node.token) {
        await NodeService.deleteNode(node.token);
    }
    try {
        await database_1.db.run('DELETE FROM node_state WHERE mac = ? AND state = ?', [mac, 'OFFLINE']);
    }
    catch (error) {
        // Only log error and continue with next node.
        logger_1.default.tag('nodes', 'delete-offline').error('Could not delete node state: ' + mac, error);
    }
}
//# sourceMappingURL=monitoringService.js.map