oscar
Version:
An OSCAR protocol module for node.js
1,399 lines (1,301 loc) • 156 kB
JavaScript
var net = require('net'),
util = require('util'),
EventEmitter = require('events').EventEmitter,
crypto = require('crypto');
var fnEmpty = function() {};
var debug = fnEmpty, hexy, inspectMutated = false,
hexyFormat = {
caps: 'upper',
format: 'twos',
numbering: 'none',
groupSpacing: 2
};
function OscarConnection(options) {
if (!(this instanceof OscarConnection))
return new OscarConnection(options);
EventEmitter.call(this);
this._options = {
connection: {
username: '',
password: '',
host: SERVER_AOL,
port: 5190,
connTimeout: 10000, // connection timeout in msecs
allowMultiLogin: true,
debug: false
}, other: {
initialStatus: USER_STATUSES.ONLINE,
initialFlags: USER_FLAGS.DCDISABLED
}
};
this._options = extend(true, this._options, options);
if (typeof this._options.connection.debug === 'function') {
debug = this._options.connection.debug;
if (!inspectMutated) {
inspectMutated = true;
hexy = require('./hexy').hexy;
Buffer.prototype.inspect = function () {
return hexy(this, hexyFormat);
};
}
}
this._state = {
connections: {},
serviceMap: {},
reqID: 0, // 32-bit number that identifies a single SNAC request
status: this._options.other.initialStatus,
flags: this._options.other.initialFlags,
requests: {},
isAOL: (this._options.connection.host.substr(this._options.connection.host.length-7).toUpperCase() === 'AOL.COM'),
rateLimitGroups: {},
rateLimits: {},
svcInfo: {},
svcPaused: {},
SSI: {},
iconQueue: {},
rndvCookies: { out: {}, in: {} },
chatrooms: {},
p2p: {}
};
this.icon = { datetime: undefined, data: undefined }; // my 'buddy' icon
this.me = undefined;
this.contacts = { lastModified: undefined, list: undefined, permit: undefined, deny: undefined, prefs: undefined, _totalSSICount: 0, _usedIDs: {} };
}
util.inherits(OscarConnection, EventEmitter);
// setIdle only needs to be called once when you are idle and again when you are no longer idle.
// The server will automatically increment the idle time for you, so don't call setIdle every second
// or the evil AOL wizards will come for you!
// Just kidding about the wizards, but it will make OSCAR do funny things.
OscarConnection.prototype.setIdle = function(amount) { // amount is in seconds if an integer is supplied, false disables the idle state
if (typeof amount === 'boolean' && !amount)
amount = 0;
else if (typeof amount !== 'number' || amount <= 0 || amount === true)
throw new Error('Amount must be boolean false or a positive number > 0');
this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.GENERIC, 0x11, NO_FLAGS,
[(amount >> 24 & 0xFF), (amount >> 16 & 0xFF), (amount >> 8 & 0xFF), (amount & 0xFF)]
)
));
};
OscarConnection.prototype.sendIM = function(who, message, flags, cb) {
var msgData, cookie, msgLen, self = this, features = (self._state.isAOL ? [0x01, 0x01, 0x01, 0x02] : [0x01]),
featLen = features.length, charset = ICBM_MSG_CHARSETS.ASCII, isSMS;
cb = arguments[arguments.length-1];
if (typeof flags !== 'number')
flags = 0x00000000;
if (typeof who === 'object')
who = who.name;
isSMS = /\+[\d]+/.test(who);
if (isSMS && !self._state.isAOL) {
var uin = parseInt(this._options.connection.username), len, data,
req = str2bytes('<icq_sms_message><destination>' + who + '</destination><text>' + message + '</text>'
+ '<codepage>1252</codepage><senders_UIN>' + uin + '</senders_UIN>'
+ '<senders_name>' + this.me.fullname + '</senders_name>'
+ '<delivery_receipt>' + (typeof cb === 'function' ? 'Yes' : 'No') + '</delivery_receipt>'
+ '<time>' + (new Date).toUTCString() + '</time>'
+ '</icq_sms_message>');
data = splitNum(uin, 4).reverse().concat([
0xD0, 0x07, (this._state.reqID & 0xFF) << 8, (this._state.reqID >> 8 & 0xFF), 0x82, 0x14,
0x00, 0x01, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
(req.length >> 8 & 0xFF), (req.length & 0xFF)
]).concat(req);
data.push(0x00);
len = data.length;
data.unshift(len >> 8 & 0xFF);
data.unshift(len & 0xFF);
self._send(self._createFLAP(self._state.serviceMap[SNAC_SERVICES.ICQ_EXT], FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.ICQ_EXT, 0x02, NO_FLAGS,
self._createTLV(0x01, data)
)
), cb);
return;
} else if (isSMS && (flags & ICBM_MSG_FLAGS.OFFLINE))
flags -= ICBM_MSG_FLAGS.OFFLINE;
cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4));
who = str2bytes(who);
if (who.length > MAX_SN_LEN) {
var err = new Error('Screen names cannot be longer than ' + MAX_SN_LEN + ' characters');
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
message = str2bytes(''+message);
if (message.length > MAX_MSG_LEN) {
// TODO: try stripping message of any HTML to see if it then fits within the length limit
var err = new Error('IM messages cannot be longer than ' + MAX_MSG_LEN + ' characters');
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
msgLen = message.length + 4;
msgData = [0x05, 0x01, (featLen >> 8 & 0xFF), (featLen & 0xFF)]
.concat(features)
.concat([0x01, 0x01, (msgLen >> 8 & 0xFF), (msgLen & 0xFF),
(charset >> 8 & 0xFF), (charset & 0xFF), 0x00, 0x00]);
var content = cookie.concat([0x00, 0x01, who.length])
.concat(who)
.concat(self._createTLV(0x02, msgData.concat(message)));
if (flags & ICBM_MSG_FLAGS.AWAY)
content = content.concat(self._createTLV(0x04));
else {
// request that the server send us an ACK that the message was sent ok,
// but not necessarily immediately received by the destination user (i.e. they are offline)
if (typeof cb === 'function')
content = content.concat(self._createTLV(0x03));
if (flags & ICBM_MSG_FLAGS.OFFLINE)
content = content.concat(self._createTLV(0x06));
}
if (flags & ICBM_MSG_FLAGS.REQ_ICON)
content = content.concat(self._createTLV(0x09));
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS,
content
)
), cb);
};
OscarConnection.prototype.setProfile = function(text) {
var self = this, maxProfileLen = self._state.svcInfo[SNAC_SERVICES.LOCATION].maxProfileLen;
if (text.length > maxProfileLen)
throw new Error('Profile text cannot exceed max profile length of ' + maxProfileLen + ' characters');
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.LOCATION, 0x04, NO_FLAGS,
self._createTLV(0x01, str2bytes('text/aolrtf; charset="us-ascii"'))
.concat(self._createTLV(0x02, str2bytes(text)))
)
));
};
OscarConnection.prototype.warn = function(who, isAnonymous, cb) {
isAnonymous = (typeof isAnonymous !== 'boolean' ? true : isAnonymous);
cb = arguments[arguments.length-1];
if (self._state.isAOL) {
var msgLen, self = this;
who = str2bytes(''+who);
if (who.length > MAX_SN_LEN) {
var err = new Error('Screen names cannot be longer than ' + MAX_SN_LEN + ' characters');
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.ICBM, 0x08, NO_FLAGS,
[0x00, (isAnonymous ? 0x01 : 0x00), who.length].concat(who)
)
), function(e, gain, newLevel) { if (typeof cb === 'function') cb(e, gain, newLevel); });
} else {
var err = new Error('Warn feature only available on AOL');
if (typeof cb === 'function')
cb(err);
else
throw err;
}
};
OscarConnection.prototype.notifyTyping = function(who, which) {
var self = this;
who = str2bytes(''+who);
if (who.length > MAX_SN_LEN)
throw new Error('Screen names cannot be longer than ' + MAX_SN_LEN + ' characters');
var notifyType = [0x00];
if (typeof which !== 'undefined')
notifyType.push((which ? 0x02 : 0x00));
else
notifyType.push(0x01);
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.ICBM, 0x14, NO_FLAGS,
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, who.length]
.concat(who)
.concat(notifyType)
)
));
};
OscarConnection.prototype.addContact = function(who, group, cb) {
var self = this, check, err, record, skipTrans = (typeof arguments[arguments.length-1] === 'boolean'), contactCount = 0;
cb = (typeof group === 'function' ? group : cb);
for (var i=0,groups=Object.keys(self.contacts.list),len=groups.length; i<len; i++)
contactCount += Object.keys(self.contacts.list[groups[i]].contacts).length;
if (contactCount < self._state.svcInfo[SNAC_SERVICES.SSI].maxContacts) {
record = (typeof who !== 'object' ? { name: '' } : who);
if (typeof group !== 'function')
record.group = group;
if (typeof who === 'string')
record.name = who;
check = self._SSIFindContact(record.group, record.name);
if (check[1] > -1)
err = 'That contact already exists';
else if (check[0] === -1)
err = 'Group not found';
} else
err = 'Maximum number of contacts reached';
if (err) {
err = new Error(err);
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
record.type = 0x00;
record.group = check[0];
record.item = -1;
if (!skipTrans)
self._SSIStartTrans();
self._SSIModify(record, 0, function(e) {
if (e) {
if (!skipTrans)
self._SSIEndTrans();
if (typeof cb === 'function')
cb(e);
return;
}
self.contacts.list[record.group].contacts[record.item] = record;
self._SSIModify(self.contacts.list[record.group], 2, function(e) {
if (!skipTrans)
self._SSIEndTrans();
if (!e)
self.contacts._usedIDs[record.group][record.item] = true;
if (typeof cb === 'function')
cb(e);
});
});
};
OscarConnection.prototype.delContact = function(who, group, cb) {
var self = this, check, record, skipTrans = (typeof arguments[arguments.length-1] === 'boolean');
cb = (typeof group === 'function' ? group : cb);
record = (typeof who !== 'object' ? { name: '' } : who);
record.group = (typeof group !== 'function' ? group : undefined);
if (typeof who === 'string')
record.name = who;
check = self._SSIFindContact(record.group, record.name);
if (check[1] === -1) {
var err = new Error('That contact doesn\'t exists');
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
record.type = 0x00;
record.group = check[0];
record.item = check[1];
if (!skipTrans)
self._SSIStartTrans();
self._SSIModify(record, 1, function(e) {
if (e) {
if (!skipTrans)
self._SSIEndTrans();
if (typeof cb === 'function')
cb(e);
return;
}
delete self.contacts.list[record.group].contacts[record.item];
self._SSIModify(self.contacts.list[record.group], 2, function(e) {
if (!skipTrans)
self._SSIEndTrans();
if (!e)
delete self.contacts._usedIDs[record.group][record.item];
if (typeof cb === 'function')
cb(e);
});
});
};
OscarConnection.prototype.moveContact = function(who, newGroup, cb) {
var self = this, check, err, record;
cb = arguments[arguments.length-1];
if (typeof who !== 'object')
record = { name: '' };
if (typeof who === 'string')
record.name = who;
check = self._SSIFindContact(record.group, record.name);
newGroup = self._SSIFindGroup(newGroup);
if (check[1] === -1)
err = 'That contact doesn\'t exist';
else if (newGroup === -1)
err = 'Destination group not found';
if (err) {
err = new Error(err);
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
var contact = self.contacts.list[check[0]].contacts[check[1]];
self.delContact(contact, function(e) {
if (e) {
self._SSIEndTrans();
if (typeof cb === 'function')
cb(e);
return;
}
self.addContact(contact, newGroup, function(e) {
self._SSIEndTrans();
cb(e);
}, true);
}, true);
};
OscarConnection.prototype.addGroup = function(group, cb) {
var self = this, check, record, err;
if (Object.keys(self.contacts.list).length < self._state.svcInfo[SNAC_SERVICES.SSI].maxGroups) {
record = (typeof group !== 'object' ? { name: '' } : group);
record.item = 0x00;
record.type = 0x01;
if (typeof group === 'string')
record.name = group;
check = self._SSIFindGroup(record.name);
if (check > -1)
err = 'That group already exists';
} else
err = 'Maximum number of groups reached';
if (err) {
err = new Error(err);
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
record.group = -1;
self._SSIStartTrans();
self._SSIModify(record, 0, function(e) {
if (e) {
self._SSIEndTrans();
if (typeof cb === 'function')
cb(e);
return;
}
record.contacts = {};
self.contacts.list[record.group] = record;
self._SSIModify(SSI_ROOT_GROUP, 2, function(e) {
self._SSIEndTrans();
if (!e)
self.contacts._usedIDs[record.group] = { 0: true };
if (typeof cb === 'function')
cb(e);
});
});
};
OscarConnection.prototype.delGroup = function(group, force, cb) {
var self = this, check, err, record;
cb = arguments[arguments.length-1];
if (typeof force !== 'boolean')
force = false;
record = (typeof group !== 'object' ? { name: '' } : group);
record.item = 0x00;
record.type = 0x01;
if (typeof group === 'string')
record.name = group;
check = self._SSIFindGroup(record.name);
if (check === -1)
err = 'That group doesn\'t exist';
else if (!force && Object.keys(self.contacts.list[check].contacts).length > 0)
err = 'That group isn\'t empty';
if (err) {
err = new Error(err);
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
record.group = check;
self._SSIStartTrans();
var fnContinue = function() {
self._SSIModify(record, 1, function(e) {
if (e) {
self._SSIEndTrans();
if (typeof cb === 'function')
cb(e);
return;
}
delete self.contacts.list[record.group];
self._SSIModify(SSI_ROOT_GROUP, 2, function(e) {
self._SSIEndTrans();
if (!e)
delete self.contacts._usedIDs[record.group];
if (typeof cb === 'function')
cb(e);
});
});
};
var children = Object.keys(self.contacts.list[record.group].contacts), chlen = children.length;
if (force && chlen > 0) {
var contacts = [];
for (var i=0; i<chlen; i++)
contacts.push(self.contacts.list[record.group].contacts[children[i]]);
self._SSIModify(contacts, 1, function(e) {
if (e) {
self._SSIEndTrans();
if (typeof cb === 'function')
cb(e);
return;
}
fnContinue();
});
} else
fnContinue();
};
OscarConnection.prototype.renameGroup = function(group, newName, cb) {
var self = this, check, err, record;
record = (typeof group !== 'object' ? { name: '' } : group);
record.item = 0x00;
record.type = 0x01;
if (typeof group === 'string')
record.name = group;
check = self._SSIFindGroup(record.name);
if (check === -1)
err = 'The source group doesn\'t exist';
else if (self._SSIFindGroup(newName) > -1)
err = 'The destination group already exists';
if (err) {
err = new Error(err);
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
record.group = check;
record.name = newName;
self._SSIStartTrans();
self._SSIModify(record, 2, function(e) {
self._SSIEndTrans();
if (!e)
self.contacts.list[record.group].name = newName;
if (typeof cb === 'function')
cb(e);
});
};
OscarConnection.prototype.getInfo = function(who, cb) {
// requests profile, away msg, capabilities
this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.LOCATION, 0x15, NO_FLAGS,
[0x00, 0x00, 0x00, 0x07, who.length]
.concat(str2bytes(who))
)
), cb);
};
OscarConnection.prototype.addDeny = function(who, cb) {
// TODO
};
OscarConnection.prototype.delDeny = function(who) {
// TODO
};
OscarConnection.prototype.addPermit = function(who, cb) {
// TODO
};
OscarConnection.prototype.delPermit = function(who) {
// TODO
};
OscarConnection.prototype.getIcon = function(who, metaData, cb) {
var icons, self = this,
fnGetBest = function(list) {
var idx;
for (var i=0,best=0; i<list.length; i++) {
if (list[i].type > best) {
best = list[i].type;
idx = i;
} else if (typeof idx === 'undefined')
idx = i;
}
process.nextTick(function(){ self._downloadIcons((typeof who === 'string' ? who : who.name), list[idx], cb); });
};
cb = arguments[arguments.length-1];
if (typeof metaData !== 'function')
icons = metaData;
if (typeof who === 'string') {
var where = self._SSIFindContact(who);
if (where[1] === -1 && !metaData) {
self.getInfo(who, function(e, info) {
if (e || !info.icons) {
if (typeof cb === 'function')
cb(e);
return;
}
fnGetBest(info.icons);
});
return;
} else if (where[1] > -1)
icons = self.contacts.list[where[0]].contacts[where[1]].icons;
} else if (typeof who === 'object')
icons = who.icons;
if (!icons) {
if (typeof cb === 'function')
cb(); // user has no icon set
} else
fnGetBest(icons);
};
OscarConnection.prototype.getOfflineMsgs = function() {
this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.ICBM, 0x10, NO_FLAGS
)
));
};
OscarConnection.prototype.joinChat = function(name, cb) {
var self = this;
if (!self._state.chatrooms[name]) {
var exchange = 4;
this._send(this._createFLAP(self._state.serviceMap[SNAC_SERVICES.CHAT_NAV], FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.CHAT_NAV, 0x08, NO_FLAGS,
[(exchange >> 8) & 0xFF, exchange & 0xFF, 0x06]
.concat(str2bytes('create')).concat([0xFF, 0xFF, 0x01, 0x00, 0x03])
.concat(this._createTLV(0xD3, name))
.concat(this._createTLV(0xD6, 'us-ascii'))
.concat(this._createTLV(0xD7, 'en'))
)
), function(err, roomInfo) {
// CHAT_NAV 0x09 activates this callback
if (err) {
cb(err);
return;
}
this._addService(SNAC_SERVICES.CHAT, roomInfo, function(err, conn) {
if (err) {
cb(err);
return;
}
cb();
});
});
} else
cb(new Error('You are already in that chat room'));
};
OscarConnection.prototype.inviteChat = function(name, msg, who) {
if (this._state.chatrooms[name]) {
if (typeof who === 'undefined') {
who = msg;
msg = 'Please join me in this chat';
}
return this._chatInvite(who, msg, this._state.chatrooms[name].roomInfo);
} else
return false;
};
OscarConnection.prototype.sendChatMsg = function(name, text) {
if (this._state.chatrooms[name]) {
var conn = this._state.chatrooms[name],
cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4));
if (text.length > conn.roomInfo.maxMsgLen)
return false;
this._send(conn, this._createFLAP(conn, FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.CHAT, 0x05, NO_FLAGS,
cookie.concat([0x00, 0x03]) // channel
.concat(this._createTLV(0x01)) // send to entire chat room
.concat(this._createTLV(0x06)) // send us our own message
.concat(this._createTLV(0x05,
this._createTLV(0x02, 'us-ascii')
.concat(this._createTLV(0x03, 'en'))
.concat(this._createTLV(0x01, str2bytes(text)))
))
)
));
return true;
} else
return false;
};
OscarConnection.prototype.leaveChat = function(name) {
if (this._state.chatrooms[name]) {
this._state.chatrooms[name].destroy();
delete this._state.chatrooms[name];
return true;
} else
return false;
};
OscarConnection.prototype.connect = function(cb) {
var self = this;
self._addConnection('login', null, self._options.connection.host, self._options.connection.port, function(e) {
if (self._state.connections.main) {
self._state.connections.main.authCookie = undefined;
self._state.connections.main.rateLimitGroups = undefined;
}
if (e) {
if (typeof cb === 'function')
cb(e);
return;
}
self._addService(SNAC_SERVICES.BART, function(e) {
if (e) {
if (typeof cb === 'function')
cb(e);
return;
}
self._addService(SNAC_SERVICES.CHAT_NAV, cb);
});
});
};
OscarConnection.prototype.end = function() {
var ids = Object.keys(this._state.connections);
for (var i=0,len=ids.length; i<len; ++i)
this._state.connections[ids[i]].end();
this._resetState();
};
// Connection handlers -------------------------------------------------------------------------------
function connect_handler(oscar) {
var self = oscar, conn = this;
conn.availServices = {};
conn.isConnected = true;
clearTimeout(conn.tmrConn);
conn.restartKeepAlive();
if (conn.isTransferring) {
if (conn.id === 'login') {
self._state.connections.main = self._state.connections.login;
delete self._state.connections.login;
conn.id = 'main';
}
conn.serverType = 'BOS';
conn.isTransferring = false;
}
debug('(' + conn.remoteAddress + ') Connected to ' + conn.serverType + ' server');
conn.write('');
}
function data_handler(oscar, data, cb) {
var self = oscar, conn = this;
//debug('(' + conn.remoteAddress + ') RECEIVED: \n' + util.inspect(data)) + '\n';
if (conn.curData)
conn.curData = bufferAppend(conn.curData, data);
else
conn.curData = data;
data = conn.curData;
if (data[0] === 0x2A) {
switch (data[1]) {
case FLAP_CHANNELS.CONN_NEW: // new connection negotiation
debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: New connection negotiation');
if (conn.serverType === 'login') {
self._send(conn, self._createFLAP(conn, FLAP_CHANNELS.CONN_NEW, [0x00, 0x00, 0x00, 0x01])); // send FLAP protocol version
self._login(undefined, conn, cb);
} else
self._login(undefined, conn, cb, 2);
conn.curData = undefined;
break;
case FLAP_CHANNELS.SNAC: // SNAC response
var payloadLen = (data[4] << 8) + data[5];
if (6+payloadLen > data.length)
return;
else
conn.curData = undefined;
//debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: SNAC response');
self._parseSNAC(conn, data.slice(6, 6+payloadLen), cb);
if (data.length > 6 + payloadLen) {
// extra bytes -- start of another FLAP message?
var extra = new Buffer(data.length - (6 + payloadLen));
data.copy(extra, 0, 6 + payloadLen);
process.nextTick(function() {
conn.emit('data', extra);
});
}
break;
case FLAP_CHANNELS.ERROR: // FLAP-level error
debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: FLAP error');
conn.curData = undefined;
break;
case FLAP_CHANNELS.CONN_CLOSE: // close connection negotiation
debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: Close connection negotiation');
if (conn.serverType === 'BOS') {
var tlvs = extractTLVs(data, 6);
if (tlvs[TLV_TYPES.ERROR]) {
var code = (tlvs[TLV_TYPES.ERROR][0] << 8) + tlvs[TLV_TYPES.ERROR][1];
var err = new Error(AUTH_ERRORS_TEXT[code]);
err.code = code;
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
} else if (tlvs[TLV_TYPES.BOS_SERVER])
self._login(undefined, conn, cb, 1, tlvs[TLV_TYPES.BOS_SERVER].toString(), tlvs[TLV_TYPES.AUTH_COOKIE]);
}
conn.curData = undefined;
break;
case FLAP_CHANNELS.KEEPALIVE: // keep alive
debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: Keep-alive');
conn.curData = undefined;
break;
default:
debug('(' + conn.remoteAddress + ') RECEIVED FLAP type: UNKNOWN (0x' + data[1].toString(16) + ')');
conn.curData = undefined;
}
} else
debug('(' + conn.remoteAddress + ') RECEIVED Non-FLAP message');
}
function end_handler(oscar) {
var self = oscar, conn = this;
conn.isConnected = false;
if (!conn.isTransferring) {
if (conn === self._state.connections.main) {
self._resetState();
self.emit('end');
}
debug('(' + conn.remoteAddress + ') [' + getConnSvcNames(conn) + '] FIN packet received. Disconnecting...');
}
}
function error_handler(oscar, err, cb) {
var self = oscar, conn = this;
clearTimeout(conn.tmrConn);
if (!conn.isConnected)
cb(new Error('(' + conn.remoteAddress + ') Unable to connect to ' + conn.serverType + ' server.\n ' + err.stack));
if (conn.readyState === 'closed')
conn.isConnected = false;
debug('(' + conn.remoteAddress + ') ' + err);
self.emit('error', err);
}
function close_handler(oscar, had_error) {
var self = oscar, conn = this;
conn.isConnected = false;
if (!conn.isTransferring || had_error) {
if (conn === self._state.connections.main) {
self._resetState();
self.emit('close', had_error);
}
debug('(' + conn.remoteAddress + ') [' + getConnSvcNames(conn) + '] Connection forcefully closed.');
}
}
// Private methods -----------------------------------------------------------------------------------
OscarConnection.prototype._resetState = function() {
this._state = {
connections: {},
serviceMap: {},
reqID: 0, // 32-bit number that identifies a single SNAC request
status: this._options.other.initialStatus,
flags: this._options.other.initialFlags,
requests: {},
isAOL: (this._options.connection.host.substr(this._options.connection.host.length-7).toUpperCase() === 'AOL.COM'),
rateLimitGroups: {},
rateLimits: {},
svcInfo: {},
svcPaused: {},
SSI: {},
iconQueue: {},
rndvCookies: { out: {}, in: {} },
chatrooms: {},
p2p: {}
};
};
OscarConnection.prototype._addConnection = function(id, services, host, port, cb) {
var self = this;
self._state.connections[id] = new net.Socket();
self._state.connections[id].id = id;
self._state.connections[id].neededServices = services;
self._state.connections[id].serverType = (id === 'login' ? 'login' : 'BOS');
self._state.connections[id].isTransferring = false;
self._state.connections[id].isReady = false;
self._state.connections[id].isConnected = false;
self._state.connections[id].seqNum = 0; // 0x0000 to 0x7FFF, wraps to 0x0000 past 0x7FFF
self._state.connections[id].fnTmrConn = function(cbConn) {
cbConn(new Error('Connection timed out while connecting to ' + self._state.connections[id].serverType + ' server'));
self._state.connections[id].destroy();
};
self._state.connections[id].tmrConn = setTimeout(self._state.connections[id].fnTmrConn, self._options.connection.connTimeout, cb);
self._state.connections[id].setTimeout(0);
self._state.connections[id].setKeepAlive(true);
self._state.connections[id].restartKeepAlive = function() {
var conn = this;
if (conn.keepAliveTimer)
clearTimeout(conn.keepAliveTimer);
conn.keepAliveTimer = setInterval(function() { self._sendKeepAlive(conn); }, KEEPALIVE_INTERVAL);
};
self._state.connections[id].on('connect', function() { connect_handler.call(this, self); });
self._state.connections[id].on('data', function(data) { data_handler.call(this, self, data, cb); });
self._state.connections[id].on('end', function() { end_handler.call(this, self); });
self._state.connections[id].on('error', function(err) { error_handler.call(this, self, err, cb); });
self._state.connections[id].on('close', function(had_error) { close_handler.call(this, self, had_error); });
self._state.connections[id].connect(port, host);
}
OscarConnection.prototype._addService = function(svc, roomInfo, cb) {
var self = this;
if (typeof cb === 'undefined') {
cb = roomInfo;
roomInfo = undefined;
}
if (svc === SNAC_SERVICES.CHAT || !self._state.serviceMap[svc]) {
var content = [svc >> 8 & 0xFF, svc & 0xFF];
if (roomInfo) {
content = content.concat(self._createTLV(0x01,
[(roomInfo.exchange >> 8) & 0xFF, roomInfo.exchange & 0xFF,
roomInfo.cookie.length].concat(roomInfo.cookie).concat([0x00, 0x00])
));
}
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.GENERIC, 0x04, NO_FLAGS,
content
)
), function(e, address, cookie) {
if (e) {
if (typeof cb === 'function')
cb(e);
return;
}
var services = {}, server, port, id = Date.now();
if (address.indexOf(':') > -1) {
server = address.substring(0, address.indexOf(':'));
port = parseInt(address.substr(address.indexOf(':')+1));
} else {
server = address;
port = self._state.connections.main.remotePort;
}
services[svc] = true;
self._addConnection(id, services, server, port, function(e) {
self._state.connections[id].authCookie = undefined;
self._state.connections[id].rateLimitGroups = undefined;
if (!e && roomInfo) {
self._state.chatrooms[roomInfo.name] = self._state.connections[id];
self._state.chatrooms[roomInfo.name].roomInfo = roomInfo;
}
if (typeof cb === 'function')
process.nextTick(function(){ cb(e, self._state.connections[id]); });
});
self._state.connections[id].authCookie = cookie;
});
} else if (typeof cb === 'function')
process.nextTick(function(){ cb(); });
};
// action === 0 (add), 1 (delete), 2 (modify)
OscarConnection.prototype._SSIModify = function(items, action, cb) {
// TODO: check max (and min) name length, etc before hitting the server
cb = arguments[arguments.length-1];
var err = '', exists, bytes = [], self = this;
items = (Array.isArray(items) ? items : [items]);
for (var i=0,ilen=items.length; i<ilen; i++) {
bytes.push(items[i].name.length >> 8 & 0xFF);
bytes.push(items[i].name.length & 0xFF);
for (var j=0; j<items[i].name.length; j++)
bytes.push(items[i].name.charCodeAt(j) & 0xFF);
if (action === 0) {
if (items[i].type === 0x01) {
for (var j=0; j<=0x7FFF; j++) {
if (typeof self.contacts._usedIDs[j] === 'undefined') {
items[i].group = j;
break;
}
}
} else if (items[i].group === 0) {
var groups = Object.keys(self.contacts._usedIDs), groupsLen = groups.length;
for (var j=0,found; j<=0x7FFF; j++) {
if (typeof self.contacts._usedIDs[j] === 'undefined') {
found = false;
for (var k=0; k<groupsLen; k++) {
if (typeof self.contacts._usedIDs[groups[k]][j] !== 'undefined')
found = true;
}
if (!found) {
items[i].item = j;
break;
}
}
}
} else {
for (var j=0; j<=0x7FFF; j++) {
if (typeof self.contacts._usedIDs[items[i].group][j] === 'undefined') {
items[i].item = j;
break;
}
}
}
}
bytes.push(items[i].group >> 8 & 0xFF);
bytes.push(items[i].group & 0xFF);
bytes.push(items[i].item >> 8 & 0xFF);
bytes.push(items[i].item & 0xFF);
bytes.push(items[i].type >> 8 & 0xFF);
bytes.push(items[i].type & 0xFF);
var itemBytes = [];
if (action !== 1) {
if (items[i].type === 0x00) {
if (items[i].localInfo) {
if (items[i].localInfo.alias)
itemBytes = itemBytes.concat(self._createTLV(0x0131, str2bytes(items[i].localInfo.alias)));
if (items[i].localInfo.emailAddress)
itemBytes = itemBytes.concat(self._createTLV(0x0137, str2bytes(items[i].localInfo.emailAddress)));
if (items[i].localInfo.homePhoneNum)
itemBytes = itemBytes.concat(self._createTLV(0x0138, str2bytes(items[i].localInfo.homePhoneNum)));
if (items[i].localInfo.cellPhoneNum)
itemBytes = itemBytes.concat(self._createTLV(0x0139, str2bytes(items[i].localInfo.cellPhoneNum)));
if (items[i].localInfo.smsPhoneNum)
itemBytes = itemBytes.concat(self._createTLV(0x013A, str2bytes(items[i].localInfo.smsPhoneNum)));
if (items[i].localInfo.workPhoneNum)
itemBytes = itemBytes.concat(self._createTLV(0x0158, str2bytes(items[i].localInfo.workPhoneNum)));
if (items[i].localInfo.otherPhoneNum)
itemBytes = itemBytes.concat(self._createTLV(0x0159, str2bytes(items[i].localInfo.otherPhoneNum)));
if (items[i].localInfo.notes)
itemBytes = itemBytes.concat(self._createTLV(0x013C, str2bytes(items[i].localInfo.notes)));
}
// TODO: support adding/modifying the alerts for this contact and check maxWatchers first
} else if (items[i].type === 0x01) {
if (action === 2) {
var ids = [],
children = (Object.keys(items[i].group > 0 ?
this.contacts.list[items[i].group].contacts : this.contacts._usedIDs),len2 = children.length);
for (var j=0; j<len2; j++) {
if (items[i].group === 0 && children[j] === 0)
continue;
ids.push(children[j] >> 8 & 0xFF);
ids.push(children[j] & 0xFF);
}
if (ids.length > 0)
itemBytes = itemBytes.concat(self._createTLV(0x00C8, ids));
}
} else if (items[i].tlvs) {
for (var j=0,types=Object.keys(items[i].tlvs),len2=types.length; j<len2; j++)
itemBytes = itemBytes.concat(self._createTLV(parseInt(types[j]), items[i].tlvs[types[j]]));
}
}
bytes.push(itemBytes.length >> 8 & 0xFF);
bytes.push(itemBytes.length & 0xFF);
if (itemBytes.length > 0)
bytes = bytes.concat(itemBytes);
}
var subtype = 0x08;
if (action === 1)
subtype = 0x0A;
else if (action === 2)
subtype = 0x09;
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.SSI, subtype, NO_FLAGS,
bytes
)
), function(e) {
if (typeof cb === 'function')
cb(e);
});
};
OscarConnection.prototype._SSIStartTrans = function() {
var self = this;
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.SSI, 0x11, NO_FLAGS
)
));
};
OscarConnection.prototype._SSIEndTrans = function() {
var self = this;
self._send(self._createFLAP(self._state.connections.main, FLAP_CHANNELS.SNAC,
self._createSNAC(SNAC_SERVICES.SSI, 0x12, NO_FLAGS
)
));
};
OscarConnection.prototype._SSIFindGroup = function(group) {
var retVal = -1;
if (typeof group === 'undefined' || group === null)
retVal = -1;
else if (typeof group === 'number') {
if (typeof this.contacts.list[group] === 'undefined')
retVal = -1;
} else {
group = (''+group).toUpperCase();
for (var i=0,groups=Object.keys(this.contacts.list),len=groups.length; i<len; i++) {
if (this.contacts.list[groups[i]].name.toUpperCase() === group) {
retVal = groups[i];
break;
}
}
}
return retVal;
};
OscarConnection.prototype._SSIFindContact = function(group, contact) {
var retVal = [-1, -1];
if (arguments.length === 1) {
contact = arguments[0];
group = undefined;
}
group = this._SSIFindGroup(group);
if (group > -1)
retVal[0] = group;
if (typeof contact === 'number') {
if (typeof this.contacts.list[group].contacts[contact] !== 'undefined')
retVal[1] = contact;
} else {
contact = (''+contact).toUpperCase().replace(/ /g, '');
for (var i=0,groups=Object.keys(this.contacts.list),glen=groups.length; i<glen; i++) {
for (var j=0,contacts=Object.keys(this.contacts.list[groups[i]].contacts),len=contacts.length; j<len; j++) {
if (this.contacts.list[groups[i]].contacts[contacts[j]].name.toUpperCase().replace(/ /g, '') === contact) {
retVal[0] = groups[i];
retVal[1] = contacts[j];
break;
}
}
}
}
return retVal;
};
OscarConnection.prototype._send = function(conn, payload, cb) {
var self = this, isSNAC, svc;
if (!(conn instanceof net.Stream)) {
cb = payload;
payload = conn;
}
isSNAC = (payload[1] === FLAP_CHANNELS.SNAC);
svc = (isSNAC ? (payload[6] << 8) + payload[7] : undefined);
if (!(conn instanceof net.Stream) && svc !== 'undefined')
conn = self._state.serviceMap[svc];
if (isSNAC) {
if (Object.keys(conn.availServices).length > 0 && typeof conn.availServices[svc] === 'undefined') {
var err = new Error('No available server supports the requested SNAC: 0x' + svc.toString(16));
if (typeof cb === 'function')
cb(err);
else
throw err;
return;
}
if (typeof cb === 'function') { // some requests don't expect a response
var reqID = (payload[12] << 24) + (payload[13] << 16) + (payload[14] << 8) + payload[15]; // SNAC request ID
this._state.requests[reqID] = cb;
}
}
payload = new Buffer(payload);
debug('(' + conn.remoteAddress + ') SENDING: \n' + util.inspect(payload) + '\n');
conn.write(payload);
conn.restartKeepAlive();
};
OscarConnection.prototype._dispatch = function(reqID) {
var cb = this._state.requests[reqID];
if (typeof cb === 'function') {
this._state.requests[reqID] = undefined;
cb.apply(this, Array.prototype.slice.call(arguments).slice(1));
}
};
OscarConnection.prototype._incomingFile = function(data) {
var fileInfo = {}, i = 0, len;
fileInfo.subtype = (data[i++] << 8) + data[i++]; // 0x0001 === 'one file', 0x0002 === 'more than one file'
fileInfo.numFiles = (data[i++] << 8) + data[i++];
fileInfo.totalSize = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++];
fileInfo.filename = data.toString(i, i+(data.length-1)); // string is null-terminated
return fileInfo;
};
OscarConnection.prototype._incomingIcon = function(data) {
var i = 0, hash = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++],
len = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++],
datetime = (data[i++] << 24) + (data[i++] << 16) + (data[i++] << 8) + data[i++];
return { hash: hash, datetime: datetime, data: data.slice(i, i+len) };
};
OscarConnection.prototype._incomingChat = function(data) {
var roomInfo = {}, i = 0, len;
roomInfo.exchange = (data[i++] << 8) + data[i++];
len = data[i++];
roomInfo.name = data.toString('utf8', i, i+len);
i += len;
roomInfo.instance = (data[i++] << 8) + data[i++];
return roomInfo;
};
OscarConnection.prototype._incomingList = function(data) {
var list = {};
for (var i=0,len=data.length,group,namelen,numContacts; i<len;) {
namelen = (data[i++] << 8) + data[i++];
group = data.toString('utf8', i, i+namelen);
list[group] = [];
i += namelen;
numContacts = (data[i++] << 8) + data[i++];
for (var j=0; j<numContacts; j++) {
namelen = (data[i++] << 8) + data[i++];
list[group].push(data.toString('utf8', i, i+namelen));
i += namelen;
}
}
return list;
};
OscarConnection.prototype._calcIconSum = function(icon) {
var sum = 0; // 16-bit
if (Buffer.isBuffer(icon) || Array.isArray(icon)) {
var iconLen = icon.length, i;
for (i=0; i+1<iconLen; i+=2)
sum += (icon[i+1] << 8) + icon[i];
if (i < iconLen)
sum += icon[i];
sum = ((sum & 0xFFFF0000) >> 16) + (sum & 0x0000FFFF);
}
return sum;
};
OscarConnection.prototype._sendIcon = function(who) {
if (Buffer.isBuffer(this.icon.data) || Array.isArray(this.icon.data)) {
if (this.icon.data.length <= MAX_ICON_LEN) {
var content = [], cookie;
who = str2bytes(who);
cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4));
content = content.concat(cookie).concat[0x00, 0x02, who.length].concat(who)
.concat(this._createTLV(0x05, [0x00, 0x00].concat(cookie).concat(CAPABILITIES.BUDDY_ICON)));
content = content.concat(this._createTLV(0x0A, [0x00, 0x01]));
content = content.concat(this._createTLV(0x0F));
content = content.concat(this._createTLV(0x2711, [0x00].concat(splitNum(this._calcIconSum(this.icon.data), 2))
.concat(splitNum(this.icon.data.length, 4))
.concat(splitNum(this.icon.datetime, 4))
));
for (var i=0,len=this.icon.data.length; i<len; i++)
content.push(this.icon.data[i]);
content = content.concat(str2bytes('AVT1picture.id'));
this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS,
content
)
));
} else
debug('Uh oh, my icon exceeds the maximum icon size of ' + MAX_ICON_LEN + ' bytes. Cannot send it to: ' + who);
}
};
OscarConnection.prototype.sendFile = function(who, file, ip, port, cb) {
};
OscarConnection.prototype._cancelRendezvous = function(inout, cookie, cb) {
cookie = ''+cookie;
var info = this._state.rndvCookies[inout][cookie];
if (typeof info !== 'undefined') {
var content, type;
cookie = info.cookie;
if (info.type === 'chat')
type = CAPABILITIES.CHAT;
else if (info.type === 'file')
type = CAPABILITIES.SEND_FILE;
if (inout === 'out') {
content = cookie.concat[0x00, 0x02, who.length].concat(who).concat(this._createTLV(0x03))
.concat(this._createTLV(0x05, [0x00, 0x01].concat(cookie).concat(type).concat(this._createTLV(0x0B))));
this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS,
content
)
), cb);
} else if (inout === 'in') {
if (info.type === 'file') {
}
}
delete this._state.rndvCookies[inout][cookie];
}
};
OscarConnection.prototype._chatInvite = function(who, msg, roomInfo) {
var content, cookie;
who = str2bytes(who);
msg = str2bytes(msg);
name = str2bytes(roomInfo.name);
exchange = splitNum(roomInfo.exchange, 2);
instance = splitNum(roomInfo.instance, 2);
cookie = splitNum(Date.now(), 4).concat(splitNum(Date.now()+1, 4));
this._state.rndvCookies.out[cookie.join('')] = {
cookie: cookie,
type: 'chat',
user: who,
name: name,
exchange: exchange,
instance: instance
};
content = cookie.concat([0x00, 0x02, who.length]).concat(who)
.concat(this._createTLV(0x05, [0x00, 0x00].concat(cookie).concat(CAPABILITIES.CHAT)
.concat(this._createTLV(0x0A, [0x00, 0x01])).concat(this._createTLV(0x0F))
.concat(this._createTLV(0x0C, msg))
.concat(this._createTLV(0x2711, exchange.concat(name).concat(instance)))));
this._send(this._createFLAP(this._state.connections.main, FLAP_CHANNELS.SNAC,
this._createSNAC(SNAC_SERVICES.ICBM, 0x06, NO_FLAGS,
content
)
));
return cookie.join('');
};
OscarConnection.prototype._parseSNAC = function(conn, snac, cb) {
//debug('(' + conn.remoteAddress + ') SNAC response follows:\n' + util.inspect(snac));
var serviceID, subtypeID, flags, reqID, tlvs, idx, isServerOrig, moreFollows,
debugtext, self = this;
serviceID = (snac[0] << 8) + snac[1];
subtypeID = (snac[2] << 8) + snac[3];
flags = (snac[4] << 8) + snac[5];
reqID = (snac[6] << 24) + (snac[7] << 16) + (snac[8] << 8) + snac[9];
isServerOrig = (flags < 0); // MSB === 1 indicates the SNAC is not the result of a client request,
// (i.e. the server is asking/telling us something)
moreFollows = (flags & 0x1); // At least one more packet of the same SNAC service and subtype (?) will come after this packet
idx = 10;
if (flags & 0x8000) {
// apparently this means AOL has decided to prepend service version information for the fun of it,
// so skip it to get to the real data
idx += 2+((snac[idx] << 8) + snac[idx+1]);
}
debugtext = '(' + conn.remoteAddress + ') RECEIVED SNAC: ';
switch (serviceID) {
case SNAC_SERVICES.AUTH:
debugtext += 'AUTH > ';
switch (subtypeID) {
case 0x03:
debugtext += 'MD5 login reply';
tlvs = extractTLVs(snac);
if (tlvs[TLV_TYPES.ERROR]) {
var err = new Error(AUTH_ERRORS_TEXT[(tlvs[TLV_TYPES.ERROR][0] << 8) + tlvs[TLV_TYPES.ERROR][1]]);
debugtext += ' (error: ' + err + ')';
if (typeof cb === 'function')
cb(err);
else
throw err;
} else {
self._dispatch(reqID, undefined, tlvs[TLV_TYPES.BOS_SERVER].toString(), tlvs[TLV_TYPES.AUTH_COOKIE].toArray());
debugtext += ' (no error)';
}
break;
case 0x07: // md5 salt
debugtext += 'MD5 key/salt';
var saltLen, salt;
saltLen = (snac[idx++] << 8) + snac[idx++];
salt = snac.toString('utf8', idx, idx+saltLen);
self._dispatch(reqID, undefined, salt);
break;
default:
debugtext += 'Unknown (0x' + subtypeID.toString(16) + ')';
}
break;
case SNAC_SERVICES.GENERIC:
debugtext += 'GENERIC > ';
switch (subtypeID) {
case 0x01: // error
debugtext += 'Error';
var code = (snac[idx++] << 8) + snac[idx++], msg = GLOBAL_ERRORS_TEXT[code] || 'Unknown error code received: ' + code,
err = new Error(msg);
err.code = code;
debugtext += ': ' + msg;
self._dispatch(reqID, err);
break;
case 0x03:
debugtext += 'Supported services: ';
var debugAvail = [], services = flip(SNAC_SERVICES);
for (var len=snac.length,svc; idx<len;) {
svc = (snac[idx++] << 8) + snac[idx++];
conn.availServices[svc] = true;
if (typeof services[svc] !== 'undefined')
debugAvail.push(services[svc]);
}
debugtext += debugAvail.join(', ');
self._login(undefined, conn, cb, 3);
break;
case 0x05: // redirect info for requested service
debugtext += 'Service request info';
var services = flip(SNAC_SERVICES), useSSL;
tlvs = extractTLVs(snac, idx);
useSSL = (tlvs[0x8E] && tlvs[0x8E][0] === 0x01);
if (typeof services[(tlvs[0x0D][0] << 8) + tlvs[0x0D][1]] !== 'undefined')
debugtext += ' (' + services[(tlvs[0x0D][0] << 8) + tlvs[0x0D][1]] + ')';
else
debugtext += ' (Unknown: ' + ((tlvs[0x0D][0] << 8) + tlvs[0x0D][1]).toString(16) + ')';
debugtext += '. Host: ' + tlvs[0x05].toString();
self._dispatch(reqID, undefined, tlvs[0x05].toString(), tlvs[0x06].toArray());
break;
case 0x07:
debugtext += 'Rate limit classes and groups';
var numGroups = 0, classId;
if (typeof snac[idx] !== 'undefined') {
numGroups = (snac[idx++] << 8) + snac[idx++];
for (var i=0, group; i<numGroups; i++) {
classId = (snac[idx++] << 8) + snac[idx++];
group = {
windowSize: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++],
levels: {
clear: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++],
alert: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++],
limit: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++],
disconnect: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++],
current: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++],
max: (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++],
},
delta: 0,
droppingSNACs: false
};
if (conn.availServices[serviceID] >= 3) {
group.delta = (snac[idx++] << 24) + (snac[idx++] << 16) + (snac[idx++] << 8) + snac[idx++];
group.droppingSNACs = (snac[idx++] === 0x01 ? true : false);
}
if (!self._state.rateLimitGroups[classId])
self._state.rateLimitGroups[classId] = group;
if (!conn.rateLimitGroups)
conn.rateLimitGroups = [];
if (conn.rateLimitGroups.indexOf(classId) === -1)
conn.rateL