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 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<TokenData | null> {
@@ -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();

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