From e782c5d8ac0dd853dc2bfbd3a2314a3a8f33058a Mon Sep 17 00:00:00 2001 From: GW_MC <72297530+GWMCwing@users.noreply.github.com> Date: Fri, 23 Jan 2026 18:12:46 +0800 Subject: [PATCH] refactor token service --- src/discordService.ts | 134 +++++++++++++++++------------------------- src/tokenService.ts | 36 ++++++++++++ 2 files changed, 90 insertions(+), 80 deletions(-) create mode 100644 src/tokenService.ts diff --git a/src/discordService.ts b/src/discordService.ts index 1a7b66a..ab6ef20 100644 --- a/src/discordService.ts +++ b/src/discordService.ts @@ -1,51 +1,48 @@ import * as RPC from 'discord-rpc'; import axios from 'axios'; -import * as fs from 'fs'; -import * as path from 'path'; import { CONFIG } from './config'; import { VoiceSettingsPayload } from './types'; - -interface TokenData { - accessToken: string; - refreshToken: string; - expiresAt: number; // Unix ms timestamp -} +import { TokenData, TokenService } from './tokenService'; class DiscordService { - private client: RPC.Client; - private loggedIn = false; - private isLoginInProgress = false; - private voiceUpdateCallbacks: Array<(d: VoiceSettingsPayload) => void> = []; - - private static TOKEN_FILE = path.join(__dirname, '../', '.discord-token.json'); - constructor() { + this.initialize(); + } + + public onVoiceSettingsUpdate(cb: (d: VoiceSettingsPayload) => void) { + this.voiceUpdateCallbacks.push(cb); + } + + public initialize() { this.client = new RPC.Client({ transport: 'ipc' }); this.setupEvents(); } - private saveTokens(data: TokenData): void { - try { - fs.writeFileSync(DiscordService.TOKEN_FILE, JSON.stringify(data, null, 2)); - console.log('Tokens saved locally'); - } catch (err) { - console.error('Failed to save tokens:', err); + public async login() { + if (this.loggedIn || this.isLoginInProgress) return; + await this._login(); + // load tokens and setup refresh + const tokens = this.tokenService.loadTokens(); + if (tokens) { + const { refreshToken, expiresAt } = tokens; + if (this.refreshTimeout) { + clearTimeout(this.refreshTimeout); + } + this.refreshTimeout = setTimeout(() => { + console.log('Starting token refresh flow...'); + this.refreshTokenFlow(refreshToken).catch(console.error); + }, expiresAt - Date.now() - 60000); // refresh 1 min before expiry + } else { + console.warn('No tokens found after login; cannot schedule refresh.'); } } - private loadTokens(): TokenData | null { - try { - if (!fs.existsSync(DiscordService.TOKEN_FILE)) return null; - const txt = fs.readFileSync(DiscordService.TOKEN_FILE, 'utf-8'); - return JSON.parse(txt) as TokenData; - } catch (err) { - console.error('Error reading token file:', err); - return null; - } + public isLoggedIn() { + return this.loggedIn; } - private isTokenValid(tokenData: TokenData): boolean { - return Date.now() < tokenData.expiresAt - 5000; // small buffer + public getClient() { + return this.client; } private async refreshAccessToken(refreshToken: string): Promise { @@ -65,7 +62,7 @@ class DiscordService { expiresAt: Date.now() + response.data.expires_in * 1000, }; - this.saveTokens(newTokenData); + this.tokenService.saveTokens(newTokenData); return newTokenData; } catch (error) { console.error('Token refresh failed:', error); @@ -109,36 +106,16 @@ class DiscordService { for (const cb of this.voiceUpdateCallbacks) cb(data); } - public onVoiceSettingsUpdate(cb: (d: VoiceSettingsPayload) => void) { - this.voiceUpdateCallbacks.push(cb); - } - - public async login() { - if (this.loggedIn || this.isLoginInProgress) return; - await this._login(); - // load tokens and setup refresh - const tokens = this.loadTokens(); - if (tokens) { - const { refreshToken, expiresAt } = tokens; - setTimeout(() => { - if (DiscordService.refreshed) return; - DiscordService.refreshed = true; - console.log('Starting token refresh flow...'); - this.refreshTokenFlow(refreshToken).catch(console.error); - }, expiresAt - Date.now() - 60000); // refresh 1 min before expiry - } - } - private async _login() { if (this.loggedIn) return; try { this.isLoginInProgress = true; // Try to load existing token - const savedTokens = this.loadTokens(); + const savedTokens = this.tokenService.loadTokens(); if (savedTokens) { - if (this.isTokenValid(savedTokens)) { + if (this.tokenService.isTokenValid(savedTokens)) { console.log('Using saved token...'); await this.authAndLoginClient({ clientId: CONFIG.CLIENT_ID, @@ -147,26 +124,26 @@ class DiscordService { this.loggedIn = true; console.log('Authenticated with saved token!'); return; - } else { - // Try to refresh - console.log('Token expired, refreshing...'); - const refreshed = await this.refreshAccessToken(savedTokens.refreshToken); - if (refreshed) { - await this.authAndLoginClient({ - clientId: CONFIG.CLIENT_ID, - accessToken: refreshed.accessToken, - }); - this.loggedIn = true; - console.log('Authenticated with refreshed token!'); - return; - } } + // Try to refresh + console.log('Token expired, refreshing...'); + const refreshed = await this.refreshAccessToken(savedTokens.refreshToken); + if (refreshed) { + await this.authAndLoginClient({ + clientId: CONFIG.CLIENT_ID, + accessToken: refreshed.accessToken, + }); + this.loggedIn = true; + console.log('Authenticated with refreshed token!'); + return; + } + console.log('Refresh failed, ignoring saved tokens.'); } // Fall back to full OAuth flow console.log('No valid token found. Starting authorization...'); - const client = await this.authAndLoginClient({ + await this.authAndLoginClient({ clientId: CONFIG.CLIENT_ID, clientSecret: CONFIG.CLIENT_SECRET, }); @@ -183,16 +160,6 @@ class DiscordService { } } - public isLoggedIn() { - return this.loggedIn; - } - - public getClient() { - return this.client; - } - - // dev: refresh only once - static refreshed = false; private async authAndLoginClient({ clientId, clientSecret, @@ -223,7 +190,7 @@ class DiscordService { const refreshToken = this.client.refreshToken; const expiresAt = this.client.accessTokenExpiresAt || 0; if (accessToken && refreshToken && expiresAt) { - this.saveTokens({ accessToken, refreshToken, expiresAt }); + this.tokenService.saveTokens({ accessToken, refreshToken, expiresAt }); } else { console.warn('Could not retrieve tokens after login with client secret.'); } @@ -255,6 +222,13 @@ class DiscordService { throw new Error('Failed to refresh token'); } } + + private client!: RPC.Client; + private loggedIn = false; + private isLoginInProgress = false; + private voiceUpdateCallbacks: Array<(d: VoiceSettingsPayload) => void> = []; + private tokenService = new TokenService(); + private refreshTimeout: NodeJS.Timeout | null = null; } export const discordService = new DiscordService(); diff --git a/src/tokenService.ts b/src/tokenService.ts new file mode 100644 index 0000000..4c59488 --- /dev/null +++ b/src/tokenService.ts @@ -0,0 +1,36 @@ +import { writeFileSync, existsSync, readFileSync } from 'fs'; +import { join } from 'path'; + +export interface TokenData { + accessToken: string; + refreshToken: string; + expiresAt: number; // Unix ms timestamp +} + +export class TokenService { + private TOKEN_FILE = join(__dirname, '../', '.discord-token.json'); + + public saveTokens(data: TokenData): void { + try { + writeFileSync(this.TOKEN_FILE, JSON.stringify(data, null, 2)); + console.log('Tokens saved locally'); + } catch (err) { + console.error('Failed to save tokens:', err); + } + } + + public loadTokens(): TokenData | null { + try { + if (!existsSync(this.TOKEN_FILE)) return null; + const txt = readFileSync(this.TOKEN_FILE, 'utf-8'); + return JSON.parse(txt) as TokenData; + } catch (err) { + console.error('Error reading token file:', err); + return null; + } + } + + public isTokenValid(tokenData: TokenData): boolean { + return Date.now() < tokenData.expiresAt - 5000; // small buffer + } +}