import files to git
This commit is contained in:
		
							
								
								
									
										310
									
								
								src/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								src/api.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										45
									
								
								src/httpClient.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
export * from "./api";
 | 
			
		||||
							
								
								
									
										32
									
								
								src/log.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/log.ts
									
									
									
									
									
										Normal 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 );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user