fchat-rising/fchat/connection.ts

390 lines
14 KiB
TypeScript
Raw Normal View History

2018-07-20 01:12:26 +00:00
import Axios, {AxiosError, AxiosResponse} from 'axios';
2017-09-02 01:50:31 +00:00
import * as qs from 'qs';
import {Connection as Interfaces, WebSocketConnection} from './interfaces';
2019-09-17 17:14:14 +00:00
import ReadyState = WebSocketConnection.ReadyState;
2020-06-29 19:30:08 +00:00
import log from 'electron-log'; //tslint:disable-line:match-default-export-name
import core from '../chat/core';
import throat from 'throat';
2017-09-02 01:50:31 +00:00
const fatalErrors = [2, 3, 4, 9, 30, 31, 33, 39, 40, 62, -4];
2018-03-28 13:51:05 +00:00
const dieErrors = [9, 30, 31, 39, 40];
2017-09-02 01:50:31 +00:00
let lastFetch = Date.now();
2020-06-29 19:30:08 +00:00
let lastApiTicketFetch = Date.now();
const queryApiThroat = throat(2);
const queryTicketThroat = throat(1);
2017-09-02 01:50:31 +00:00
async function queryApi(this: void, endpoint: string, data: object): Promise<AxiosResponse> {
lastFetch = Date.now();
2017-09-02 01:50:31 +00:00
return Axios.post(`https://www.f-list.net/json/api/${endpoint}`, qs.stringify(data));
}
export default class Connection implements Interfaces.Connection {
character = '';
2019-09-17 17:14:14 +00:00
vars: Interfaces.Vars = <any>{}; //tslint:disable-line:no-any
2017-09-02 01:50:31 +00:00
protected socket: WebSocketConnection | undefined = undefined;
2019-09-17 17:14:14 +00:00
//tslint:disable-next-line:no-object-literal-type-assertion
private messageHandlers = <{ [key in keyof Interfaces.ServerCommands]: Interfaces.CommandHandler<key>[] }>{};
private connectionHandlers: { [key in Interfaces.EventType]?: Interfaces.EventHandler[] } = {};
2017-09-02 01:50:31 +00:00
private errorHandlers: ((error: Error) => void)[] = [];
private ticket = '';
2017-09-02 01:50:31 +00:00
private cleanClose = false;
private reconnectTimer: NodeJS.Timer | undefined;
2019-09-17 17:14:14 +00:00
private account = '';
private ticketProvider?: Interfaces.TicketProvider;
2017-09-02 01:50:31 +00:00
private reconnectDelay = 0;
2018-01-06 16:14:21 +00:00
private isReconnect = false;
2019-09-17 17:14:14 +00:00
private pinTimeout?: NodeJS.Timer;
2017-09-02 01:50:31 +00:00
2018-01-06 16:14:21 +00:00
constructor(private readonly clientName: string, private readonly version: string,
2019-09-17 17:14:14 +00:00
private readonly socketProvider: new() => WebSocketConnection) {
}
setCredentials(account: string, ticketProvider: Interfaces.TicketProvider | string): void {
this.account = account;
2017-09-02 01:50:31 +00:00
this.ticketProvider = typeof ticketProvider === 'string' ? async() => this.getTicket(ticketProvider) : ticketProvider;
}
async connect(character: string): Promise<void> {
2019-09-17 17:14:14 +00:00
if(!this.ticketProvider) throw new Error('No credentials set!');
2017-09-02 01:50:31 +00:00
this.cleanClose = false;
if(this.character !== character) this.isReconnect = false;
2017-09-02 01:50:31 +00:00
this.character = character;
try {
this.ticket = await this.ticketProvider();
} catch(e) {
2018-07-20 01:12:26 +00:00
if(this.reconnectTimer !== undefined)
if((<AxiosError>e).request !== undefined) this.reconnect();
else await this.invokeHandlers('closed', false);
2018-04-11 19:17:58 +00:00
return this.invokeErrorHandlers(<Error>e, true);
}
try {
await this.invokeHandlers('connecting', this.isReconnect);
} catch(e) {
await this.invokeHandlers('closed', false);
return this.invokeErrorHandlers(<Error>e);
2018-01-06 16:14:21 +00:00
}
if(this.cleanClose) {
this.cleanClose = false;
await this.invokeHandlers('closed', false);
return;
}
2018-04-11 19:17:58 +00:00
try {
this.socket = new this.socketProvider();
} catch(e) {
await this.invokeHandlers('closed', false);
return this.invokeErrorHandlers(<Error>e, true);
}
this.socket.onOpen(() => {
2017-09-02 01:50:31 +00:00
this.send('IDN', {
account: this.account,
character: this.character,
2018-01-06 16:14:21 +00:00
cname: this.clientName,
cversion: this.version,
2017-09-02 01:50:31 +00:00
method: 'ticket',
ticket: this.ticket
});
2019-09-17 17:14:14 +00:00
this.resetPinTimeout();
2017-09-02 01:50:31 +00:00
});
2018-04-11 19:17:58 +00:00
this.socket.onMessage(async(msg: string) => {
2017-09-02 01:50:31 +00:00
const type = <keyof Interfaces.ServerCommands>msg.substr(0, 3);
const data = msg.length > 6 ? <object>JSON.parse(msg.substr(4)) : undefined;
2020-06-30 21:51:06 +00:00
log.silly(
2020-07-05 17:43:27 +00:00
'socket.recv',
2020-06-30 21:51:06 +00:00
{
type, data
}
);
2018-01-06 16:14:21 +00:00
return this.handleMessage(type, data);
2017-09-02 01:50:31 +00:00
});
2020-06-30 16:46:38 +00:00
this.socket.onClose(async(event: CloseEvent) => {
log.debug(
'socket.onclose',
{
2020-07-01 14:01:54 +00:00
code: event.code,
reason: event.reason,
wasClean: event.wasClean,
2020-06-30 16:46:38 +00:00
event
}
);
2019-09-17 17:14:14 +00:00
if(this.pinTimeout) clearTimeout(this.pinTimeout);
2018-01-06 16:14:21 +00:00
if(!this.cleanClose) this.reconnect();
2017-09-02 01:50:31 +00:00
this.socket = undefined;
await this.invokeHandlers('closed', !this.cleanClose);
});
2018-04-11 19:17:58 +00:00
this.socket.onError((error: Error) => this.invokeErrorHandlers(error, true));
2017-09-02 01:50:31 +00:00
}
2018-01-06 16:14:21 +00:00
private reconnect(): void {
2018-04-11 19:17:58 +00:00
this.reconnectTimer = setTimeout(async() => this.connect(this.character), this.reconnectDelay);
2018-01-06 16:14:21 +00:00
this.reconnectDelay = this.reconnectDelay >= 30000 ? 60000 : this.reconnectDelay >= 10000 ? 30000 : 10000;
}
2019-01-03 17:38:17 +00:00
close(keepState: boolean = true): void {
if(this.reconnectTimer !== undefined) clearTimeout(this.reconnectTimer);
2018-04-16 23:14:13 +00:00
this.reconnectTimer = undefined;
2017-09-02 01:50:31 +00:00
this.cleanClose = true;
if(this.socket !== undefined) this.socket.close();
if(!keepState) {
this.character = '';
}
2017-09-02 01:50:31 +00:00
}
get isOpen(): boolean {
2019-09-17 17:14:14 +00:00
return this.socket !== undefined && this.socket.readyState === ReadyState.OPEN;
}
async queryApi<T = object>(endpoint: string, data?: {account?: string, ticket?: string}): Promise<T> {
return queryApiThroat(async() => this.queryApiExec<T>(endpoint, data));
}
protected async refreshTicket(oldTicket: string): Promise<string> {
if (this.ticket !== oldTicket) {
log.debug(
'api.ticket.renew.resolve.cache',
{
character: core.characters.ownCharacter?.name
}
);
return this.ticket;
}
if (!this.ticketProvider) {
throw new Error('No credentials set!');
}
this.ticket = await queryTicketThroat(async() => this.ticketProvider!());
log.debug(
'api.ticket.renew.resolve.refresh',
{
character: core.characters.ownCharacter?.name
}
);
return this.ticket;
}
protected async queryApiExec<T = object>(endpoint: string, data?: {account?: string, ticket?: string}): Promise<T> {
2019-09-17 17:14:14 +00:00
if(!this.ticketProvider) throw new Error('No credentials set!');
2020-06-29 19:30:08 +00:00
log.debug(
'api.query.start',
{
endpoint,
2021-09-10 23:02:50 +00:00
data,
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - lastApiTicketFetch
}
);
2017-09-02 01:50:31 +00:00
if(data === undefined) data = {};
2020-06-29 19:30:08 +00:00
2017-09-02 01:50:31 +00:00
data.account = this.account;
data.ticket = this.ticket;
2020-06-29 19:30:08 +00:00
let res = <T & {error: string}>(await queryApi(endpoint, data)).data;
2020-06-29 19:30:08 +00:00
2017-09-02 01:50:31 +00:00
if(res.error === 'Invalid ticket.' || res.error === 'Your login ticket has expired (five minutes) or no ticket requested.') {
2020-06-29 19:30:08 +00:00
log.debug(
'api.ticket.loss',
2020-06-29 19:30:08 +00:00
{
error: res.error,
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - lastApiTicketFetch
}
);
data.ticket = await this.refreshTicket(data.ticket);
res = <T & {error: string}>(await queryApi(endpoint, data)).data;
2017-09-02 01:50:31 +00:00
}
2020-06-29 19:30:08 +00:00
2017-10-16 23:58:57 +00:00
if(res.error !== '') {
2020-06-29 19:30:08 +00:00
log.debug(
'api.query.error',
2020-06-29 19:30:08 +00:00
{
error: res.error,
endpoint,
2021-09-10 23:02:50 +00:00
data,
2020-06-29 19:30:08 +00:00
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - lastApiTicketFetch
}
);
2017-10-16 23:58:57 +00:00
const error = new Error(res.error);
(<Error & {request: true}>error).request = true;
throw error;
}
log.debug(
'api.query.success',
{
endpoint,
2021-09-10 23:02:50 +00:00
data,
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - lastApiTicketFetch
}
);
2017-09-02 01:50:31 +00:00
return res;
}
onError(handler: (error: Error) => void): void {
this.errorHandlers.push(handler);
}
onEvent(type: Interfaces.EventType, handler: Interfaces.EventHandler): void {
let handlers = this.connectionHandlers[type];
if(handlers === undefined) handlers = this.connectionHandlers[type] = [];
handlers.push(handler);
}
offEvent(type: Interfaces.EventType, handler: Interfaces.EventHandler): void {
const handlers = this.connectionHandlers[type];
if(handlers === undefined) return;
handlers.splice(handlers.indexOf(handler), 1);
}
onMessage<K extends keyof Interfaces.ServerCommands>(type: K, handler: Interfaces.CommandHandler<K>): void {
let handlers = <Interfaces.CommandHandler<K>[] | undefined>this.messageHandlers[type];
2017-09-02 01:50:31 +00:00
if(handlers === undefined) handlers = this.messageHandlers[type] = [];
handlers.push(handler);
}
offMessage<K extends keyof Interfaces.ServerCommands>(type: K, handler: Interfaces.CommandHandler<K>): void {
const handlers = <Interfaces.CommandHandler<K>[] | undefined>this.messageHandlers[type];
2017-09-02 01:50:31 +00:00
if(handlers === undefined) return;
handlers.splice(handlers.indexOf(handler), 1);
}
send<K extends keyof Interfaces.ClientCommands>(command: K, data?: Interfaces.ClientCommands[K]): void {
2020-07-05 17:43:27 +00:00
if(this.socket !== undefined && this.socket.readyState === WebSocketConnection.ReadyState.OPEN) {
const msg = <string>command + (data !== undefined ? ` ${JSON.stringify(data)}` : '');
2022-08-13 01:38:13 +00:00
log.silly('socket.send', { data: msg });
2020-07-05 17:43:27 +00:00
this.socket.send(msg);
}
2017-09-02 01:50:31 +00:00
}
//tslint:disable:no-unsafe-any no-any
2018-01-06 16:14:21 +00:00
protected async handleMessage<T extends keyof Interfaces.ServerCommands>(type: T, data: any): Promise<void> {
const time = new Date();
const handlers = <Interfaces.CommandHandler<T>[] | undefined>this.messageHandlers[type];
if(handlers !== undefined)
for(const handler of handlers) await handler(data, time);
2017-09-02 01:50:31 +00:00
switch(type) {
case 'VAR':
2019-09-17 17:14:14 +00:00
this.vars[<keyof Interfaces.Vars>data.variable] = data.value;
2017-09-02 01:50:31 +00:00
break;
case 'PIN':
this.send('PIN');
2019-09-17 17:14:14 +00:00
this.resetPinTimeout();
2017-09-02 01:50:31 +00:00
break;
case 'ERR':
if(fatalErrors.indexOf(data.number) !== -1) {
2018-04-11 19:17:58 +00:00
this.invokeErrorHandlers(new Error(data.message), true);
2018-03-28 13:51:05 +00:00
if(dieErrors.indexOf(data.number) !== -1) {
this.close();
this.character = '';
} else this.socket!.close();
2017-09-02 01:50:31 +00:00
}
break;
case 'NLN':
if(data.identity === this.character) {
2018-01-06 16:14:21 +00:00
await this.invokeHandlers('connected', this.isReconnect);
2017-09-02 01:50:31 +00:00
this.reconnectDelay = 0;
this.isReconnect = true;
2017-09-02 01:50:31 +00:00
}
}
}
//tslint:enable
private async getTicket(password: string): Promise<string> {
console.log('Acquiring new API ticket');
const oldLastApiTicketFetch = lastApiTicketFetch;
log.debug(
'api.getTicket.start',
{
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - oldLastApiTicketFetch
}
);
lastApiTicketFetch = Date.now();
2020-06-29 19:30:08 +00:00
const data = <{ticket?: string, error: string}>(await Axios.post('https://www.f-list.net/json/getApiTicket.php', qs.stringify(
{account: this.account, password, no_friends: true, no_bookmarks: true, no_characters: true}))).data;
if(data.ticket !== undefined) {
2020-06-29 19:30:08 +00:00
log.debug(
'api.getTicket.success',
2020-06-29 19:30:08 +00:00
{
character: core.characters.ownCharacter?.name,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - oldLastApiTicketFetch
2020-06-29 19:30:08 +00:00
}
);
return data.ticket;
}
2020-06-29 19:30:08 +00:00
console.error('API Ticket Error', data.error);
2020-06-29 19:30:08 +00:00
log.error(
'error.api.getTicket',
{
character: core.characters.ownCharacter.name,
error: data.error,
deltaToLastApiCall: Date.now() - lastFetch,
deltaToLastApiTicket: Date.now() - oldLastApiTicketFetch
}
);
2020-06-29 19:30:08 +00:00
2017-09-02 01:50:31 +00:00
throw new Error(data.error);
}
private async invokeHandlers(type: Interfaces.EventType, isReconnect: boolean): Promise<void> {
const handlers = this.connectionHandlers[type];
if(handlers === undefined) return;
for(const handler of handlers) await handler(isReconnect);
}
2018-04-11 19:17:58 +00:00
private invokeErrorHandlers(error: Error, request: boolean = false): void {
if(request) (<Error & {request: true}>error).request = true;
for(const handler of this.errorHandlers) handler(error);
}
2019-09-17 17:14:14 +00:00
private resetPinTimeout(): void {
if(this.pinTimeout) clearTimeout(this.pinTimeout);
2020-07-05 17:43:27 +00:00
this.pinTimeout = setTimeout(
() => {
log.error('pin.timeout');
this.socket!.close();
},
90000
);
2019-09-17 17:14:14 +00:00
}
2020-05-24 22:07:58 +00:00
}