212 lines
6.0 KiB
TypeScript
212 lines
6.0 KiB
TypeScript
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<TableEntry[]> {
|
|
// 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<string> {
|
|
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<string> {
|
|
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;
|
|
}
|
|
}
|