10
\$\begingroup\$

This is a script that accesses the SE impromptu APIs to receive the fkey, a variable need for connection to chatrooms, and uses it to build a websockets connection which is handled by Node.js.

This is also available on GitHub.

It requires your username and password in the ws.js file and looks like this once executed:

image

chat.js:

var messages = {};
var stars = {};
var flagged = [];
var EVENT_TYPES = {
      MessagePosted: 1
    , MessageEdited: 2
    , UserEntered: 3
    , UserLeft: 4
    , RoomNameChanged: 5
    , MessageStarred: 6
    , UserMentioned: 8
    , MessageFlagged: 9
    , MessageDeleted: 10
    , UserNotification: 16
    , Invitation: 17
    , MessageReply: 18
    , MessageMovedOut: 19
    , MessageMovedIn: 20
};
function convert(str){
    return !str ? "":
    str.replace(/&/g, "&")
       .replace(/>/g, ">")
       .replace(/&lt;/g, "<")
       .replace(/&quot;/g, "\"")
       .replace(/&#39;/g, "'");
}
var LAST_EVENT;
function processEvent(event){
    if (event.content == LAST_EVENT){
        return false;
    }
    LAST_EVENT = event.content;
    event.content = convert(event.content);
    switch (event.event_type){
        case EVENT_TYPES.MessagePosted:
            say(
                event.user_name + ": " + event.content,
                event.room_name
            );
            messages[event.message_id] = event.content;
            break;
        case EVENT_TYPES.MessageEdited:
        var message = messages[event.message_id];
            if (typeof message != "undefined" && typeof message == "string"){
                say(
                    event.user_name + " edited their post of '" + (message.length > 25 ? message.substring(0, 12) + "..." : message) + "' to say: " + event.content,
                    event.room_name
                );
            } else {
                say (
                    event.user_name + "edited a post to say: " + event.content,
                    event.room_name
                )
            }
            messages[event.message_id] = event.content;
            break;
        case EVENT_TYPES.UserEntered:
            say(event.user_name + " entered " + event.room_name);
            break;
        case EVENT_TYPES.UserLeft:
            say(event.user_name + " left " + event.room_name);
            break;
        case EVENT_TYPES.RoomNameChanged:
            say(event.user_name + " changed the name of " + event.room_name + " to " + event.content.substring(0, event.content.lastIndexOf(" /")));
            break;
        case EVENT_TYPES.MessageStarred:
            event.message_id in stars ? event.message_id++ : event.message_id = 1;
            if (messages[event.message_id] > 3){
                say(
                    "highly starred message: " + event.content,
                    event.room_name
                );
            }
            break;
        case EVENT_TYPES.UserMentioned:
            say("@" + event.user_name + " mentioned you in " + event.room_name + "!");
            say(event.content);
            break;
        case EVENT_TYPES.MessageFlagged:
        flagged.push(event);
            say("A message was flagged in " + event.room_name + ":");
            say(event.content);
            break;
        case EVENT_TYPES.MessageDeleted:
            if (event.message_id in messages){
                say(
                    event.user_name + " deleted a message",
                    event.room_name
                );
            } else {
                say(
                    event.user_name + " deleted their message of: " + messages[event.message_id],
                    event.room_nmae
                );
            }
            break;
        case EVENT_TYPES.UserNotification:
            say("@" + event.user_name + " superpinged you into " + event.room_name);
            if (event.content) say(event.content);
            break;
        case EVENT_TYPES.Invitation:
            say("@" + event.user_name + " invited you to join " + event.room_name);
            break;
        case EVENT_TYPES.MessageReply:
            say(
                "@" + event.user_name + " replied to you",
                event.room_name
            );
            say(event.content);
            break;
        case EVENT_TYPES.MessageMovedOut:
            say("You had a message moved into" + event.room_name + "by @" + event.user_name);
            break;
        case EVENT_TYPES.MessageMovedIn:
            say("Your messaged was moved into " + event.room_name);
            say(event.content);
            break;
    }
}
function removeMessage(){
    delete messages[Object.keys(messages)[0]];
}
function say(message, room){
    if (!message) throw new Error("No message provided.");
    var finalMessage = room ? ' [' + room + '] ' : "";
    console.log(finalMessage + message);
}

exports.modules = {
    processEvent: processEvent
};

ws.js:

var cheerio = require('cheerio');
var Promise = require("bluebird");
var request = Promise.promisifyAll(require('request'));
var WebSocket = require('ws');
var handler = require('./chat').modules

var email = "",
    password = "",
    roomid = 8595;

var j = request.jar()
request.getAsync({url:"https://codereview.stackexchange.com/users/login", jar:j}).then(function(response){
    var $ = cheerio.load(response.body)
    var fkey = $("input[name=fkey]").attr('value')
    return request.postAsync({url:"https://codereview.stackexchange.com/users/login", form: {email:email,password:password,fkey:fkey}, jar:j})
}).then(function(response){
    return request.getAsync({url:"http://chat.stackexchange.com/rooms/" + roomid, jar:j})
}).then(function(response){
    var $ = cheerio.load(response.body)
    var fkey = $("input[name=fkey]").attr('value')
    var url, time;
    request.postAsync({url:"http://chat.stackexchange.com/ws-auth",form:{fkey:fkey,roomid:roomid}, jar:j}).then(function(response){
        url = JSON.parse(response.body).url
        return request.postAsync({url:"http://chat.stackexchange.com/chats/" + roomid + "/events", form:{fkey:fkey}, jar:j})
    }).then(function(response){
        time = JSON.parse(response.body).time;

        function send(text, prefix){
            request.post({
                url: "http://chat.stackexchange.com/chats/" +
                     roomid + "/messages/new",
                form: {
                    fkey: fkey,
                    text: prefix
                        ? prefix + ' ' + text
                        : text
                }, 
                jar: j
            });
        }
        var ws = new WebSocket(url + "?l=" + time, null, {
            headers: { 
                'Origin': 'http://chat.stackexchange.com'
            }
        });
        ws.on('message', function(message) {
            var m = JSON.parse(message);
            var keys = Object.keys(m);
            for (var i = 0; i < keys.length; i++){
                if (m[keys[i]].e){
                    m[keys[i]].e.forEach(function(ev){
                        handler.processEvent(ev);
                    });
                }
            }
        });
        ws.on('open', function() {
            console.log('opened')
        });
        ws.on('error', function(e) {
            console.log(e)
        });
    })
})
\$\endgroup\$
9
\$\begingroup\$

Very nice overall organization, use of promises, and use of constants where appropriate! Your code was a pleasure to read.

General comments:

  • You shouldn't implement convert(). It seems that you're receiving html from the WebSocket server. You should include a library to properly parse it and convert it to text. You already have cheerio, so you can use the .text() method to get just the raw text (with all the encoded entities properly converted). As a side suggestion, you could run this html through a HTML2Markdown converter so that the output doesn't lose the rich text formatting or the links
  • In general, it's probably a bad idea to hardcode a username and password into a script. I'd have it either passed in as a command line arg (with the password possibly entered interactively for security) which defaults to values stored in a dotfile in the user's home directory (something like ~/.sechatlogin)
  • As a matter of taste, and so that chat.js is more reusable, I'd forgo the console.log() in it and make chat into an event emitter. Then in ws.js you can listen for the specific events and output the proper text to stdout.
  • I see a lot of building URLs by concatenating strings. Since you can't be sure that some variables (which you receive from the server before concatenating into the URL) won't contain characters than need to be URL encoded, you're probably better off using a URL building library to handle this for you (look at url.format())
  • Since there are so many event types, it might be beneficial to extract their stringification logic into separate objects. So for each event create an object that knows how to print that event. Then, have chat.js convert a raw event object into a specialized event object depending on the type. This is a bit OO-ey, but it could be a nice abstraction if you need to use the events in other ways/places. The benefit of this, is that in ws.js you can just use chat.on('event', function(e) { console.log(e.toString()); }); to print events.

A few nitpicks:

  • You can define your constants with const instead of var
  • You should probably stick to ending all statements with semicolons or never ending statements with semicolons. I'd prefer the former, but whatever style you prefer pick one and stick to it!
  • There are a few mis-indented lines here and there
  • I would keep the number of hardcoded URLs to a minimum (try to read as many as you can from the returned content). This way your code is more future proof. For the ones you can't replace, turn them into constants with descriptive names instead of hardcoding them into the requests calls
  • LAST_EVENT should be camelCase since it changes
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I'm a bit curious on the HTML2Markdown idea, because it's the Node command line, and I didn't link it would support URLs and formatted text \$\endgroup\$ – Quill Jan 12 '16 at 0:38
  • 1
    \$\begingroup\$ I would suggest using environment variables over command line args for sensitive information, which can then be stored in ~/.bashrc (or similar). ssh is the method you'll probably be starting this, and that can be logged - you don't want a username/password ending up in your ssh logs. You could alternatively use the dotfile .env in your script's working directory by using the npm packagedotenv. \$\endgroup\$ – Dan Pantry Jan 12 '16 at 7:33
  • \$\begingroup\$ @Quill Overriding toString is fairly common: stackoverflow.com/questions/6307514/… It's bad to add methods to the prototype of a builtin like Object or Date, but for objects you define it should be fine. \$\endgroup\$ – Bailey Parker Jan 12 '16 at 8:25
  • 2
    \$\begingroup\$ @BaileyParker this is a great first javascript answer, by the way. I hope to see more of them :) \$\endgroup\$ – Dan Pantry Jan 12 '16 at 8:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.