Advertisement
Guest User

Untitled

a guest
Jan 23rd, 2019
75
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import axios from 'axios'
  2. import * as colors from 'colors/safe'
  3. import * as winston from 'winston'
  4. import * as WebSocket from 'ws'
  5. import * as exitHook from 'async-exit-hook'
  6. // import './WebSocketClient'
  7.  
  8. const LOG_FILE_NAME : string     = 'chat';
  9. const CHAT_BATCH_SIZE : number   = 25;
  10. const CHAT_USERNAME : string     = process.argv[2] || 'dispatch';
  11.  
  12. // Stream.me API locations
  13. const CHAT_MANIFEST_URL : string = 'https://www.stream.me/api-web/v1/chat/room/';
  14. const WEB_SOCKET_URL : string    = 'wss://www.stream.me/api-rooms/v3/ws';
  15.  
  16. // Role to color map.
  17. const ROLE_COLORS = {
  18.     guest     : colors.grey,
  19.     user      : colors.white,
  20.     moderator : colors.cyan,
  21.     owner     : colors.red,
  22.     admin     : colors.red,
  23. };
  24.  
  25.  
  26. //////////////////////////////////////////
  27.  
  28. import * as firebase from 'firebase/app';
  29.  
  30. import 'firebase/auth'
  31. import 'firebase/firestore'
  32.  
  33. if ( !firebase.apps.length )
  34. {
  35.     firebase.initializeApp({
  36.         apiKey            : "",
  37.         authDomain        : "",
  38.         projectId         : "",
  39.         messagingSenderId : "",
  40.         // databaseURL: "",
  41.     })
  42. }
  43.  
  44. const db = firebase.firestore();
  45.  
  46. db.settings({
  47.     timestampsInSnapshots: true,
  48. });
  49.  
  50. const docRef = db.collection('chatlogs').doc(CHAT_USERNAME).collection('messages');
  51.  
  52. //////////////////////////////////////////
  53.  
  54. // Create Logger
  55. const logger = winston.createLogger({
  56.     transports: [
  57.         new winston.transports.File({ filename: `${LOG_FILE_NAME}.log` }),
  58.     ],
  59. });
  60.  
  61.  
  62. const getRoomId = async (username: string) => {
  63.     const url = `https://stream.me/api-user/v2/${username}/app/web/channel`;
  64.  
  65.     try {
  66.         const resp   = await axios.get(url);
  67.         const userId = resp.data['userPublicId'];
  68.         return `user:${userId}:web`;
  69.     } catch (e) {
  70.         if (e.response) {
  71.             console.log(`${e.response.status} - ${e.response.statusText}`);
  72.         } else if (e.request) {
  73.             console.log(e.request);
  74.         } else {
  75.             console.log(e.message);
  76.         }
  77.         return null;
  78.     }
  79. };
  80.  
  81. /**
  82.  * Fetches the chat manifest from the given path.
  83.  *
  84.  * @param {string} roomKey - The room we want to look at
  85.  * @return {Promise<Object>} promise resolving to chat manifest object
  86.  */
  87. const getChatManifest = async ( roomKey: string ) => {
  88.     const path = `${CHAT_MANIFEST_URL}${roomKey}`;
  89.     console.log(colors.red(CHAT_USERNAME));
  90.     console.log(path);
  91.  
  92.     try {
  93.         const resp = await axios.get(path);
  94.         return resp.data;
  95.     } catch (e) {
  96.         console.log(e.response);
  97.         return null;
  98.     }
  99. };
  100.  
  101. /**
  102.  * Gets the final value of the node from the given subnode of the manifest.
  103.  *
  104.  * @param {*} node - The node to convert
  105.  * @param {Object} manifestItem - The part of manifest for this node
  106.  * @param {Object} chatData - The rest of the parserManifest
  107.  * @return {*} The converted object
  108.  */
  109. const getNodeValue = (node, manifestItem, chatData) => {
  110.     // skip null values
  111.     if (node === null) return null;
  112.  
  113.     switch (manifestItem.type) {
  114.         case 'nestedArray':
  115.             return node.map((item) => parseMessageNode(item, manifestItem.nestedItems, chatData));
  116.         case 'nestedObject':
  117.             return parseMessageNode(node, manifestItem.nestedItems, chatData);
  118.         case 'string':
  119.         case 'url':
  120.             return node.toString();
  121.         case 'stringArray':
  122.             return node.map(i => i.toString());
  123.         case 'int':
  124.             return parseInt(node);
  125.         case 'intArray':
  126.             return node.map((item) => parseInt(item));
  127.         case 'urlTemplate':
  128.             // hasMultipleTypes ignored for now
  129.             const urlData = parseMessageNode(node, manifestItem.nestedItems, chatData);
  130.             const key = urlData['key'];
  131.             const template = chatData.urlTemplates[ key ];
  132.             if (!template) return '';
  133.             return template.replace(/{{n}}/g, () => {
  134.                 return urlData['vars'].shift();
  135.             });
  136.         case 'urlTemplates':
  137.             return node.map(
  138.                 (item) => getNodeValue(item, { ...manifestItem, type: 'urlTemplate' }, chatData));
  139.         default:
  140.             throw new Error(`unknown type ${manifestItem.type}`);
  141.     }
  142. };
  143.  
  144. /**
  145.  * Parses a specific node of the message tree.
  146.  *
  147.  * @param {*} node - The node to be parsed
  148.  * @param {Object} manifest - The manifest subtree for this node
  149.  * @param {Object} chatData - The rest of the parserManifest
  150.  * @return {*} Parsed node
  151.  */
  152. const parseMessageNode = (node, manifest, chatData) =>
  153.     Object.keys(manifest).reduce((acc, key) => {
  154.         const manifestItem = manifest[key];
  155.         const currentNode = node[manifestItem.index];
  156.  
  157.         acc[key] = getNodeValue(currentNode, manifestItem, chatData);
  158.  
  159.         return acc;
  160.     }, {});
  161.  
  162. /**
  163.  * Generates the hydrated message object from a chat message.
  164.  *
  165.  * @param {Object} message - The incoming message object from the websocket
  166.  * @param {Object} manifest - The chat manifest
  167.  * @return {Object} Hydrated message object
  168.  */
  169. const generateMessage = (message, manifest) => {
  170.     const {
  171.         parserManifests: {
  172.             manifests: { v2: messageManifest },
  173.             ...chatData
  174.         }
  175.     } = manifest;
  176.  
  177.     return parseMessageNode(message.data, messageManifest, chatData);
  178. };
  179.  
  180. // Counts the amount of characters that are colors in the text.
  181. const colorCharsCount = s => s.length - colors.stripColors(s).length;
  182.  
  183. /**
  184.  * Updates a chat message, styling the emotes.
  185.  *
  186.  * @param {Object} data - The message
  187.  * @return {Object} A new message object with the updated message
  188.  */
  189. const styleEmoticons = data => ({
  190.     ...data,
  191.     message: data.emoticons
  192.         .reduce(
  193.             (message, emote) =>
  194.                 emote.positions.reduce(
  195.                     (m, pos) => colors.italic(emote.pattern) +
  196.                                 colors.bold(emote.pattern) +
  197.                                 m.substr(0, pos + colorCharsCount(m)) +
  198.                                 m.substr(pos + colorCharsCount(m) + emote.length),
  199.                     message,
  200.                 ),
  201.             data.message,
  202.         ),
  203. });
  204.  
  205.  
  206. let batch = db.batch();
  207. let batchSize = 0;
  208.  
  209. /**
  210.  * Formats a message into a readable format from a chat object.
  211.  *
  212.  * @param {Object} message - The message object
  213.  * @return {string} formatted message
  214.  */
  215. const logMessageBatch = async message => {
  216.     console.log(`${ROLE_COLORS[message.actor.role](message.actor.username)}: ${message.message}`);
  217.  
  218.     const msg = `(${CHAT_USERNAME}) ${message.actor.username}: ${message.message}`;
  219.     logger.info(msg);
  220.  
  221.     const docRef = db.collection('chatlogs').doc(CHAT_USERNAME).collection('messages').doc();
  222.     const data = {
  223.         author    : message.actor.username,
  224.         message   : message.message,
  225.         timestamp : firebase.firestore.Timestamp.now(), // firebase.firestore.FieldValue.serverTimestamp(),
  226.     };
  227.  
  228.     batch.set(docRef, data);
  229.     batchSize++;
  230.  
  231.     if (batchSize >= CHAT_BATCH_SIZE) await batchCommit();
  232. };
  233.  
  234.  
  235. const batchCommit = async () => {
  236.     await batch.commit();
  237.     batch = db.batch();
  238.     batchSize = 0;
  239.     console.log(colors.yellow('Saved to FireBase.'));
  240. };
  241.  
  242.  
  243. /**
  244.  * The main part of the program opening a websocket and watching messages.
  245.  */
  246. const main = async () => {
  247.  
  248.     const CHAT_ROOM_KEY = await getRoomId(CHAT_USERNAME);
  249.     if (!CHAT_ROOM_KEY) {
  250.         console.log(`Failed to retrieve user ${CHAT_USERNAME}!`);
  251.         return;
  252.     }
  253.  
  254.     const CHAT_MANIFEST = await getChatManifest(CHAT_ROOM_KEY);
  255.     if (!CHAT_MANIFEST) {
  256.         console.log(`Failed to retrieve chatroom manifest data.`);
  257.         return;
  258.     }
  259.  
  260.     const ws = new WebSocket(WEB_SOCKET_URL);
  261.  
  262.     ws.on('open', () => {
  263.         console.log(colors.green('Connected.\n'));
  264.         logger.info(`Connected to ${CHAT_USERNAME}.`);
  265.         const joinRoom = JSON.stringify({ action: 'join', room: CHAT_ROOM_KEY });
  266.         ws.send(`chat ${joinRoom}`);
  267.     });
  268.  
  269.     ws.on('message', async (data: string, flags) => {
  270.         const message = JSON.parse(data.substr(13)); // skip 'chat message '
  271.  
  272.         if (message.type !== 'chat') return;
  273.         const formedMessage = generateMessage(message, CHAT_MANIFEST);
  274.         await logMessageBatch(formedMessage);
  275.     });
  276.  
  277.     ws.on('close', async (reason, number) => {
  278.         if (batchSize > 0) await batchCommit();
  279.  
  280.         console.log('Closed, Attempting to reconnect', reason, number);
  281.  
  282.         setTimeout( async () => await main(), 5000 );
  283.     });
  284.  
  285.     ws.on('error', (e) => {
  286.         console.log(e)
  287.     });
  288.  
  289. };
  290.  
  291.  
  292. exitHook(exit => {
  293.     if (batchSize > 0) {
  294.         console.log(colors.cyan('\nATTEMPTING TO SAVE PENDING BATCH'));
  295.         batchCommit().then(() => {
  296.             console.log('\nGoodbye :)');
  297.             exit();
  298.         });
  299.     } else {
  300.         console.log('\nNo pending data - Goodbye :)');
  301.         exit();
  302.     }
  303. });
  304.  
  305. if (require.main === module) main();
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement