refactor token service
This commit is contained in:
@@ -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,26 +124,26 @@ 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
|
|
||||||
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
|
// 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
36
src/tokenService.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user