Last active
January 1, 2016 15:39
-
-
Save calzoneman/8165294 to your computer and use it in GitHub Desktop.
Personal IRC bridge for berrytube
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* NOTE: BRIDGE DEVELOPMENT HAS MOVED TO https://github.com/berrytube/bt-irc-bridge. | |
* THIS GIST IS NO LONGER BEING ACTIVELY MAINTAINED | |
*/ | |
/** | |
* ### cyzon's berrytube/irc bridge ### | |
* | |
* Installation | |
* - Requires nodejs (http://nodejs.org). Available for Windows/OSX/Linux and source code. | |
* - Requires socket.io (run `npm install socket.io-client` in the same directory as server.js) | |
* - At some point I may try to put together a portable windows version | |
* | |
* Running | |
* - Start as `node server.js [<port>]`. <port> is optional and defaults to 6667. On Windows you may need to unblock it from the firewall. | |
* - Connect your favorite irc client to localhost with the port chosen above | |
* | |
* Controls | |
* - Choose a guest name with `/nick whatever` | |
* - Login as a regular user with `/msg control login <user> <pass>` | |
* - Change configuration options with `/msg control set <key> <value>` | |
* - `/msg control set` lists config keys | |
* - `/msg control set <key>` shows the current value for <key> | |
* - View the current poll with `/msg control poll` | |
* - View the previous poll with `/msg control poll last` | |
* - Vote in a poll with `/msg control poll vote <option #>` | |
* - Reconnect to berrytube with `/msg control reconnect` | |
* | |
* Config | |
* - yay_color: If enabled, use mIRC colors to turn <span class="flutter"> to be pink | |
* - rcv_color: If enabled, use mIRC colors to make rcv messages red | |
* - rcv_bold: If enabled, set the bold attribute for rcv messages | |
* - drink_bold: If enabled, set the bold attribute for drink calls (makes them more visible) | |
* - request_color: If enabled, use mIRC colors to make requests blue | |
* - poll_bold: If enabled, poll notification messages are bolded | |
* - show_bold: If enabled, set the bold attribute for modmin bold messages | |
* - show_underline: If enabled, set the underline attribute for italicized messages (_message_) | |
* - strip_html: If enabled, strip HTML tags from messages | |
* - echo: If enabled, echo back messages sent to berrytube. | |
* By default, when you send a message from IRC, it is displayed | |
* plain (unfiltered) in your IRC client. I cannot fix this | |
* with anything I do serverside, so by default messages | |
* from berrytube from your nickname are ignored | |
* because showing them would duplicate messages. | |
* You can override this by setting echo to true | |
* - hide_spoilers: If enabled, replace spoiler messages with [SPOILER]. Enabled by default. | |
* | |
* Disclaimer | |
* - I take no responsibility for anything bad that happens if you use this. Use at your own risk. | |
* | |
* Enjoy! Feel free to report bugs/suggestions | |
* -cyzon | |
*/ | |
/** | |
* CHANGELOG | |
* | |
* 2014-01-16 (v1.0.7) | |
* - Fix Pidgin | |
* | |
* 2014-01-08 (v1.0.6) | |
* - Fix not responding to NAMES | |
* | |
* 2014-01-07 (v1.0.5) | |
* - Fix requests showing up as CTCP errors | |
* - Make logged in users show as +v | |
* - Add /msg control reconnect to force a reconnection to berrytube | |
* | |
* 2014-01-01 (v1.0.3, v1.0.4) | |
* - Fix requests so they show up as * user requests foo instead of <user> foo | |
* - Add support for viewing and voting in polls | |
* | |
* 2013-12-29 (v1.0.2) | |
* - Minor fix for NICK command | |
* | |
* 2013-12-29 (v1.0.1) | |
* - Added hide_spoilers option | |
* - Corrected IRC PRIVMSG syntax | |
* - Send PREFIX message (fixes hop status on some clients) | |
*/ | |
var net = require('net'); | |
var fs = require('fs'); | |
// Config | |
var VERSION = '1.0.7'; | |
var HOSTNAME = 'localhost'; | |
var CONFIG = { | |
yay_color: true, | |
rcv_color: true, | |
rcv_bold: true, | |
drink_bold: true, | |
request_color: true, | |
poll_bold: true, | |
show_bold: true, | |
show_underline: true, | |
strip_html: true, | |
echo: false, | |
hide_spoilers: true | |
}; | |
function saveConfig() { | |
fs.writeFileSync('options.json', JSON.stringify(CONFIG, null, 4)); | |
} | |
function loadConfig() { | |
try { | |
var json = JSON.parse(fs.readFileSync('options.json')+''); | |
for (var key in json) { | |
if (key in CONFIG) { | |
CONFIG[key] = json[key]; | |
} | |
} | |
} catch (e) { | |
} | |
} | |
function IRCServer(port) { | |
var self = this; | |
this.clients = []; | |
this.irc = net.createServer(function (c) { | |
self.newClient(c); | |
}); | |
this.irc.listen(port, function () { | |
console.log('Listening on port', port); | |
}); | |
} | |
IRCServer.prototype.newClient = function (socket) { | |
var self = this; | |
console.log('Accepted connection from ' + socket.remoteAddress); | |
var c = new Client(socket); | |
socket.on('end', function () { | |
console.log(c.ip, 'disconnected'); | |
}); | |
}; | |
function Client(socket) { | |
var self = this; | |
this.ip = socket.remoteAddress; | |
this.name = 'anonymous'; | |
this.loggedIn = false; | |
this.inChannel = false; | |
this.poll = null; | |
this.lastPoll = null; | |
this.socket = socket; | |
this.buffer = ''; | |
socket.on('data', function (data) { | |
self.buffer += data; | |
if (self.buffer.indexOf('\r\n') !== -1) { | |
self.handleBuffer(); | |
} | |
}); | |
var _socketwrite = socket.write; | |
socket.write = function (what) { | |
console.log(self.ip + ' <-- ', what.replace(/[\r\n]/g, '')); | |
try { | |
_socketwrite.call(socket, what); | |
} catch (e) { | |
console.log(e); | |
} | |
}; | |
// BerryTube data | |
this.btnicks = []; | |
this.initBerrytube(); | |
socket.on('end', function () { | |
self.bt.disconnect(true); | |
}); | |
} | |
Client.prototype.initBerrytube = function () { | |
var self = this; | |
this.bt = require('socket.io-client').connect('96.127.152.99:8344', { | |
'force new connection': true | |
}); | |
this.bt.on('newChatList', function (data) { | |
self.btnicks = data; | |
self.btnicks.forEach(function (u) { | |
u.ircnick = u.nick + '!' + u.nick + '@berrytube.tv'; | |
}); | |
self.btnicks.sort(function (a, b) { | |
var x = a.nick.toLowerCase(); | |
var y = b.nick.toLowerCase(); | |
var z = a.type - b.type; | |
if (z !== 0) { | |
return -z; | |
} | |
return x > y ? 1 : -1; | |
}); | |
self.handleNAMES(null, ['#berrytube']); | |
}); | |
this.bt.on('userJoin', function (data) { | |
data.ircnick = data.nick + '!' + data.nick + '@berrytube.tv'; | |
self.btnicks.push(data); | |
self.btnicks.sort(function (a, b) { | |
var x = a.nick.toLowerCase(); | |
var y = b.nick.toLowerCase(); | |
var z = a.type - b.type; | |
if (z !== 0) { | |
return -z; | |
} | |
return x > y ? 1 : -1; | |
}); | |
if (data.nick === self.name) { | |
return; | |
} | |
self.socket.write(':' + data.ircnick + ' JOIN #berrytube\r\n'); | |
switch (data.type) { | |
case 2: | |
self.socket.write(':' + HOSTNAME + ' MODE #berrytube +o ' + data.nick + '\r\n'); | |
break; | |
case 1: | |
self.socket.write(':' + HOSTNAME + ' MODE #berrytube +h ' + data.nick + '\r\n'); | |
break; | |
case 0: | |
self.socket.write(':' + HOSTNAME + ' MODE #berrytube +v ' + data.nick + '\r\n'); | |
break; | |
default: | |
break; | |
} | |
}); | |
this.bt.on('userPart', function (data) { | |
data.ircnick = data.nick + '!' + data.nick + '@berrytube.tv'; | |
var found = false; | |
for (var i = 0; i < self.btnicks.length; i++) { | |
if (self.btnicks[i].nick === data.nick) { | |
found = i; | |
break; | |
} | |
} | |
if (found !== false) { | |
self.btnicks.splice(found, 1); | |
} | |
self.socket.write(':' + data.ircnick + ' PART #berrytube\r\n'); | |
}); | |
this.bt.on('setNick', function (nick) { | |
self.socket.write(':' + self.name + ' NICK ' + nick + '\r\n'); | |
self.name = nick; | |
self.loggedIn = true; | |
}); | |
this.bt.on('chatMsg', function (data) { | |
if (data.nick !== self.name) { | |
self.handleBTMessage(data); | |
} | |
}); | |
this.bt.on('forceVideoChange', function (data) { | |
var title = decodeURIComponent(data.video.videotitle); | |
self.rpl('332 {nick} #berrytube :Now Playing: ' + title); | |
}); | |
this.bt.on('createPlayer', function (data) { | |
var title = decodeURIComponent(data.video.videotitle); | |
self.rpl('332 {nick} #berrytube :Now Playing: ' + title); | |
}); | |
this.bt.on('kicked', function (reason) { | |
self.socket.write(':' + HOSTNAME + ' PRIVMSG ' + self.name + ' :Kicked: ' + reason + '\r\n'); | |
}); | |
this.bt.on('newPoll', function (data) { | |
self.handleBTPoll(data); | |
}); | |
this.bt.on('updatePoll', function (data) { | |
self.handleBTPollUpdate(data); | |
}); | |
this.bt.on('clearPoll', function (data) { | |
self.handleBTPollUpdate(data); | |
self.lastPoll = self.poll; | |
self.poll = null; | |
}); | |
this.bt.emit('myPlaylistIsInited'); | |
}; | |
Client.prototype.rpl = function (msg) { | |
msg = msg.replace(/\{nick\}/g, this.name); | |
msg = ':' + HOSTNAME + ' ' + msg + '\r\n'; | |
try { | |
this.socket.write(msg); | |
} catch (e) { | |
console.log(e); | |
} | |
}; | |
Client.prototype.handleBuffer = function () { | |
var msgs = this.buffer.split('\r\n'); | |
this.buffer = msgs[msgs.length - 1]; | |
msgs.length -= 1; | |
for (var i = 0; i < msgs.length; i++) { | |
console.log(this.ip + ' --> ',msgs[i]); | |
var cmd = '', prefix = null, args = msgs[i].split(' '); | |
if (msgs[i].indexOf(':') === 0) { | |
prefix = args[0].substring(1); | |
cmd = args[1]; | |
args.shift(); | |
args.shift(); | |
} else { | |
cmd = args[0]; | |
args.shift(); | |
} | |
this.handleCommand(prefix, cmd, args); | |
} | |
}; | |
Client.prototype.handleCommand = function (prefix, cmd, args) { | |
switch (cmd) { | |
case 'NICK': | |
this.handleNICK(prefix, args); | |
break; | |
case 'USER': | |
this.handleUSER(prefix, args); | |
break; | |
case 'JOIN': | |
if (args[0] === '#berrytube') { | |
this.socket.write(':' + this.name + ' JOIN #berrytube\r\n'); | |
} | |
break; | |
case 'PING': | |
this.socket.write('PONG :' + this.name + '\r\n'); | |
break; | |
case 'WHO': | |
this.handleWHO(prefix, args); | |
break; | |
case 'NAMES': | |
this.handleNAMES(prefix, args); | |
break; | |
case 'MODE': | |
this.handleMODE(prefix, args); | |
break; | |
case 'PRIVMSG': | |
this.handlePRIVMSG(prefix, args); | |
break; | |
case 'VERSION': | |
this.handleVERSION(prefix, args); | |
break; | |
case 'QUIT': | |
try { | |
this.socket.end(); | |
} catch (e) { | |
console.log(e); | |
} | |
break; | |
default: | |
break; | |
} | |
}; | |
Client.prototype.handleNICK = function (prefix, args) { | |
if (!this.loggedIn && this.inChannel) { | |
if (args[0][0] === ':') { | |
args[0] = args[0].substring(1); | |
} | |
this.bt.emit('setNick', { | |
nick: args[0], | |
pass: false | |
}); | |
} else if (!this.inChannel) { | |
this.socket.write(':' + args[0] + ' JOIN #berrytube\r\n'); | |
this.socket.write(':' + args[0] + ' NICK anonymous\r\n'); | |
this.inChannel = true; | |
} | |
}; | |
Client.prototype.handleUSER = function (prefix, args) { | |
this.rpl('001 {nick} :Welcome to BerryTube'); | |
this.handleVERSION(null, []); | |
this.rpl('375 {nick} :IRC Bridge by cyzon'); | |
// Pidgin is the only IRC client that will break if this is not sent. | |
this.rpl('376 {nick} :End of MOTD'); | |
}; | |
const PREFIXES = { | |
2: '@', | |
1: '%', | |
0: '+' | |
}; | |
Client.prototype.handleWHO = function (prefix, args) { | |
if (args[0] === '#berrytube') { | |
var nicks = Array.prototype.slice.call(this.btnicks); | |
if (this.name === 'anonymous') { | |
nicks.unshift({ | |
nick: 'anonymous', | |
type: -1 | |
}); | |
} | |
for (var i = 0; i < nicks.length; i++) { | |
var u = nicks[i]; | |
this.rpl('352 {nick} ' + [ | |
'#berrytube', | |
u.nick, | |
'berrytube.tv', | |
HOSTNAME, | |
u.nick, | |
'H' + (PREFIXES[u.type] || ''), | |
':0', | |
u.nick | |
].join(' ')); | |
} | |
this.rpl('315 {nick} :End of /WHO list'); | |
} | |
}; | |
Client.prototype.handleNAMES = function (prefix, args) { | |
if (args[0] === '#berrytube') { | |
var names = []; | |
if (this.name === 'anonymous') { | |
names.push('anonymous'); | |
} | |
for (var i = 0; i < this.btnicks.length; i++) { | |
var pre = PREFIXES[this.btnicks[i].type] || ''; | |
names.push(pre + this.btnicks[i].nick); | |
} | |
var msg = ':' + HOSTNAME + ' 353 ' + this.name + ' = #berrytube :'; | |
for (var i = 0; i < names.length; i++) { | |
if (msg.length + names[i].length + 3 > 512) { | |
this.socket.write(msg + '\r\n'); | |
msg = ':' + HOSTNAME + ' 353 ' + this.name + ' = #berrytube :'; | |
} else { | |
msg += names[i] + ' '; | |
} | |
} | |
if (msg[msg.length - 1] !== ':') { | |
this.socket.write(msg + '\r\n'); | |
} | |
this.rpl('366 {nick} #berrytube :End of /NAMES list'); | |
} | |
}; | |
Client.prototype.handleMODE = function (prefix, args) { | |
if (args[0] === this.name) { | |
this.socket.write(':' + HOSTNAME + ' MODE ' + this.name + ' +i\r\n'); | |
} else if (args[0] === '#berrytube') { | |
this.rpl('324 {nick} #berrytube +nt'); | |
} | |
}; | |
Client.prototype.handlePRIVMSG = function (prefix, args) { | |
switch (args[0]) { | |
case 'control': { | |
this.handleControl(args); | |
break; | |
} | |
case '#berrytube': { | |
if (!this.loggedIn) { | |
this.rpl('404 {nick} #berrytube :Cannot send to channel'); | |
break; | |
} | |
args.shift(); | |
if (args[0].indexOf(':') === 0) { | |
args[0] = args[0].substring(1); | |
} | |
var msg = args.join(' '); | |
msg = msg.replace(/\x01ACTION(.*?)\x01/, '/me $1'); | |
this.bt.emit('chat', { | |
msg: args.join(' '), | |
metadata: { | |
channel: 'main', | |
flair: 0 | |
} | |
}); | |
break; | |
} | |
default: | |
this.rpl('404 {nick} ' + args[0] + ' :Cannot send to channel'); | |
break; | |
} | |
}; | |
Client.prototype.handleControl = function (args) { | |
var cmd = args[1].substring(1); | |
args.shift(); | |
args.shift(); | |
var self = this; | |
var cmsg = function (msg) { | |
self.socket.write(':control PRIVMSG ' + self.name + ' :' + msg + '\r\n'); | |
}; | |
switch (cmd) { | |
case 'login': { | |
var pass = false; | |
var nick = args[0]; | |
if (nick === undefined) { | |
this.socket.write(':control PRIVMSG ' + this.name + ' :Invalid login details\r\n'); | |
break; | |
} | |
if (nick[0] === ':') { | |
nick = nick.substring(1); | |
} | |
if (args.length > 1) { | |
pass = args[1]; | |
} | |
this.bt.emit('setNick', { | |
nick: nick, | |
pass: pass | |
}); | |
break; | |
} | |
case 'reconnect': { | |
this.socket.write(':control PRIVMSG ' + this.name + ' :Attempting to reconnect to berrytube\r\n'); | |
try { | |
this.bt.disconnect(); | |
} catch (e) { | |
} | |
this.initBerrytube(); | |
break; | |
} | |
case 'set': { | |
var key = args[0]; | |
if (key === undefined) { | |
this.socket.write(':control PRIVMSG ' + this.name + ' :Available config keys: ' + Object.keys(CONFIG).join(' ') + '\r\n'); | |
break; | |
} | |
var val = args[1]; | |
if (val === '=') { | |
val = args[2]; | |
} | |
if (typeof val !== 'string') { | |
val = ''; | |
} | |
if (!(key in CONFIG)) { | |
this.socket.write(':control PRIVMSG ' + this.name + ' :Unknown config key ' + key + '\r\n'); | |
break; | |
} | |
if (val.trim() === '') { | |
this.socket.write(':control PRIVMSG ' + this.name + ' :Current value of ' + key + ' = ' + CONFIG[key] + '\r\n'); | |
break; | |
} | |
var isBool = (typeof CONFIG[key] === 'boolean'); | |
if (isBool) { | |
val = Boolean(val.match(/^true|1|yes|on$/)); | |
} | |
CONFIG[key] = val; | |
saveConfig(); | |
this.socket.write(':control PRIVMSG ' + this.name + ' :Updated ' + key + ' = ' + val + '\r\n'); | |
break; | |
} | |
case 'poll': { | |
var showPoll = function (poll) { | |
cmsg('Poll (' + poll.title + ')'); | |
for (var i = 0; i < poll.options.length; i++) { | |
cmsg(i +'. [' + poll.votes[i] + '] ' + poll.options[i]); | |
} | |
} | |
if (args[0] === undefined) { | |
if (this.poll == null) { | |
cmsg('No active poll. Use /msg control poll last to view results of previous poll'); | |
} else { | |
showPoll(this.poll); | |
} | |
} else switch (args[0]) { | |
case 'last': { | |
if (this.lastPoll == null) { | |
cmsg('No previous poll recorded'); | |
} else { | |
showPoll(this.lastPoll); | |
} | |
break; | |
} | |
case 'vote': { | |
if (this.poll == null) { | |
cmsg('Cannot vote: no active poll'); | |
break; | |
} | |
var opt = parseInt(args[1]); | |
if (isNaN(opt) || opt < 0 || opt >= this.poll.options.length) { | |
cmsg('Invalid poll choice.'); | |
} else { | |
this.bt.emit('votePoll', { op: opt }); | |
cmsg('Voted for option ' + opt + ': ' + this.poll.options[opt]); | |
} | |
break; | |
} | |
default: { | |
cmsg('Invalid poll command'); | |
break; | |
} | |
} | |
break; | |
} | |
} | |
}; | |
Client.prototype.handleVERSION = function (prefix, args) { | |
this.rpl('004 {nick} cyzonbridge-'+VERSION+'. '+HOSTNAME+' :'); | |
this.rpl('005 {nick} PREFIX=(ohv)@%+ :are supported by this server'); | |
}; | |
Client.prototype.handleBTMessage = function (data) { | |
var nick = data.msg.nick; | |
var msg = data.msg.msg; | |
if (CONFIG.yay_color) { | |
msg = msg.replace(/<span class="flutter">(.*?)<\/span>/g, '\x0313$1\x03'); | |
} | |
if (CONFIG.show_bold) { | |
msg = msg.replace(/<strong>(.*?)<\/strong>/g, '\x02$1\x0f'); | |
} | |
if (CONFIG.show_underline) { | |
msg = msg.replace(/<em>(.*?)<\/em>/g, '\x1f$1\x0f'); | |
} | |
if (CONFIG.hide_spoilers) { | |
msg = msg.replace(/<span class="spoiler">.*?<\/span>/g, '\x02(SPOILER HIDDEN)\x0f'); | |
} | |
if (CONFIG.strip_html) { | |
msg = msg.replace(/<strong>(.*?)<\/strong>/g, '**$1**'); | |
msg = msg.replace(/<em>(.*?)<\/em>/g, '_$1_'); | |
msg = msg.replace(/<strike>(.*?)<\/strike>/g, '~~$1~~'); | |
msg = msg.replace(/<\/?.*?>(.*?)/g, '$1'); | |
} | |
msg = msg.replace(/>/g, '>'); | |
msg = msg.replace(/</g, '<'); | |
msg = msg.replace(/&/g, '&'); | |
switch (data.msg.emote) { | |
case 'request': | |
msg = 'requests ' + msg; | |
if (CONFIG.request_color) { | |
msg = '\x032' + msg + '\x0f'; | |
} | |
msg = '\x01ACTION ' + msg + '\x01'; | |
break; | |
case 'rcv': | |
if (CONFIG.rcv_bold) { | |
msg = '\x02' + msg + '\x0f'; | |
} | |
if (CONFIG.rcv_color) { | |
msg = '\x034' + msg + '\x0f'; | |
} | |
break; | |
case 'drink': | |
msg = msg + ' drink!'; | |
if (data.msg.multi > 1) { | |
msg += ' (x' + data.msg.multi + ')'; | |
} | |
if (CONFIG.drink_bold) { | |
msg = '\x02' + msg + '\x0f'; | |
} | |
break; | |
case 'act': | |
msg = '\x01ACTION ' + msg + '\x01'; | |
break; | |
case 'spoiler': | |
if (CONFIG.hide_spoilers) { | |
msg = '\x02(SPOILER HIDDEN)\x0f'; | |
} | |
break; | |
default: | |
break; | |
} | |
if (nick !== this.name || CONFIG.echo) { | |
nick = nick + '!' + nick + '@berrytube.tv'; | |
this.socket.write(':' + nick + ' PRIVMSG #berrytube :' + msg + '\r\n'); | |
} | |
}; | |
Client.prototype.handleBTPoll = function (data) { | |
this.poll = data; | |
var msg = 'opened poll: ' + data.title; | |
if (CONFIG.poll_bold) { | |
msg = '\x02' + msg + '\x0f'; | |
} | |
msg = '\x01ACTION ' + msg + '\x01'; | |
var nick = data.creator + '!' + data.creator + '@berrytube.tv'; | |
this.socket.write(':' + nick + ' PRIVMSG #berrytube :' + msg + '\r\n'); | |
}; | |
Client.prototype.handleBTPollUpdate = function (data) { | |
this.poll.votes = data.votes; | |
}; | |
process.on('uncaughtException', function (e) { | |
console.log(e); | |
}); | |
var port = process.argv[2]; | |
if (port !== undefined && port.match(/\d+/)) { | |
port = parseInt(port); | |
} else { | |
port = 6667; | |
} | |
loadConfig(); | |
var s = new IRCServer(port); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment