A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
at main 164 lines 4.3 kB view raw
1import SqliteDb from "better-sqlite3"; 2import chalk from "chalk"; 3import type { Context } from "context"; 4import { 5 Kysely, 6 type Migration, 7 type MigrationProvider, 8 Migrator, 9 SqliteDialect, 10} from "kysely"; 11import { createAgent } from "lib/agent"; 12import { consola } from "consola"; 13 14// Types 15 16export type DatabaseSchema = { 17 status: Status; 18 auth_session: AuthSession; 19 auth_state: AuthState; 20}; 21 22export type Status = { 23 uri: string; 24 authorDid: string; 25 status: string; 26 createdAt: string; 27 indexedAt: string; 28}; 29 30export type AuthSession = { 31 key: string; 32 session: AuthSessionJson; 33 expiresAt?: string | null; 34}; 35 36export type AuthState = { 37 key: string; 38 state: AuthStateJson; 39}; 40 41type AuthStateJson = string; 42 43type AuthSessionJson = string; 44 45// Migrations 46 47const migrations: Record<string, Migration> = {}; 48 49const migrationProvider: MigrationProvider = { 50 async getMigrations() { 51 return migrations; 52 }, 53}; 54 55migrations["001"] = { 56 async up(db: Kysely<unknown>) { 57 await db.schema 58 .createTable("status") 59 .addColumn("uri", "varchar", (col) => col.primaryKey()) 60 .addColumn("authorDid", "varchar", (col) => col.notNull()) 61 .addColumn("status", "varchar", (col) => col.notNull()) 62 .addColumn("createdAt", "varchar", (col) => col.notNull()) 63 .addColumn("indexedAt", "varchar", (col) => col.notNull()) 64 .execute(); 65 await db.schema 66 .createTable("auth_session") 67 .addColumn("key", "varchar", (col) => col.primaryKey()) 68 .addColumn("session", "varchar", (col) => col.notNull()) 69 .execute(); 70 await db.schema 71 .createTable("auth_state") 72 .addColumn("key", "varchar", (col) => col.primaryKey()) 73 .addColumn("state", "varchar", (col) => col.notNull()) 74 .execute(); 75 }, 76 async down(db: Kysely<unknown>) { 77 await db.schema.dropTable("auth_state").execute(); 78 await db.schema.dropTable("auth_session").execute(); 79 await db.schema.dropTable("status").execute(); 80 }, 81}; 82 83migrations["002"] = { 84 async up(db: Kysely<unknown>) { 85 await db.schema 86 .alterTable("auth_session") 87 .addColumn("expiresAt", "text", (col) => col.defaultTo("NULL")) 88 .execute(); 89 }, 90 async down(db: Kysely<unknown>) { 91 await db.schema 92 .alterTable("auth_session") 93 .dropColumn("expiresAt") 94 .execute(); 95 }, 96}; 97 98// APIs 99 100export const createDb = (location: string): Database => { 101 return new Kysely<DatabaseSchema>({ 102 dialect: new SqliteDialect({ 103 database: new SqliteDb(location), 104 }), 105 }); 106}; 107 108export const migrateToLatest = async (db: Database) => { 109 const migrator = new Migrator({ db, provider: migrationProvider }); 110 const { error } = await migrator.migrateToLatest(); 111 if (error) throw error; 112}; 113 114export const updateExpiresAt = async (db: Database) => { 115 // get all sessions that have expiresAt is null 116 const sessions = await db.selectFrom("auth_session").selectAll().execute(); 117 consola.info("Found", sessions.length, "sessions to update"); 118 for (const session of sessions) { 119 const data = JSON.parse(session.session) as { 120 tokenSet: { expires_at?: string | null }; 121 }; 122 consola.info(session.key, data.tokenSet.expires_at); 123 await db 124 .updateTable("auth_session") 125 .set({ expiresAt: data.tokenSet.expires_at }) 126 .where("key", "=", session.key) 127 .execute(); 128 } 129 130 consola.info(`Updated ${chalk.greenBright(sessions.length)} sessions`); 131}; 132 133export const refreshSessionsAboutToExpire = async ( 134 db: Database, 135 ctx: Context, 136) => { 137 const now = new Date().toISOString(); 138 139 const sessions = await db 140 .selectFrom("auth_session") 141 .selectAll() 142 .where("expiresAt", "is not", "NULL") 143 .where("expiresAt", ">", now) 144 .orderBy("expiresAt", "asc") 145 .execute(); 146 147 for (const session of sessions) { 148 consola.info( 149 "Session about to expire:", 150 chalk.cyan(session.key), 151 session.expiresAt, 152 ); 153 const agent = await createAgent(ctx.oauthClient, session.key); 154 // Trigger a token refresh by fetching preferences 155 await agent.getPreferences(); 156 await new Promise((r) => setTimeout(r, 200)); 157 } 158 159 consola.info( 160 `Found ${chalk.yellowBright(sessions.length)} sessions to refresh`, 161 ); 162}; 163 164export type Database = Kysely<DatabaseSchema>;