refactor token service

This commit is contained in:
GW_MC
2026-01-23 18:12:46 +08:00
parent 501198b5a0
commit e782c5d8ac
2 changed files with 90 additions and 80 deletions

View File

@@ -1,51 +1,48 @@
import * as RPC from 'discord-rpc'; import * as RPC from 'discord-rpc';
import axios from 'axios'; import axios from 'axios';
import * as fs from 'fs';
import * as path from 'path';
import { CONFIG } from './config'; import { CONFIG } from './config';
import { VoiceSettingsPayload } from './types'; import { VoiceSettingsPayload } from './types';
import { TokenData, TokenService } from './tokenService';
interface TokenData {
accessToken: string;
refreshToken: string;
expiresAt: number; // Unix ms timestamp
}
class DiscordService { 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() { constructor() {
this.initialize();
}
public onVoiceSettingsUpdate(cb: (d: VoiceSettingsPayload) => void) {
this.voiceUpdateCallbacks.push(cb);
}
public initialize() {
this.client = new RPC.Client({ transport: 'ipc' }); this.client = new RPC.Client({ transport: 'ipc' });
this.setupEvents(); this.setupEvents();
} }
private saveTokens(data: TokenData): void { public async login() {
try { if (this.loggedIn || this.isLoginInProgress) return;
fs.writeFileSync(DiscordService.TOKEN_FILE, JSON.stringify(data, null, 2)); await this._login();
console.log('Tokens saved locally'); // load tokens and setup refresh
} catch (err) { const tokens = this.tokenService.loadTokens();
console.error('Failed to save tokens:', err); 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 { public isLoggedIn() {
try { return this.loggedIn;
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;
}
} }
private isTokenValid(tokenData: TokenData): boolean { public getClient() {
return Date.now() < tokenData.expiresAt - 5000; // small buffer return this.client;
} }
private async refreshAccessToken(refreshToken: string): Promise<TokenData | null> { private async refreshAccessToken(refreshToken: string): Promise<TokenData | null> {
@@ -65,7 +62,7 @@ class DiscordService {
expiresAt: Date.now() + response.data.expires_in * 1000, expiresAt: Date.now() + response.data.expires_in * 1000,
}; };
this.saveTokens(newTokenData); this.tokenService.saveTokens(newTokenData);
return newTokenData; return newTokenData;
} catch (error) { } catch (error) {
console.error('Token refresh failed:', error); console.error('Token refresh failed:', error);
@@ -109,36 +106,16 @@ class DiscordService {
for (const cb of this.voiceUpdateCallbacks) cb(data); 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() { private async _login() {
if (this.loggedIn) return; if (this.loggedIn) return;
try { try {
this.isLoginInProgress = true; this.isLoginInProgress = true;
// Try to load existing token // Try to load existing token
const savedTokens = this.loadTokens(); const savedTokens = this.tokenService.loadTokens();
if (savedTokens) { if (savedTokens) {
if (this.isTokenValid(savedTokens)) { if (this.tokenService.isTokenValid(savedTokens)) {
console.log('Using saved token...'); console.log('Using saved token...');
await this.authAndLoginClient({ await this.authAndLoginClient({
clientId: CONFIG.CLIENT_ID, clientId: CONFIG.CLIENT_ID,
@@ -147,7 +124,7 @@ class DiscordService {
this.loggedIn = true; this.loggedIn = true;
console.log('Authenticated with saved token!'); console.log('Authenticated with saved token!');
return; return;
} else { }
// Try to refresh // Try to refresh
console.log('Token expired, refreshing...'); console.log('Token expired, refreshing...');
const refreshed = await this.refreshAccessToken(savedTokens.refreshToken); const refreshed = await this.refreshAccessToken(savedTokens.refreshToken);
@@ -160,13 +137,13 @@ class DiscordService {
console.log('Authenticated with refreshed token!'); console.log('Authenticated with refreshed token!');
return; return;
} }
} console.log('Refresh failed, ignoring saved tokens.');
} }
// Fall back to full OAuth flow // Fall back to full OAuth flow
console.log('No valid token found. Starting authorization...'); console.log('No valid token found. Starting authorization...');
const client = await this.authAndLoginClient({ await this.authAndLoginClient({
clientId: CONFIG.CLIENT_ID, clientId: CONFIG.CLIENT_ID,
clientSecret: CONFIG.CLIENT_SECRET, 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({ private async authAndLoginClient({
clientId, clientId,
clientSecret, clientSecret,
@@ -223,7 +190,7 @@ class DiscordService {
const refreshToken = this.client.refreshToken; const refreshToken = this.client.refreshToken;
const expiresAt = this.client.accessTokenExpiresAt || 0; const expiresAt = this.client.accessTokenExpiresAt || 0;
if (accessToken && refreshToken && expiresAt) { if (accessToken && refreshToken && expiresAt) {
this.saveTokens({ accessToken, refreshToken, expiresAt }); this.tokenService.saveTokens({ accessToken, refreshToken, expiresAt });
} else { } else {
console.warn('Could not retrieve tokens after login with client secret.'); console.warn('Could not retrieve tokens after login with client secret.');
} }
@@ -255,6 +222,13 @@ class DiscordService {
throw new Error('Failed to refresh token'); 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(); export const discordService = new DiscordService();

36
src/tokenService.ts Normal file
View File

@@ -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
}
}