import files to git
This commit is contained in:
parent
a2273e4899
commit
9e9b1556d8
|
@ -0,0 +1,310 @@
|
||||||
|
import $http from "./httpClient";
|
||||||
|
import logger from "./log";
|
||||||
|
import { io } from "socket.io-client";
|
||||||
|
|
||||||
|
const $log = new logger("[bitwave.tv API]");
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
avatar: string;
|
||||||
|
badge: string;
|
||||||
|
color: string;
|
||||||
|
unum: number;
|
||||||
|
message: string;
|
||||||
|
timestamp: number;
|
||||||
|
username: string;
|
||||||
|
channel: string;
|
||||||
|
global: boolean;
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OutgoingMessage {
|
||||||
|
message: string;
|
||||||
|
channel: string;
|
||||||
|
global: boolean;
|
||||||
|
showBadge: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Credentials = string | Token | void;
|
||||||
|
|
||||||
|
const apiPrefix = "https://api.bitwave.tv/api/";
|
||||||
|
const chatServer = "https://chat.bitwave.tv";
|
||||||
|
const whisperEndpoint: [string, string] = [
|
||||||
|
"api.bitwave.tv",
|
||||||
|
"/v1/whispers/send",
|
||||||
|
];
|
||||||
|
|
||||||
|
export interface Token {
|
||||||
|
recaptcha: any;
|
||||||
|
page: string;
|
||||||
|
jwt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========================================= */
|
||||||
|
|
||||||
|
export class FedwaveChat {
|
||||||
|
private _socket = null;
|
||||||
|
|
||||||
|
private _chatServer = "https://fw.rnih.org";
|
||||||
|
private _apiPrefix = "https://fw.rnih.org";
|
||||||
|
private _whisperEndpoint = "/v1/whispers/send";
|
||||||
|
|
||||||
|
public async getTrollToken() {
|
||||||
|
try {
|
||||||
|
return await $http.get(this._apiPrefix + "/mktroll");
|
||||||
|
} catch (e) {
|
||||||
|
$log.error(`Couldn't get troll token!`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private userProfile: Token = {
|
||||||
|
recaptcha: null,
|
||||||
|
page: "global", // room name
|
||||||
|
jwt: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses `credentials` to get a token from the server.
|
||||||
|
*
|
||||||
|
* @return JWT token as string
|
||||||
|
*/
|
||||||
|
public async initToken(credentials: Token | void) {
|
||||||
|
if (credentials) {
|
||||||
|
this.userProfile = credentials;
|
||||||
|
} else {
|
||||||
|
this.userProfile.jwt = await this.getTrollToken();
|
||||||
|
this.onUpdateCredentials(this.userProfile.jwt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public global: boolean | any = true; /**< Global chat mode flag */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function that receives messages (in bulk)
|
||||||
|
* @param ms Message object array
|
||||||
|
*/
|
||||||
|
public rcvMessageBulk(ms: Message[]): void {
|
||||||
|
for (const m of ms) console.log(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function that receives paid chat alert objects
|
||||||
|
* @param message Alert object
|
||||||
|
*/
|
||||||
|
public alert(message: Object): void {
|
||||||
|
$log.warn(`Received alert: `, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public channelViewers = []; /**< Array of channel viewers. */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function that gets called when the list of usernames gets updated.
|
||||||
|
*/
|
||||||
|
public async onUpdateUsernames(newViewers: any[]) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback function that gets called when current credentials change.
|
||||||
|
*/
|
||||||
|
public async onUpdateCredentials(newCredentials: Credentials) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets an array of usernames from the server and puts it in channelViewers
|
||||||
|
* It is called automatically at request from the server, but can be called manually
|
||||||
|
* @see channelViewers
|
||||||
|
*/
|
||||||
|
public async updateUsernames(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const data = await $http.get(this._apiPrefix + "/v1/chat/channels");
|
||||||
|
if (data && data.success) {
|
||||||
|
await this.onUpdateUsernames(data.data);
|
||||||
|
this.channelViewers = data.data;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$log.error(`Couldn't update usernames!`);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onHydrate(data: Message[]) {
|
||||||
|
this.rcvMessageBulk(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests messages from the server (called hydration)
|
||||||
|
* It is called automatically when reconnecting.
|
||||||
|
* @see socketError()
|
||||||
|
* @return False if unsuccessful or empty
|
||||||
|
*/
|
||||||
|
public async hydrate(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const url: string =
|
||||||
|
this._apiPrefix +
|
||||||
|
"/v1/messages/" +
|
||||||
|
(!this.global && this.userProfile.page ? this.userProfile.page : "");
|
||||||
|
const data = JSON.parse(await $http.get(url));
|
||||||
|
if (!data.size)
|
||||||
|
return $log.warn("Hydration data was empty") === undefined && false;
|
||||||
|
|
||||||
|
this.onHydrate(data.data);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
$log.error(`Couldn't get chat hydration data!`);
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called when connecting to the server
|
||||||
|
*/
|
||||||
|
public socketConnect() {
|
||||||
|
this._socket.emit("new user", this.userProfile);
|
||||||
|
$log.info(`Connected to chat! (${this.userProfile.page})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called when the server issues a reconnect.
|
||||||
|
* It force hydrates chat to catch up.
|
||||||
|
*/
|
||||||
|
public async socketReconnect() {
|
||||||
|
$log.info("Socket issued 'reconnect'. Forcing hydration...");
|
||||||
|
await this.hydrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called when there's a socket error.
|
||||||
|
*/
|
||||||
|
public async socketError(message: string, error) {
|
||||||
|
$log.error(`Socket error: ${message}`, error);
|
||||||
|
// TODO: handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
public blocked(data) {
|
||||||
|
$log.info("TODO: handle blocked event", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public pollstate(data) {
|
||||||
|
$log.info("TODO: handle pollstate event", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(doLogging?: boolean) {
|
||||||
|
$log.doOutput = doLogging;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inits data and starts connection to server
|
||||||
|
* @param room is a string for the channel you wish to connect to
|
||||||
|
* @param credentials User credentials if falsy, gets a new troll token. If a string, it's taken as the JWT chat token
|
||||||
|
* @param specificServer URI to a specific chat server
|
||||||
|
*/
|
||||||
|
async connect(
|
||||||
|
room: string,
|
||||||
|
credentials: string | Token | void,
|
||||||
|
specificServer?: string
|
||||||
|
) {
|
||||||
|
if (typeof credentials === "string") this.userProfile.jwt = credentials;
|
||||||
|
else this.initToken(credentials);
|
||||||
|
|
||||||
|
this.userProfile.page = room;
|
||||||
|
|
||||||
|
const socketOptions = { transports: ["websocket"] };
|
||||||
|
this._socket = io(specificServer || this._chatServer, socketOptions);
|
||||||
|
|
||||||
|
const sockSetup = new Map([
|
||||||
|
[
|
||||||
|
"connect",
|
||||||
|
async () => {
|
||||||
|
this._socket.emit("new user", this.userProfile);
|
||||||
|
$log.info(`Connected to chat! (${this.userProfile.page})`);
|
||||||
|
await this.socketConnect.call(this);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"reconnect",
|
||||||
|
async () => {
|
||||||
|
$log.info("Socket issued 'reconnect'. Forcing hydration...");
|
||||||
|
await this.hydrate();
|
||||||
|
await this.socketReconnect.call(this);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"error",
|
||||||
|
async (error: Object) => {
|
||||||
|
// TODO: handle error
|
||||||
|
$log.error(`Socket error: Connection Failed`, error);
|
||||||
|
await this.socketError.call(this, `Connection Failed`, error);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"disconnect",
|
||||||
|
async (data: Object) =>
|
||||||
|
await $log.error(`Socket error: Connection Lost`, data),
|
||||||
|
],
|
||||||
|
["update usernames", async () => await this.updateUsernames()],
|
||||||
|
[
|
||||||
|
"bulkmessage",
|
||||||
|
async (data: Message[]) => await this.rcvMessageBulk(data),
|
||||||
|
],
|
||||||
|
["alert", async (data) => await this.alert(data)],
|
||||||
|
]);
|
||||||
|
|
||||||
|
sockSetup.forEach((cb, event) => {
|
||||||
|
this._socket.on(event, cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get room() {
|
||||||
|
return this.userProfile.page;
|
||||||
|
} /**< Current room */
|
||||||
|
set room(r) {
|
||||||
|
this.userProfile.page = r;
|
||||||
|
$log.info(`Changed to room ${r}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get doLogging() {
|
||||||
|
return $log.doOutput;
|
||||||
|
} /**< Enable log output */
|
||||||
|
set doLogging(r) {
|
||||||
|
$log.doOutput = r;
|
||||||
|
}
|
||||||
|
|
||||||
|
get socket() {
|
||||||
|
return this._socket;
|
||||||
|
} /**< Deprecated, but allows access to underlying socket */
|
||||||
|
set socket(s) {
|
||||||
|
this._socket = s;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect(): void {
|
||||||
|
this.socket?.off();
|
||||||
|
this.socket?.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends message with current config (this.userProfile)
|
||||||
|
* @param msg Message to be sent. Can be an object: { message, channel, global, showBadge }, or just a string (in which case channel/global use current values)
|
||||||
|
*/
|
||||||
|
sendMessage(msg: OutgoingMessage | string): void {
|
||||||
|
switch (typeof msg) {
|
||||||
|
case "object":
|
||||||
|
this._socket.emit("message", msg);
|
||||||
|
break;
|
||||||
|
case "string":
|
||||||
|
this._socket.emit("message", {
|
||||||
|
message: msg,
|
||||||
|
channel: this.userProfile.page,
|
||||||
|
global: this.global,
|
||||||
|
showBadge: true,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendWhisper(recipient: string, msg: string): Promise<void> {
|
||||||
|
await $http.post(this._whisperEndpoint[0], this._whisperEndpoint[1], {
|
||||||
|
chatToken: this.userProfile.jwt,
|
||||||
|
receiver: recipient,
|
||||||
|
message: msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
const isNode = typeof process === "object";
|
||||||
|
|
||||||
|
const $get = (url: string, cb: Function): void => {
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
req.onreadystatechange = () => {
|
||||||
|
if (req.readyState === 4 && req.status === 200) {
|
||||||
|
cb(req.responseText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.open("GET", url, true);
|
||||||
|
req.send(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const $post = (
|
||||||
|
url: string,
|
||||||
|
path: string,
|
||||||
|
data: any,
|
||||||
|
cb: (...args: any[]) => void
|
||||||
|
): void => {
|
||||||
|
const req = new XMLHttpRequest();
|
||||||
|
req.onreadystatechange = () => {
|
||||||
|
if (req.readyState === 4 && req.status === 200) {
|
||||||
|
cb(JSON.parse(req.responseText));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
req.open("POST", url + path);
|
||||||
|
|
||||||
|
req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||||
|
req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
|
||||||
|
|
||||||
|
req.send(JSON.stringify(data));
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
get: (url: string): Promise<any> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
$get(url, (response) => resolve(response));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
post: (url: string, path: string, data: Object): Promise<any> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
$post(url, path, data, (response) => resolve(response));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "./api";
|
|
@ -0,0 +1,32 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a logger
|
||||||
|
* @constructor
|
||||||
|
* @param {string} prefix - logger prefix
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class {
|
||||||
|
public readonly prefix: string;
|
||||||
|
public doOutput: boolean = true;
|
||||||
|
|
||||||
|
/** Creates new logger */
|
||||||
|
constructor( prefix?: string, doOutput?: boolean ) {
|
||||||
|
this.doOutput = doOutput;
|
||||||
|
this.prefix = (prefix ?? '[bitwave.tv API]') + ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates logger info */
|
||||||
|
info( message: String, ...args ): void {
|
||||||
|
this.doOutput && console.log( this.prefix + message, ...args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates logger warn */
|
||||||
|
warn( message: String, ...args ): void {
|
||||||
|
this.doOutput && console.warn( this.prefix + '[WARN] ' + message, ...args );
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates logger error */
|
||||||
|
error( message: String, ...args ): void {
|
||||||
|
this.doOutput && console.error( this.prefix + '[ERROR] ' + message, ...args );
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue