Advertisement
Guest User

Untitled

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