43
\$\begingroup\$

The challenge

The goal of this challenge is to create a chatbot that can run in the chatrooms of Stack Exchange. Your bot needs to be able to detect when specific commands are posted by a user and respond to it. This is the list of commands, and what your bot should do:

  • !!newest: output the title (no link, but the title) of the newest question posted on this site (codegolf.SE).
  • !!metanewest: output the title of the newest question posted on the meta site (meta.codegolf.SE).
  • !!questioncount: output the current question count.
  • !!metaquestioncount: output the current question count on the meta site.
  • !!tag tagname: output the tag excerpt (the short description) of the tag that's given as the first parameter.
  • !!metatag tagname: same as above, but for the meta site.
  • !!featured: output the count of questions that currently have a bounty.
  • !!metafeatured: output the count of questions that have the [featured] tag on Meta.

Rules

  1. You should write a complete program, not a snippet or function.

  2. In case it's necessary, you can request username and password as input (prompting for input, STDIN, command-line arguments). This will be necessary if you use, for example, Python or Ruby, but it won't be necessary if you use JavaScript and run the script on the chat room page itself.

  3. You are allowed to use external libraries to do stuff like WebSockets. These libraries do not have to count for your character count.

  4. You can use an external chat wrapper (but you don't have to, writing your own is encouraged), and then that has to count for the character count. You also are not allowed to change the wrapper's code. If you use it, you use it without modifications and all characters have to be counted (that's as a penalty for not writing your own wrapper).

    Only the code of the wrapper itself has to count. If there are other files such as examples, these don't have to count.

  5. No use of URL shorteners or other ways that can make URLs shorter: the challenge is to golf a chatbot, not to golf a URL.

  6. No web requests, except those necessary to chat and to get the information necessary to respond to the commands.

  7. Use of the Standard "loopholes" is not allowed.

  8. If someone posts a command, you need to respond with a chat message of this format: @user response. So, if I write the command !!featured and there are 5 featured questions, your bot should post @ProgramFOX 5.

  9. If I test your bot, I'll run it from my chatbot account and I'll run it in this chatroom. I will always test the bots in that room, so it is not necessary to provide room ID as input, it will always be 14697. This ID won't be given as input, it should be hard-coded.

  10. If the command is not found, output @user The command [command] does not exist. Replace [command] by the name of the non-existing command. If arguments are provided to the command, don't output the arguments, only the command name.

  11. If a command has to many arguments, ignore the arguments that are not necessary.

  12. If a command has not enough arguments, output @user You have not provided enough arguments

  13. The system prevents that duplicate messages are posted within a short time range. So, when testing your bot, I will never run two commands that give the same output successively (which means that you do not have to implement a system that makes messages different if they are duplicates, by adding a dot for example).

  14. The system prevents that too many messages get posted within a short time range, so when testing, I will never send too many commands within a short time range, which means that your bot does not have to take care of this (by waiting some time before posting, for example).

  15. This is , the program with the least amount of bytes wins.

Getting started

Here is some info to get started with writing your bot. You don't have to use this, but it can be a guidance.

  • To log in, first log in to an OpenID provider. This will always be Stack Exchange OpenID (https://openid.stackexchange.com). The login form is located at https://openid.stackexchange.com/account/login, and provide the username and password there.

  • Then, login to stackexchange.com. The login form is located at https://stackexchange.com/users/login. Choose Stack Exchange as OpenID provider.

  • After doing that, log in to chat. The login form for that is located at http://stackexchange.com/users/chat-login. Choose Stack Exchange as OpenID provider.

  • Then you need to get your fkey. For that, go to http://chat.stackexchange.com/chats/join/favorite and get the fkey from an hidden input field.

  • To post a message, send a request to http://chat.stackexchange.com/chats/14697/messages/new, and provide two POST parameters: a text parameter containing the message text, and a fkey parameter containing the fkey.

  • To see when a new message is posted, you can use WebSockets (but don't have to, feel free to use something else if it's shorter). Please see this Meta Stack Exchange answer:

    Chat

    (wss://chat.sockets.stackexchange.com/events/<roomnumber>/<somehash>?l=<timethingy>)

    The hash can be fetched by POSTing the room id and fkey to http://chat.stackexchange.com/ws-auth

    The timethingy is the time key of the json returned by /chats/<roomno>/events.

    The event ID when a message is posted is 1.

  • It is useful to look at the existing chat-wrappers, such as Doorknob's StackExchange-Chatty and Manishearth's ChatExchange, to see how it exactly works.

\$\endgroup\$
21
  • 3
    \$\begingroup\$ The moment I saw the title, I instantly thought "ah, ProgramFOX". \$\endgroup\$
    – seequ
    Jun 26, 2014 at 13:10
  • \$\begingroup\$ I was hoping metafeatured would mean bountied questions on meta, but... thanks :-) \$\endgroup\$ Jun 26, 2014 at 13:33
  • \$\begingroup\$ @JanDvorak Per-site metas don't have bounties, so I can't use that. When I wrote this challenge, I forgot that Meta had a [featured] tag, so thanks for your suggestion! \$\endgroup\$
    – ProgramFOX
    Jun 26, 2014 at 13:34
  • \$\begingroup\$ What I did to see if a new message was posted was check every 2 seconds via JS if the last message wasn't by me (last item in class) \$\endgroup\$
    – Cilan
    Jun 26, 2014 at 13:49
  • \$\begingroup\$ We already have one over here \$\endgroup\$
    – Mr. Alien
    Jun 28, 2014 at 6:20

1 Answer 1

17
+100
\$\begingroup\$

JavaScript + jQuery, 1362 1258 bytes

Golfed using a minifier:

$(function(){function e(){function e(e,t){$("#input").val("@"+$(e).parents(".user-container").find(".username").eq(0).text()+" "+t),$("#sayit-button").click()}var i,a=$(t),s=a.map(function(e,t){return t.id}),r=s.slice(-1)[0]
n!=r&&(i=a.slice($.inArray(n,s)+1),n=r,i.map(function(t,n){var i,a,s,r,o,u,c,f=n.textContent.match(/!!(\S+)(?:\s+(\S+))?/)
if(f){switch(i=f[1],a=f[2],s="codegolf",0==i.indexOf("meta")&&(s="meta."+s,i=i.slice(4)),r="?site="+s,c=0,i){case"newest":o=["questions","&order=desc&sort=creation"],u=function(e){return e.items[0].title}
break
case"questioncount":o=["info",""],u=function(e){return e.items[0].total_questions}
break
case"tag":if(!a){c=1
break}o=["tags/"+a+"/wikis",""],u=function(e){return 0==e.items.length?"Tag not found":e.items[0].excerpt}
break
case"featured":o=0==s.indexOf("meta.")?["questions","&tagged=featured"]:["questions/featured",""],u=function(e){var t=e.items.length
return(e.items.has_more?"more than ":"")+t}}c?e(n,"You have not provided enough arguments"):o?$.get("http://api.stackexchange.com/2.2/"+o[0]+r+o[1],function(t){e(n,u(t))}):e(n,"The command "+i+" does not exist")}}))}var t="[id^=message-]",n=$(t).eq(-1).attr("id")
new MutationObserver(e).observe($("#chat").get(0),{childList:!0,subtree:!0})})

You have to run the script directly in the browser (using Stack Exchange's jQuery works):

  1. Open http://chat.stackexchange.com/rooms/14697/chatbot-challenge-on-programming-puzzles-code-golf
  2. Paste the above code in the console
  3. Enter some commands in the chat

It could be golfed a lot more, but couldn't be bothered.


Un-golfed:

$(function() {
    var sel = '[id^=message-]';
    var latestMessage = $(sel).eq(-1).attr('id');
    function update() {
        var messages = $(sel);
        var ids = messages.map(function(i, x) { return x.id; });
        var newest = ids.slice(-1)[0];
        if(latestMessage == newest) {
            return;
        }
        var newMessages = messages.slice($.inArray(latestMessage, ids) + 1);
        latestMessage = newest;
        newMessages.map(function(i, x) {
            var m = x.textContent.match(/!!(\S+)(?:\s+(\S+))?/);
            if(!m) {
                return;
            }
            var c = m[1];
            var a = m[2];
            var s = 'codegolf';
            if(c.indexOf('meta') == 0) {
                s = 'meta.' + s;
                c = c.slice(4);
            }
            var site = '?site=' + s;
            var url;
            var extractor;
            var too_few_args = 0;
            switch(c) {
                case 'newest':
                    var url = ['questions', '&order=desc&sort=creation'];
                    extractor = function(data) {
                        return data.items[0].title;
                    };
                    break;
                case 'questioncount':
                    url = ['info', ''];
                    extractor = function(data) {
                        return data.items[0].total_questions;
                    };
                    break;
                case 'tag':
                    if(!a) {
                        too_few_args = 1;
                        break;
                    }
                    url = ['tags/' + a + '/wikis', ''];
                    extractor = function(data) {
                        if(data.items.length == 0) {
                            return 'Tag not found';
                        }
                        return data.items[0].excerpt;
                    };
                    break;
                case 'featured':
                    url = s.indexOf('meta.') == 0? ['questions', '&tagged=featured']: ['questions/featured', ''];
                    extractor = function(data) {
                        var l = data.items.length;
                        return (data.items.has_more? 'more than ': '') + l;
                    }
                    break;
            }
            if(too_few_args) {
                write(x, 'You have not provided enough arguments');
            } else if(!url) {
                write(x, 'The command ' + c + ' does not exist');
            } else {
                $.get('http://api.stackexchange.com/2.2/' + url[0] + site + url[1], function(data) {
                    write(x, extractor(data));
                });
            }
        });

        function write(x, m) {
            $('#input').val('@' + $(x).parents('.user-container').find('.username').eq(0).text() + ' ' + m);
            $('#sayit-button').click();
        }
    }
    new MutationObserver(update).observe($('#chat').get(0), {childList: true, subtree: true});
});
\$\endgroup\$
4
  • \$\begingroup\$ Nice, thanks for posting an answer here! I'm now going to run the tests in the room. Anyway, I think you can save some characters by using more one-letter variables and avoiding update and latestMessage. \$\endgroup\$
    – ProgramFOX
    Dec 18, 2014 at 6:24
  • \$\begingroup\$ Great! You passed all tests. The only weird thing I noticed was that your bot returned a different question count than on the homepage, but I saw that the API returned that number, so I reported this on Meta and marked the test case as correct. Well done! +1 \$\endgroup\$
    – ProgramFOX
    Dec 18, 2014 at 6:40
  • \$\begingroup\$ I saw you made the bot shorter. Nice! :) I re-tested it and you still pass all tests. \$\endgroup\$
    – ProgramFOX
    Dec 19, 2014 at 10:09
  • \$\begingroup\$ Rather late, but I just found a one-char improvement: you can replace 0==e.items.length with 1>e.items.length because the length will never go below zero. \$\endgroup\$
    – ProgramFOX
    May 27, 2015 at 15:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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