helpers/pusher-htb.js

/**
 * Implements functionality for parsing and responding to Pusher events from the public HTB Shoutbox.
 @module Pusher-Htb
*/

const EventEmitter = require('events');
const Pusher = require('pusher-client');
const HTMLParser = require('node-html-parser');
const TD = require("turndown")

const td = new TD()

/**
 * Handles a single Pusher achievement message, sending it to our announcement channel as a pretty embed if it concerns our team.
 * @param {Object} data - Contains the Pusher event data.
 * @param {Discord.Channel} channel - The Discord channel to send the announcement to, if one is configured.
 * @returns {string} - Debug description of the event / achievement.
 */


function parsePusherEvent(data) {
  try {
    var msg = HTMLParser.parse(data.text)
    var nodes = (msg.lastChild.rawText == "[Tweet]" ? msg.childNodes.slice(0, -1) : msg.childNodes)        // MD: Eliminate the "[Tweet]" link
    nodes.forEach(node => { node = node.rawText; }) // MD: Get HTML from each remaining node
    var md = td.turndown(nodes.join(""))          // MD: Recombine HTML and convert to Markdown
    var uid = Number(msg.firstChild.rawAttributes.href.substring(45))
    var type = undefined
    var target = undefined
    var lemmas = msg.childNodes[1].rawText.trim().split(" ")
    verb = lemmas[0]
    if (verb == "solved") {
      // This is a challenge own.
      type = "challenge"
      target = msg.childNodes[2].structuredText.trim()
    } else {
      // This is (probably) a box own.
      target = lemmas[1]
      switch (target) {
        case "root": case "system": type = "root"; break;
        case "user": type = "user"; break;
        default: break;
      }
    }
    target = msg.childNodes[2].structuredText.trim()
    return new HtbPusherEvent(uid, type, target, "", md, msg.structuredText)
  } catch (error) {
    console.error(error)
    return null
  }

}

class HtbPusherEvent {
  /**
   * An object containing data parsed from a HTB Pusher event. Contains structured information about the specific achievement, target and users involved, as well as the original text for debugging.
   * @param {number} uid  - The Htb UID of the user involved.
   * @param {string} type - The type of message this was, e.g. a challenge own, fortress milestone, machine rating etc.
   * @param {string} target - The string name of the target (thing that was owned), if relevant.
   * @param {string} flag - The string name of the flag / milestone, if a pro lab or other necessitating challenge.
   * @param {string} markdown - The bare markdown representation of the original HTML announcement string.
   * @param {string} debug - The raw HTML string passed in the Pusher event.
   */
  constructor(uid, type, target, flag, markdown, debug) {
    this.uid = uid
    this.time = new Date().getTime()
    this.type = type
    this.target = target
    this.flag = flag
    this.markdown = markdown
    this.debug = debug
  }

}



/** Class representing a HTB challenge / box creator.
 * 
 * @typedef HtbPusherSubscription
 * @property {number} client - The Pusher Client instance.
 * @property {string} channel - The channel being listened on.
 */
class HtbPusherSubscription extends EventEmitter {
  /**
   * Creates a new HtbPusherSubscription object.
   * @param {string} apiToken - The Pusher Client instance.
   * @param {string} channel - The Pusher Client instance.
   * @param {string} bindEvent - The event trigger to bind to (e.g. 'newmessage')
   * @param {string} csrfToken - The Htb CSRF protection token, used for (primitive) authentication.
   * @returns {HtbPusherSubscription}
   */

  // new HtbPusherSubscription('97608bf7532e6f0fe898', 'owns-channel', 'display-info', token)
  //'97608bf7532e6f0fe898' (Htb pusher api token)
  constructor(apiToken, channel, bindEvent, csrfToken) {
    super()
    this.client = new Pusher(apiToken, {
      authEndpoint: 'https://www.hackthebox.com/pusher/auth',
      auth: { "X-CSRF-Token": csrfToken },
      authTransport: "ajax",
      cluster: 'eu',
      encrypted: true
    });
    this.channel = this.client.subscribe(channel);
    this.channel.bind(bindEvent,
      (data) => {
        try {
          this.alertSeven(parsePusherEvent(data)) // Pass the parsed message back for processing
        } catch (error) {
          console.error(error)
        }
      }
    );
    this.client.connection.bind('state_change', function (states) {
      console.log("Pusher client state changed from " + states.previous + " to " + states.current);
    });
  }

   /**
   * 
   * @param {*} message 
   */
  alertSeven(message) {
    if (message) {
      this.emit("pusherevent", message);
    }
  };

  /**
   * Updates the CSRF-token based authentication for the Pusher Client.
   * @param {Object} csrfToken - This param should be a token string.
   */
  set auth(csrfToken) {
    try {
      this.client.config.auth['X-CSRF-Token'] = csrfToken
    } catch (error) {
      console.error(error)
    }
  };
}

module.exports = {
  HtbPusherEvent: HtbPusherEvent,
  HtbPusherSubscription: HtbPusherSubscription,
  parsePusherEvent: parsePusherEvent
}