DramaHarvester/src/reddit/session/SessionManager.ts

109 lines
4.3 KiB
TypeScript

import axios, { AxiosInstance, AxiosError } from 'axios';
const qs = require('qs');
import dotenv from 'dotenv';
import axiosRetry from 'axios-retry';
import axiosThrottle from 'axios-request-throttle';
import { DatabaseService } from '../../db/services/Database';
dotenv.config();
class RedditSessionManager {
private static instance: RedditSessionManager;
public axiosInstance: AxiosInstance;
private constructor() {
axiosThrottle.use(axios, { requestsPerSecond: 1 }); // Throttle setup
this.axiosInstance = axios.create({
baseURL: 'https://oauth.reddit.com/', // Base URL for OAuth2 Reddit API
headers: {
'User-Agent': 'CrossTalk PM/0.1 by Whitneywisconson'
}
});
axiosRetry(this.axiosInstance, {
retries: 3,
retryDelay: this.retryDelayStrategy,
retryCondition: this.retryCondition,
});
}
public static async getInstance(): Promise<RedditSessionManager> {
if (!RedditSessionManager.instance) {
RedditSessionManager.instance = new RedditSessionManager();
await RedditSessionManager.instance.initializeAuthentication();
}
return RedditSessionManager.instance;
}
private async initializeAuthentication() {
// Check the database for an existing token
const currentToken = await DatabaseService.getCurrentOAuthToken(this.axiosInstance.defaults.baseURL as string);
if (currentToken && new Date() < new Date(currentToken.expiry_timestamp * 1000)) {
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${currentToken.access_token}`;
console.log('Using existing Reddit API token from database.');
return;
}
console.log('No current Reddit API token from database requesting one')
// Authenticate with Reddit API to get a new token
await this.authenticate();
}
private async authenticate() {
if (!process.env.redditUsername) throw 'No Reddit Username Found in .env'
if (!process.env.redditPassword) throw 'No Reddit Password Found in .env'
const redditUsername = process.env.redditUsername as string
const redditPassword = process.env.redditPassword as string
const credentials = qs.stringify({
grant_type: 'password',
username: redditUsername,
password: redditPassword,
});
const authString = `${process.env.redditClientId}:${process.env.redditSecret}`;
const buffer = Buffer.from(authString);
const base64AuthString = buffer.toString('base64');
try {
const response = await this.axiosInstance.post('https://www.reddit.com/api/v1/access_token', credentials, {
headers: {
'User-Agent': `CrossTalk PM/0.1 by ${redditUsername}`, //TODO Dynamically set app name here
'Authorization': `Basic ${base64AuthString}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
});
// Upsert the new token into the database
await DatabaseService.upsertOAuthToken(
this.axiosInstance.defaults.baseURL as string,
{
access_token: response.data.access_token,
token_type: response.data.token_type,
expires_in: response.data.expires_in,
scope: response.data.scope,
});
this.axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${response.data.access_token}`;
console.log('Reddit API authenticated successfully.');
} catch (error) {
console.error('Error authenticating with Reddit API:', error);
}
}
private retryDelayStrategy(retryCount: number, error: AxiosError): number {
const retryAfter = error.response?.headers['retry-after'];
if (retryAfter) {
console.log(`429 Retry After: ${retryAfter}`);
return +retryAfter * 1000;
}
return Math.pow(2, retryCount) * 2000;
}
private retryCondition(error: AxiosError): boolean {
const status = error.response?.status ?? 0;
return status === 429 || status >= 400;
}
}
export default RedditSessionManager;