import { TableEntry } from '../models/TableEntry'; import { TunakillUser } from '../models/TunakillUser'; import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { TunakillLogin } from '../models/TunakillLogin'; import fetch from 'node-fetch'; import logger from '../logger/Logger'; @Injectable() export class SinusBotService { private host = process.env.HOST; private credentials = { username: process.env.SINUSBOT_USER, password: process.env.SINUSBOT_PASSWORD, }; private botInfo = { id: undefined, instanceId: process.env.SINUSBOT_INSTANCEID, }; private botIdURL = `${this.host}/api/v1/botId`; private tunaKillURL = `${this.host}/api/v1/bot/i/${this.botInfo.instanceId}/event/tunakill_rank_all_user`; private loginURL = `${this.host}/api/v1/bot/login`; private bearer: string; public async fetchStats(): Promise { // Skip check as either way // - An interval needs to reset this.bearer to null // - The Sinusbot token is not a JWT => Expiration date is not decodable // - Estimated expiration time: 1d? // - I don't know if it makes a difference to check via interval or to just fetch the token // everytime a request is sent against this API. // if (this.bearer == null) { logger.info(`Hey! I'm trying to get my Bearer token!`); await this.login() .then(token => (this.bearer = token)) .then(() => { logger.debug( `Seems like I have my Bearer token! Looks like it's ${ this.bearer == null ? 'undefined' : 'not undefined' }`, ); }); // } logger.info( `I try to fetch user data now! The URL is called ${this.tunaKillURL}`, ); return await fetch(this.tunaKillURL, this.requestConfig(null, this.bearer)) .then(res => this.checkStatus(res)) .then(res => res.json()) .then(data => this.consumeTunakillResponse(data)) .catch(error => { logger.error(`I couldn't fetch user data.`, error); throw this.createHttpException( HttpStatus.INTERNAL_SERVER_ERROR, error.message, ); }); } /** * Returns bearer token if login was successful */ private async login(): Promise { if (this.botInfo.id == null) { logger.debug(`I have to fetch a bot ID before I can continue!`); await this.fetchDefaultBotId() .then(defaultBotId => (this.botInfo.id = defaultBotId)) .catch(error => { logger.warn( `I couldn't retrieve SinusBot bot information. Login is likely to fail!`, error, ); throw this.createHttpException( HttpStatus.NOT_FOUND, `Could not fetch enough bot information for further requests.`, ); }); logger.info(`The bot ID now is ${this.botInfo.id}`); } const body: TunakillLogin = { username: this.credentials.username, password: this.credentials.password, botId: this.botInfo.id, }; logger.info(`Logging in for Bearer token!`); return await fetch(this.loginURL, this.requestConfig(JSON.stringify(body))) .then(res => this.checkStatus(res)) .then(res => res.json()) .then(data => data.token) .catch(error => { logger.error( `Oh oh! Something went wrong while fetching Bearer token.`, error, ); throw this.createHttpException( HttpStatus.UNAUTHORIZED, `Fetching Bearer token for Sinusbot failed. Please refresh page or try again later!`, ); }); } private async fetchDefaultBotId(): Promise { return await fetch(this.botIdURL) .then(res => this.checkStatus(res)) .then(res => res.json()) .then(data => data.defaultBotId); } private consumeTunakillResponse(data: any): TableEntry[] { if (!(data !== null || data[0] !== null || data[0].data !== null)) { throw Error('Response from SinusBot does not have any data to parse.'); } const response = data[0].data; if (!(response.length > 0)) { throw Error('Response from SinusBot does not have any data to parse.'); } return ( response // TODO: Remove hardcoded username filter for bots. .filter( (user: TunakillUser) => user.name !== 'Server Query Admin' && user.name !== 'DJ Inshalla', ) .filter((user: TunakillUser) => user.time != null) .map((user: TunakillUser) => { return { name: user.name, rawTime: user.time, onlineTime: this.humanizeTime(user.time), }; }) .sort((a: any, b: any) => this.sortByDescendingTime(a.rawTime, b.rawTime), ) ); } private requestConfig = ( body?: string, bearerToken?: string, requestType: string = 'POST', ): RequestInit => { return { method: requestType, body: body, headers: { Accept: '*/*', 'Content-Type': 'application/json', 'User-Agent': 'HumeniusTSRankingBackend/0.0.2', Authorization: `Bearer ${bearerToken}`, }, }; }; private createHttpException = ( statusCode: HttpStatus, message?: string, ): HttpException => { return new HttpException( { status: statusCode, error: message, }, statusCode, ); }; private checkStatus = response => { if (!response.ok) { let err = new RequestError(response.errorText); err.response = response; throw err; } return response; }; private sortByDescendingTime = (a: number, b: number) => { if (a < b) { return 1; } if (a > b) { return -1; } return 0; }; private humanizeTime = (seconds: number) => { const d = Math.floor(seconds / (3600 * 24)); const h = Math.floor((seconds % (3600 * 24)) / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); return `${d}d ${h}h ${m}m ${s}s`; }; private static logResponse(res: any) { logger.debug(res); return res; } }