import files to git

This commit is contained in:
dvdrw 2023-02-11 17:35:35 +00:00
parent a2273e4899
commit 9e9b1556d8
4 changed files with 388 additions and 0 deletions

310
src/api.ts Normal file
View File

@ -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,
});
}
}

45
src/httpClient.ts Normal file
View File

@ -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));
});
},
};

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
export * from "./api";

32
src/log.ts Normal file
View File

@ -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 );
}
}