From Ape Wiki
Contents |
[edit] How to build a server-side JS module
APE 1.0 now offers a Javascript binding through Tracemonkey (SpiderMonkey 1.8.1) from Mozilla. SpiderMonkey, which empowers the Firefox browser, is one of the fastest and most reliable JS engines available.
To the developer, this is a significant change. You no longer have to know how to write C in order to create highly scalable and functional APE-powered applications.
It is framework-agnostic in nature and fully configurable through `scripts/main.ape.js`, APE ships with the MooTools framework to ease the development and deployment of modules, due to its clean code, OOP patterns and server-side JavaScript support.
[edit] What can I do with server-side JavaScript ?
Manage :
- Users
- Channels
- Commands
- RAWs
- Events
- Pipes
- Socket API (both client and server)
- HTTP Requests API
[edit] Commands
[edit] RegisterCmd
You can register a new server command with the following :
Ape.registerCmd("foocmd", true, function(params, infos) { });
This will register a new APE Command which is called through an APE url :
/?[{"cmd":"foocmd","chl":1,"sessid":"ba162c29abfe2126329bbceaf09ae269","params":{"foo":"bar"}}]
Ape.registerCmd() requires 3 arguments.
- (string) The command name
- (bool) Does this command require the user to be logged in? (sessid needed)
- (function) the callback function to call
The callback function is called with 2 arguments:
- The first is the parameters list sent by the user (in this case {"foo":"bar"})
- The second is an object containing some useful information:
- host (string) Http header : "Host :"
- client (socket) the socket object of the client
- chl (number) the challange number
- ip (string) client's IP
- user (user) User object (if logged in)
- http (array) contains user HTTP headers
Example:
Ape.registerCmd("foocmd", true, function(params, infos) { Ape.log("The user ip : ("+infos.ip+"), foo : " + params.foo); });
[edit] Send an error
You just need to return an array with two elements:
- (string) error code
- (string) error description
Ape.registerCmd("foocmd", true, function(params, infos) { if (!$defined(params.john)) return 0; // send a "BAD_PARAMS" RAW to the user if (params.john != "doe") return ["209", "NOT_A_JOHN_DOE"]; return 1; });
The user will receive:
[{"time":"1255399359","raw":"ERR","data":{"code":"209","value":"NOT_A_JOHN_DOE"}}]
[edit] RegisterHookCmd
Hook an existing command (to add some arguments, for instance)
Ape.registerHookCmd("foocmd", function(params, infos) { if (!$defined(params.john)) return 0; return 1; });
As in the error example, return 0 will send a "BAD_PARAMS" RAW to the user.
Since there is sometimes no user object in infos (in the case of a "non sessid" command) there is a special syntax to send error or custom raw.
[edit] Send a custom raw
You need to return a special formatted object:
- raw
- name: (string) Raw name
- data: (object) Raw content
Ape.registerHookCmd("foocmd", function(params, infos) { if (!$defined(params.john)) return 0; if (params.john != "doe") return ["209", "NOT_A_JOHN_DOE"]; return { 'raw':{'name':'CUSTOM_RAW','data':{'foo':'bar'}} }; });
The user will receive:
[{"time":"1255399359","raw":"CUSTOM_RAW","data":{"foo":"bar"}}]
[edit] RAW
A RAW is a "message" that you can send to a pipe (add it to the pipe's message queue).
A message is formatted like the following:
- RAW.time (string) Current timestamp
- RAW.raw (string) The raw name
- RAW.data (mixed) Data sent by the server
Example:
{"time":"1255281320","raw":"FOOBAR","data":{"foo":"bar","anything":["a","b"]}}
[edit] sendRaw
Each APE entity has a pipe (users, channels, proxy, and so on).
pipe.sendRaw("CUSTOM_RAW", {"foo":"bar"});
This will send the "CUSTOM_RAW" RAW to pipe.
{"time":"1255281320","raw":"CUSTOM_RAW","data":{"foo":"bar"}}
In addition to our previous example:
Ape.registerCmd("foocmd", true, function(params, infos) { Ape.log("The user ip : ("+infos.ip+"), foo : " + params.foo); infos.user.pipe.sendRaw("CUSTOM_RAW", {"foo":"bar"}); });
You can retrieve a pipe by its pubid:
Ape.registerCmd("foocmd", true, function(params, infos) { Ape.log("The user ip : ("+infos.ip+"), foo : " + params.foo); Ape.getPipe(params.pubid).sendRaw("CUSTOM_RAW", {"foo":"bar"}); });
This will send the RAW to the pipe which has a pubid that matches params.pubid
[edit] sendResponse
With registerCmd or registerHookCmd you sometimes need to send a response.
Indeed, you could use a simple infos.user.pipe.sendRaw, but the client can't handle a callback response on the command that it sent.
To fill this gap, you can directly use:
Ape.registerCmd("foocmd", true, function(params, infos) { infos.sendResponse('custom_raw', {'foo':'bar'}); });
This will add the chl received from the command to the RAW.
[edit] Events
You can listen for "special events", such as :
- adduser: function(user) When a user connects to APE
- deluser: function(user) When a user disconnects
- beforeJoin / join / afterJoin: function(user, channel) When a user joins a channel
- left: function(user, channel) When a user leaves a channel
- mkchan: function(channel) When a new channel is created
- rmchan: function(channel) When a channel is deleted
Ape.addEvent("adduser", function(user) { Ape.log("New user :)"); }); Ape.addEvent("join", function(user, channel) { Ape.log("New user has joined the channel ("+channel.getProperty('name')+") :)"); });
Note that all objects passed to Events are persistent. This means that you can store private data inside user, channel, ...
Ape.addEvent("adduser", function(user) { Ape.log("New user :)"); user.foo = "bar"; //Like this. }); Ape.addEvent("join", function(user, channel) { Ape.log("New user "+user.foo+" joined the channel ("+channel.getProperty('name')+") :)"); });
[edit] Sockets
APE JS Server-Side provides a complete API for both server and client sockets
What does this mean?
You can write powerful applications such as:
- IRC bots
- HTTP querying
- webhooks
- writting your own APE protocol (by writing a socket server and handle events)
All sockets are non-blocking and event driven => High I/O performances ;)
[edit] Client
Steps :
var socket = new Ape.sockClient(port, host, {flushlf: true});
Here we are connecting to host:port.
flushlf (bool) means that onRead event is fired only when a \n is received (data is split around it) e.g. foo\nbar\n will call onRead two times with "foo" and "bar". Otherwise, onRead is fired as data comes.
socket.onConnect = function() { Ape.log("We are connected !"); this.write("Hello\n"); }
This defines the callback to call when the connection is ready. this object refers to the socket itself.
socket.onRead = function(data) { Ape.log("Data : " + data); }
This callback is fired when new data is ready. If flushlf was set up to true, trailing '\n' chars are not present in data
socket.onDisconnect = function() { Ape.log("Gone !"); }
onDisconnect is fired when the connection closes. (By the client or the server).
To close the connection, use:
socket.close()
To write data, use:
socket.write(data)
[edit] Server
Steps:
var socket = new Ape.sockServer(port, "0.0.0.0", {flushlf: true});
This binds "0.0.0.0" (all IPs attributed to the machine) on the given port.
socket.onAccept = function(client) { Ape.log("New client !"); client.write("Hello world\n"); }
onAccept is fired when a new client is connecting.
Other callbacks (same API than socketClient) :
- onRead = function(client, data){}
- onDisconnect = function(client)
- client.close()
- client.write(data);
Note that you can store private data to the client objects and retrieve them on other callbacks :
socket.onAccept = function(client) { client.foo = "bar"; client.write("Hello world\n"); } socket.onRead = function(client, data) { Ape.log(client.foo); }
[edit] Users
A user is created when a client sends a "CONNECT" command and succeeds to log in.
You can do several actions through the Javascript user object:
- setting/getting public or private properties
- Sending RAW's
[edit] setProperty
This function sets a public property that is sent to each other users in relation to this user (same channels, private messages).
user.setProperty('foo', 'bar');
The foo property can now be retrieved on the client-side (JSF).
The value can be either a string, an integer or an object/Array
[edit] getProperty
This function get a public property.
var prop = user.getProperty('foo');
[edit] Private properties
The javascript user object is created when a user connects to APE and destroyed when they leave or are disconnected by the server.
This object is persistent. This means that you can store anything into it and retrieve it later.
Ape.addEvent("adduser", function(user) { user.foo = "bar"; }); Ape.registerCmd("helloworld", function(params, infos) { Ape.log(infos.user.foo); });
[edit] getUserByPubid
To retrieve a user by its pubid:
var user = Ape.getUserByPubid(pubid);
[edit] User pipe
Each user object has a pipe object in order to send it a RAW.
user.pipe.sendRaw("RAW", {"foo":"bar"});
[edit] join
Force a user to join a channel :
user.join('mychannel'); /* can be either a channel name or a channel object */
[edit] left
Force a user to leave a channel :
user.left('mychannel'); /* can be either a channel name or a channel object */
[edit] Channels
channel objects work just like user objects.
You can set/get private or public properties.
[edit] getChannelByName
To retrieve a channel object by its name:
var channel = Ape.getChannelByName('foochannel'); channel.setProperty('foo', 'bar'); channel.myprivate = {'my':'private'}; // Can be a string or whatever you want channel.pipe.sendRaw('FOORAW', {'John':'Doe'}); Ape.addEvent('beforeJoin', function(user, channel) { Ape.log('My private : ' + channel.myprivate); });
[edit] getChannelByPubId
To retrieve a channel object by its pubid :
var channel = Ape.getChannelByPubid(pubid);
[edit] mkChan
Create a new channel :
var channel = Ape.mkChan('foo');
[edit] delChan
Delete an existing channel :
var channel = Ape.delChan('foo'); /* Can be a string or a channel object */
[edit] Pipes
As seen before, a pipe is an object that is a kind of connector through which RAWs are sent on.
Each pipe has a unique identifier, pubid, which can be retrieved using:
pipe.getProperty('pubid');
The server-side JS offers a way to create your own pipe and define its behavior :
var mypipe = new Ape.pipe();
This initiate a new pipe where users can send data via the SEND Command.
{"cmd":"SEND","chl":1,"sessid":"04ad0814f987e5f9891bffd6a73ef5a1","params":{"pipe":"6a3ae905fb508aff6f1e84458038f262","data":{"foo:"bar"}}}
// Where "pipe" property is the pubid of a pipe
To handle this command, pipe objects provide a simple callback :
var mypipe = Ape.pipe(); mypipe.onSend = function(user, params) { Ape.log(params.data.foo); /* doSomething(); */ }
You can also set private/public properties on a pipe just like on a user or channel object.
- pipe.setProperty('key', val);
- pipe.getProperty('key');
- pipe.myprivate
[edit] MySQL
APE server allow you to connect to your MySQL database.
[edit] Connecting
var sql = new Ape.MySQL("ip:port", "user", "password", "database");
you must specify the port, by default mysql use 3306
Tips : You can use the local MySQL Unix socket by giving /var/run/mysqld/mysqld.sock as hostname
Connect callback
- onConnect : Callback fired when connection to mysql server is sucessfuly etablished
sql.onConnect = function() { Ape.log('Connected to mysql server'); }
- onError : Callback fired when a connection error occured
sql.onError = function(errorNo) { Ape.log('Connection Error : ' + errorNo + ' : '+ this.errorString()); }
[edit] Sending a request
Select request :
sql.query("SELECT * FROM table", function(res, errorNo) { if (errorNo) Ape.log('Request error : ' + errorNo + ' : '+ this.errorString()); else { Ape.log('Fetching ' + res.length); res.each(function(data) { Ape.log(data.content);//data.<column name> or data[column_name] }); } });
Insert request :
sql.query("INSERT INTO table VALUES('a','b','c')", function(res, errorNo) { if (errorNo) Ape.log('Request error : ' + errorNo + ' : '+ this.errorString()); else Ape.log('Inserted ' + this.getInsertId()); });
Tips : to get the last auto-incremeted value use this.getInsertId() in the callback function
[edit] Escaping
To prevent SQL injections you must escape you input data with Ape.MySQL.escape() :
sql.query("SELECT nick FROM user WHERE login = '"+Ape.MySQL.escape(mylogin)+"'");
[edit] Utils
APE server-side JS provides these useful natives functions:
[edit] Base64
Encodes/Decodes data with MIME base64
var xxx = Ape.base64.encode('foo'); var foo = Ape.base64.decode(xxx);
[edit] SHA1
Calculate the sha1 hash of a string (or a binary).
[edit] sha1.str()
Returns a 40-character hexadecimal number.
var result = Ape.sha1.str("hello world");
You can also get a HMAC-SHA1 result by giving the secret key as second argument:
var result = Ape.sha1.str("hello world", "mysecretkey");
[edit] sha1.bin()
The sha1 digest is instead returned in raw binary format with a length of 20:
var result = Ape.sha1.bin("hello world");
(Note: You can get a HMAC_SHA1 result using it like sha1.str()).
[edit] Xorize
Apply a 'XOR' between two string (or binary) :
var result = Ape.xorize("key1", "key2");
Algorithm internally used :
for (i = 0; i < key1_len; i++) { returned[i] = key1[i] ^ key2[i]; }
(Note: the second argument's length must be higher than the first argument's length.)
[edit] Timers
Javascript doesn't provide any timer functions on its own. APE provides a timer API just like browsers does (with the same API as Firefox).
[edit] setTimeout
Executes a function after the specified delay (milliseconds).
var timeoutID = Ape.setTimeout(func, delayms, [param1, param2, ...]);
var timeoutID = Ape.setTimeout(function(a, b) { Ape.log("Foo : " + a + " Bar : " + b); }, 3000, "foo", "bar");
[edit] setInterval
Calls a function repeatedly, with a fixed time delay between each call
var timeoutID = Ape.setInterval(func, delay[, param1, param2, ...]);
[edit] clearTimeout
Stop the timer set by Ape.setTimeout() or Ape.setInterval().
Ape.clearTimeout(timeoutID)
var timeoutID = Ape.setInterval(function(a, b) { Ape.log("Foo : " + a + " Bar : " + b); }, 3000, "foo", "bar"); Ape.clearTimeout(timeoutID);
[edit] include
Execute the given file in the current context.
include('./scripts/foo.js'); Ape.log('some variable set in foo.js : ' + myfoovar);
[edit] Case study
Proxy.js :
Ape.registerCmd("PROXY_CONNECT", true, function(params, infos) { if (!$defined(params.host) || !$defined(params.port)) { return 0; } var socket = new Ape.sockClient(params.port, params.host); socket.chl = infos.chl; socket.onConnect = function() { /* "this" refers to the socket object */ /* Create a new pipe (with a pubid) */ var pipe = new Ape.pipe(); infos.user.proxys.set(pipe.getProperty('pubid'), pipe); /* Set some private properties */ pipe.link = socket; pipe.nouser = false; this.pipe = pipe; /* Called when an user send a "SEND" command on this pipe */ pipe.onSend = function(user, params) { /* "this" refer to the pipe object */ this.link.write(Ape.base64.decode(params.msg)); } pipe.onDettach = function() { this.link.close(); } /* Send a PROXY_EVENT raw to the user and attach the pipe */ infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "connect", "chl": this.chl}, {from: this.pipe}); } socket.onRead = function(data) { infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "read", "data": Ape.base64.encode(data)}, {from: this.pipe}); } socket.onDisconnect = function(data) { if ($defined(this.pipe)) { if (!this.pipe.nouser) { /* User is not available anymore */ infos.user.pipe.sendRaw("PROXY_EVENT", {"event": "disconnect"}, {from: this.pipe}); infos.user.proxys.erase(this.pipe.getProperty('pubid')); } /* Destroy the pipe */ this.pipe.destroy(); } } return 1; }); Ape.addEvent("deluser", function(user) { user.proxys.each(function(val) { val.nouser = true; val.onDettach(); }); }); Ape.addEvent("adduser", function(user) { user.proxys = new $H; })


