Hey there folks,
a few weeks ago I was stumbling over CoinChat and was kind of impressed by it and quickly wanted to write a bot for a game I had in mind. First bot was done and I struggled with so many CoinChat stuff that I decided to write a coinchat-client module for easier re-use. The code for it can be found either on
GitHub or installed by using
npm install coinchat-client. Also,
if you have no experience writing JavaScript or have not yet touched anything Node.js, this tutorial probably isn't for you and you should start by getting into those two topics.
For the purpose of this Tutorial, we're going to create a bot which allows you to flip a coin and depending on the outcome, tip the user a custom amount. Unless you're running on a Unix based system I suggest you running a virtual machine with Linux on it. I suggest NOT to just copy/paste but variate some of the stuff I write to get a feeling for the framework.
Getting StartedWe start by creating a new project folder for our bot. Go to a directory of your choice, and type the following:
mkdir dicebot && cd dicebot
npm install coinchat-client
It will automatically install all dependencies of coinchat-client and you're ready to go.
Connecting to and logging in on CoinChatConnecting to coinchat really couldn't be any easier. First of, we're going to require the coinchat-client library and create a new instance of it. We need to pass an object to the coinchat-client so the framework knows its credentials (Note: You also may set the credentials at a later stage). After we have created a client, we're going to connect to the server. The connect method takes a callback which gets called when we are successfully connected to the server. In the callback, we're logging in with the specified credentials at the beginning which again, takes a callback which is called upon successful login.
var CoinChatClient = require('coinchat-client');
var client = new CoinChatClient({
username: 'foo',
password: 'bar'
});
console.log('Connecting to coinchat...');
client.connect(function() {
console.log('Connected. Logging in...');
client.login(function() {
console.log("WOOHOO! We're logged in!");
});
});
If you now run the exact code with the credentials given, you'll receive an exception. This is expected as the account is not registered and or the password is wrong. Both cases are an exit condition in which the code is expected to fail because it obviously cannot run in its current state (Note: You can use a try/catch around it but it really doesn't make sense). Change the credentials to your own ones and it should give you all debug messages thus showing a complete connection.
Handling MessagesThe framework wouldn't be any fun if it wouldn't deal with the messages. In it's current form it abstracts away all the nitty gritty details and lets you work with the messages. For this, the client instance provides the register method. It either takes a function or an object containing a method handleMessage to deal with the messages in a standardized form regardless if they have color enabled or not. Let's get to the details and enhance our example a bit. First, let us define a function which should act as our message handler.
function messageParser(msg) {
console.log("Received a message from "+msg.user+" in "+msg.room+" and the first word he said was "+msg.params[0]+". He received "+msg.tip+"mBTC for this.");
}
What this does is quite easy. It gets the message as a first parameter and writes a debug message to the console. Right now it doesn't work though because our client does not yet know the function. Let's extend the first code example and change it to look like this:
...
client.login(function() {
client.register(messageParser);
});
...
Now, every message that gets sent is received by our messageParser function and gets outputted to our command line. Well, ok. I lied. It works, but you won't see any message right now because you're not yet in any room! Let's fix this up...
Joining and leaving roomsNo fun without rooms. Let's make this quick. The client instance provides two methods for this, join and leave, both taking a rooms name as the first message. Inside your login callback type
and your bot will connect to the room. If you now write a message in there it will be outputted to your console. Isn't this great? What? You want to answer those messages? God you guys are hard to satisfy...
Writing messagesBots have a limitation when it comes to writing messages which is, don't send multiple messages in less than 550ms or your bot will get muted. The framework aids you in this that it has a so called message pump which queues and sends messages in an interval thus not getting muted. For this there are 3 methods:
client.pushMessage(roomName, message, [color]);
client.pushTip(roomName, userName, tipAmount, [message]);
client.pushPrivateMessage(userName, message);
Each of those methods will push a new message to the message queue which gets delivered then. Go on, try it out in the messageHandler function we wrote earlier (Note: pushTip does NOT work if your bots balance is below 0.25mBTC as this is the minimal tip amount).
Receiving tipsWhen a message reaches your message handler function, there is an attribute on it called isTip. Always check isTip in your message handler first. If isTip is set, there will also be the tipAmount on it.
Using an Object for message handlingBy now you should know most of the framework features and we're going to extend our example a bit. Remember earlier when I said you could use an object as a message handler? Well, now is the part where we are going to use exactly that. Let us first specify what our bot should be able to do. If you type !roll it should throw one 6 sided dice and output a message. If you type !roll 20 it should roll 20 6 sided die and if you do !roll 5 20 it should throw 5 20 sided die. As an admin only feature it should also have a shutdown command to disconnect from the server. Lets get started writing the bot in a traditional "OOP" manner:
var DiceBot = function(client) {
// save a local instance of the client
this.client = client;
// enter all admin names in lowercase here
this.admins = ['yourname']; // Save all admin names here
// this is used to save our balance
this.balance = 0;
// This should contain a mapping of your commands in all lowercase
this.commands = {
'!help': this.printHelp,
'!roll': this.rollDice
};
// It is a good practice to separate admin
// commands from normal commands by using
// another prefix so here's the shutdown command
this.adminCommands = {
'!#shutdown': this.shutdown
};
this.helpText = "Here goes your helptext";
};
/**
* Prints out the help when !help is received by a user
*/
DiceBot.prototype.printHelp = function(msg) {
this.client.pushMessage(msg.room, msg.user+": "+this.helpText);
return false;
};
/**
* This is what gets called by our client instance when
* registering it by using client.register
*/
DiceBot.prototype.handleMessage = function(msg) {
// First lets check if an admin wrote this
var admin = (this.admins.indexOf(msg.user.toLowerCase) != -1)
// Lets see if the message is a tip
if (msg.isTip) {
// ok, we don't need to actually do more here in this case
this.balance = msg.tipAmount;
this.client.pushMessage(msg.room, msg.user+': Thank you for your tip. Your contribution is greatly welcomed!');
return false;
}
// ok, here's the tricky part. We check the first parameter of the message
// and check if we have a command there. If we have one we call it and
// pass it the message as a parameter.
if (typeof this.commands[msg.params[0]] == 'function') {
return this.commands[msg.params[0]].call(this, msg);
}
// Then finally we do the same with the admin commands
// if it was an admin typing the message.
if (admin && typeof this.adminCommands[msg.params[0]] == 'function') {
return this.adminCommands[msg.params[0]].call(this, msg);
}
};
/**
* This checks the msg parameters and sets defaults if something isn't given.
*/
DiceBot.prototype.rollDice = function(msg) {
var nrDice = Number(msg.params[1]) || 1,
sides = Number(msg.params[2]) || 6,
rolls = [];
for (var i=0; i<nrDice; i++) {
rolls.push(1+Math.floor(Math.random()*sides));
}
// Give the user his dice roll
this.client.pushMessage(msg.room, msg.user+": You rolled "+nrDice+"D"+sides+" and got " + rolls.reduce(function(pv, cv) { return pv + cv; }, 0) + ". "+rolls.join(', '));
}
/**
* This shuts down the bot
*/
DiceBot.prototype.shutdown = function(msg) {
console.log('shutting down..');
process.exit();
return false;
};
module.exports = DiceBot;
Phew, that was a lot of code but you should understand it. The part you should notice in here is that we defined a handleMessage method. If we register an instance of the bot it will automatically call that message. Save that file in the same directory and name it "dicebot.js". Now, let's integrate this into our earlier sample.
var CoinChatClient = require('coinchat-client'),
DiceBot = require('./dicebot.js'),
client, bot;
client = new CoinChatClient({
username: 'foo',
password: 'bar'
});
// create a new bot instance and handover the client instance
// so we have access to the push methods.
bot = new DiceBot(client);
console.log('Connecting...');
client.connect(function() {
console.log('Connected. Logging in...');
client.login(function() {
console.log('Logged in. Joining room...');
// now we join a room
client.join('throwingdice');
// we implement a delay
setTimeout(function() {
client.register(bot);
}, 3000);
});
});
Run this code with your own credentials and everything should work. If you have any questions feel free to ask. If you want to add features to the framework just drop a pull request on GitHub and I will merge it. The tutorial can also be found on GitHub in the sample directory.
Note: If you're going to handle balances be sure to add in a persistence layer as MySQL or MongoDB. Losing user balances is a pain and can get you banned.
I hope you enjoyed this small tutorial and write some quality bots to contribute to coinchat.
Cheers,
Sargo Darya