import { app } from "electron"; import { EventEmitter } from "events"; import { createConnection, Connection } from "typeorm"; import { Server } from "@server"; import { isEmpty, isNotEmpty } from "@server/helpers/utils"; import { Config, Alert, Device, Queue, Webhook, Contact, ContactAddress } from "./entity"; import { DEFAULT_DB_ITEMS } from "./constants"; export type ServerConfig = { [key: string]: Date | string | boolean | number }; export type ServerConfigChange = { prevConfig: ServerConfig; nextConfig: ServerConfig }; export class ServerRepository extends EventEmitter { db: Connection = null; config: ServerConfig; constructor() { super(); this.db = null; this.config = {}; } async initialize(): Promise<Connection> { const isDev = process.env.NODE_ENV !== "production"; // If the DB is set, but not connected, try to connect if (this.db) { if (!this.db.isConnected) await this.db.connect(); return this.db; } let dbPath = `${app.getPath("userData")}/config.db`; if (isDev) { dbPath = `${app.getPath("userData")}/bluebubbles-server/config.db`; } this.db = await createConnection({ name: "config", type: "better-sqlite3", database: dbPath, entities: [Config, Alert, Device, Queue, Webhook, Contact, ContactAddress], // We should really use migrations for this. // This is me being lazy. Maybe someday... synchronize: true }); // Load default config items await this.loadConfig(); await this.setupDefaults(); return this.db; } /** * Get the device repo */ devices() { return this.db.getRepository(Device); } /** * Get the alert repo */ alerts() { return this.db.getRepository(Alert); } /** * Get the device repo */ queue() { return this.db.getRepository(Queue); } /** * Get the device repo */ configs() { return this.db.getRepository(Config); } /** * Get the webhooks repo */ webhooks() { return this.db.getRepository(Webhook); } /** * Get the contacts repo */ contacts() { return this.db.getRepository(Contact); } /** * Get the contact addresses repo */ contactAddresses() { return this.db.getRepository(ContactAddress); } private async loadConfig() { const items: Config[] = await this.configs().find(); for (const i of items) this.config[i.name] = ServerRepository.convertFromDbValue(i.value); } /** * Checks if the config has an item * * @param name The name of the item to check for */ hasConfig(name: string): boolean { return Object.keys(this.config).includes(name); } /** * Retrieves a config item from the cache * * @param name The name of the config item */ getConfig(name: string): Date | string | boolean | number { if (!Object.keys(this.config).includes(name)) return null; return ServerRepository.convertFromDbValue(this.config[name] as any); } /** * Sets a config item in the database * * @param name The name of the config item * @param value The value for the config item */ async setConfig(name: string, value: Date | string | boolean | number): Promise<void> { const orig = { ...this.config }; const saniVal = ServerRepository.convertToDbValue(value); const item = await this.configs().findOneBy({ name }); // Either change or create the new Config object if (item) { await this.configs().update(item, { value: saniVal }); } else { const cfg = this.configs().create({ name, value: saniVal }); await this.configs().save(cfg); } this.config[name] = ServerRepository.convertFromDbValue(saniVal); super.emit("config-update", { prevConfig: orig, nextConfig: this.config }); } async purgeOldDevices() { // Get devices that have a null last_active or older than 7 days const sevenDaysAgo = new Date().getTime() - 86400 * 1000 * 7; // Now - 7 days const devicesToDelete = (await this.devices().find()).filter( (item: Device) => !item.last_active || (item.last_active && item.last_active <= sevenDaysAgo) ); // Delete the devices if (isNotEmpty(devicesToDelete)) { Server().log(`Automatically purging ${devicesToDelete.length} devices from your server`); for (const item of devicesToDelete) { const dateStr = item.last_active ? new Date(item.last_active).toLocaleDateString() : "N/A"; Server().log(` -> Device: ${item.name} (Last Active: ${dateStr})`); await this.devices().delete({ name: item.name, identifier: item.identifier }); } } } public async getWebhooks(): Promise<Array<Webhook>> { const repo = this.webhooks(); return await repo.find(); } public async getContacts(withAvatars = false): Promise<Array<Contact>> { const repo = this.contacts(); const fields: (keyof Contact)[] = ["firstName", "lastName", "displayName", "id"]; if (withAvatars) { fields.push("avatar"); } return await repo.find({ select: fields, relations: ["addresses"] }); } public async addWebhook(url: string, events: Array<{ label: string; value: string }>): Promise<Webhook> { const repo = this.webhooks(); const item = await repo.findOneBy({ url }); // If the webhook exists, don't re-add it, just return it if (item) return item; const webhook = repo.create({ url, events: JSON.stringify(events.map(e => e.value)) }); return await repo.save(webhook); } public async updateWebhook({ id, url = null, events = null }: { id: number; url: string; events: Array<{ label: string; value: string }>; }): Promise<Webhook> { const repo = this.webhooks(); const item = await repo.findOneBy({ id }); if (!item) throw new Error("Failed to update webhook! Existing webhook does not exist!"); if (url) item.url = url; if (events) item.events = JSON.stringify(events.map(e => e.value)); await repo.update(id, item); return item; } public async deleteWebhook({ url = null, id = null }: { url: string | null; id: number | null }): Promise<void> { const repo = this.webhooks(); const item = url ? await repo.findOneBy({ url }) : await repo.findOneBy({ id }); if (!item) return; await repo.delete(item.id); } public async hasQueuedMessage(tempGuid: string): Promise<boolean> { const repo = this.queue(); // Get all queued items let entries = await repo.find(); if (isEmpty(entries)) return false; // Check if any have a matching tempGUID entries = entries.filter(item => item.tempGuid === tempGuid || item.text.startsWith(tempGuid)); // Return if there are tempGUID matches return isNotEmpty(entries); } /** * This sets any default database values, if the database * has not already been initialized */ private async setupDefaults(): Promise<void> { try { for (const key of Object.keys(DEFAULT_DB_ITEMS)) { const item = await this.hasConfig(key); if (!item) await this.setConfig(key, DEFAULT_DB_ITEMS[key]()); } } catch (ex: any) { Server().log(`Failed to setup default configurations! ${ex.message}`, "error"); } } /** * Converts a generic string value from the database * to its' corresponding correct typed value * * @param input The value straight from the database */ private static convertFromDbValue(input: string): any { if (input === "1" || input === "0") return Boolean(Number(input)); if (/^-{0,1}\d+$/.test(input)) return Number(input); return input; } /** * Converts a typed database value input to a string. * * @param input The typed database value */ private static convertToDbValue(input: any): string { if (typeof input === "boolean") return input ? "1" : "0"; if (input instanceof Date) return String(input.getTime()); return String(input); } }