/ src / modules / db.ts
db.ts
  1  import fs from 'fs';
  2  import sqlite3 from 'better-sqlite3';
  3  import { createHash } from 'crypto';
  4  import { ActivityType, CommandInteraction, StringSelectMenuInteraction } from 'discord.js';
  5  import { Locale, log } from './messages';
  6  
  7  export interface DBUser {
  8    userIDHash: string;
  9    autoRole: 1 | 0;
 10    language: Locale | 'none';
 11  }
 12  
 13  export interface DBGuild {
 14    guildID: string;
 15    language: Locale;
 16    requiredRoleID: string | null;
 17  }
 18  
 19  export interface DBActivityRole {
 20    guildID: string;
 21    activityName: string;
 22    roleID: string;
 23    exactActivityName: 1 | 0;
 24    live: 1 | 0;
 25  }
 26  
 27  export interface DBStatusRole {
 28    guildID: string;
 29    type: ActivityType;
 30    roleID: string;
 31  }
 32  
 33  export interface DBCurrentlyActiveActivity {
 34    userIDHash: string;
 35    guildID: string;
 36    activityName: string;
 37  }
 38  
 39  /// @deprecated use DBActivityStatus instead
 40  export interface DBActiveTemporaryRoles {
 41    userIDHash: string;
 42    guildId: string;
 43    roleID: string;
 44  }
 45  
 46  export interface DBActivityStats {
 47    guildID: string;
 48    activityName: string;
 49    count: number;
 50  }
 51  
 52  export interface DBVersion {
 53    version: number;
 54    enforcer: 0;
 55  }
 56  
 57  let db: sqlite3.Database;
 58  
 59  export const stats = {
 60    dbCalls: 0,
 61  }
 62  
 63  export function resetStats() {
 64    stats.dbCalls = 0;
 65  }
 66  
 67  export function prepare(query: string) {
 68    stats.dbCalls++;
 69    return db.prepare(query);
 70  }
 71  
 72  
 73  export function prepareDB() {
 74    if (!fs.existsSync('db')) fs.mkdirSync('db');
 75    db = new sqlite3('db/activity-roles.db');
 76  
 77    // `v1.9.1` live -> permanent: the database was not updated on purpose.
 78    // enforcer: see https://stackoverflow.com/a/3010975/16292720 (comment 4)
 79    prepare(
 80      'CREATE TABLE IF NOT EXISTS dbversion (version INT NOT NULL, enforcer INT DEFAULT 0 NOT NULL CHECK(enforcer == 0), UNIQUE (enforcer))'
 81    ).run();
 82    prepare(
 83      'CREATE TABLE IF NOT EXISTS users (userIDHash TEXT PRIMARY KEY, autoRole INTEGER, language TEXT)'
 84    ).run();
 85    prepare(
 86      'CREATE TABLE IF NOT EXISTS guilds (guildID TEXT PRIMARY KEY, language TEXT, requiredRoleID TEXT)'
 87    ).run();
 88    prepare(
 89      'CREATE TABLE IF NOT EXISTS activityRoles (guildID TEXT, activityName TEXT, roleID TEXT, exactActivityName INTEGER, live INTEGER, PRIMARY KEY (guildID, activityName, roleID))'
 90    ).run();
 91    prepare(
 92      'CREATE TABLE IF NOT EXISTS statusRoles (guildID TEXT, type INTEGER, roleID TEXT, PRIMARY KEY (guildID, type, roleID))'
 93    ).run();
 94    prepare(
 95      'CREATE TABLE IF NOT EXISTS currentlyActiveActivities (userIDHash TEXT, guildID TEXT, activityName TEXT, PRIMARY KEY (userIDHash, guildID, activityName))'
 96    ).run();
 97    prepare(
 98      'CREATE TABLE IF NOT EXISTS activeTemporaryRoles (userIDHash, guildID TEXT, roleID TEXT, PRIMARY KEY (userIDHash, guildID, roleID))'
 99    ).run();
100    prepare(
101      'CREATE TABLE IF NOT EXISTS activityStats (guildID TEXT, activityName TEXT, count INTEGER, PRIMARY KEY (guildID, activityName))'
102    ).run();
103  
104    const latestDBVersion = 3;
105    let dbVersion = latestDBVersion;
106  
107    if (!prepare('SELECT * FROM dbversion').get()) {
108      prepare('INSERT INTO dbversion (version) VALUES (?)').run(latestDBVersion);
109    } else {
110      dbVersion = (prepare('SELECT * FROM dbversion').get() as DBVersion).version;
111    }
112  
113    if (dbVersion === 1) {
114      // add status roles
115      // add activeTemporaryRoles
116      // fade out currentlyActiveActivites
117      prepare('UPDATE dbversion SET version = 2').run();
118      dbVersion = 2;
119    }
120    if (dbVersion === 2) {
121      prepare("DELETE FROM activityStats WHERE activityName = 'Custom Status'").run();
122      prepare('UPDATE dbversion SET version = 3').run();
123      dbVersion = 3;
124    }
125  
126    //TODO: add bot version?
127    if (dbVersion > latestDBVersion) {
128      log.warn(
129        `Database version: ${dbVersion}. The latest known database version is ${latestDBVersion}! Are you opening a database created with a newer version?`
130      );
131    } else {
132      log.info(`Database version: ${dbVersion}`);
133    }
134  }
135  
136  export function getUserConfig(userID: string): DBUser {
137    const userIDHash = createHash('sha256').update(userID).digest('base64');
138    const user = prepare('SELECT * FROM users WHERE userIDHash = ?').get(userIDHash) as DBUser;
139    if (user) return user;
140    prepare('INSERT INTO users VALUES (?, ?, ?)').run(userIDHash, 1, 'none');
141    return { userIDHash: userIDHash, autoRole: 1, language: 'none' };
142  }
143  
144  export function getGuildConfig(guildID: string): DBGuild {
145    const guild = prepare('SELECT * FROM guilds WHERE guildID = ?').get(guildID) as DBGuild;
146    if (guild) return guild;
147    prepare('INSERT INTO guilds VALUES (?, ?, NULL)').run(guildID, 'en-US');
148    return { guildID: guildID, language: 'en-US' as Locale, requiredRoleID: null };
149  }
150  
151  export function getActivityRoles(guildID: string): DBActivityRole[] {
152    return db
153      .prepare('SELECT * FROM activityRoles WHERE guildID = ?')
154      .all(guildID) as DBActivityRole[];
155  }
156  
157  export function getStatusRoles(guildID: string): DBStatusRole[] {
158    return prepare('SELECT * FROM statusRoles WHERE guildID = ?').all(guildID) as DBStatusRole[];
159  }
160  
161  export function getLang(interaction: CommandInteraction | StringSelectMenuInteraction): Locale {
162    const userLang = getUserConfig(interaction.user.id).language;
163    if (userLang !== 'none') return userLang;
164  
165    if (!interaction.guild) return 'en-US' as Locale;
166  
167    return getGuildConfig(interaction.guild.id).language;
168  }
169  
170  export async function addActivity(guildID: string, activityName: string) {
171    prepare(
172      'INSERT INTO activityStats VALUES (?, ?, ?) ON CONFLICT(guildID, activityName) DO UPDATE SET count = count + 1'
173    ).run(guildID, activityName, 1);
174  }
175  
176  export function getUserCount(): number {
177    return (prepare('SELECT COUNT(*) FROM users').get() as { 'COUNT(*)': number })['COUNT(*)'];
178  }
179  
180  export function getActivityRoleCount(): number {
181    return (prepare('SELECT COUNT(*) FROM activityRoles').get() as { 'COUNT(*)': number })['COUNT(*)'];
182  }
183  
184  export function getStatusRoleCount(): number {
185    return (prepare('SELECT COUNT(*) FROM statusRoles').get() as { 'COUNT(*)': number })['COUNT(*)'];
186  }
187  
188  export function getCurrentlyActiveActivityCount(): number {
189    return (prepare('SELECT COUNT(*) FROM currentlyActiveActivities').get() as { 'COUNT(*)': number })['COUNT(*)'];
190  }
191  
192  export function getTempRoleCount(): number {
193    return (prepare('SELECT COUNT(*) FROM activityRoles WHERE live = 1').get() as { 'COUNT(*)': number; })['COUNT(*)'];
194  }
195  
196  export function getPermRoleCount(): number {
197    return (prepare('SELECT COUNT(*) FROM activityRoles WHERE live = 0').get() as { 'COUNT(*)': number; })['COUNT(*)'];
198  }
199  
200  export function getRolesCount(): number {
201    return getActivityRoleCount() + getStatusRoleCount()
202  }