OmnomIRC.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. #!node
  2. process.chdir(__dirname);
  3. var fs = require('fs'),
  4. url = require('url'),
  5. path = require('path'),
  6. vm = require('vm'),
  7. toobusy = function(){return false;},//require('toobusy'),
  8. noop = function(){},
  9. cluster = require('cluster'),
  10. ircClient = require('node-irc'),
  11. logger = {
  12. log: function(msg){
  13. if(options.loglevel > 2){
  14. console.log(msg);
  15. }
  16. },
  17. debug: function(msg){
  18. if(options.loglevel > 2){
  19. console.log('DEBUG - '+msg);
  20. }
  21. },
  22. warn: function(msg){
  23. if(options.loglevel > 1){
  24. console.log('WARN - '+msg);
  25. }
  26. },
  27. info: function(msg){
  28. if(options.loglevel > 1){
  29. console.log('INFO - '+msg);
  30. }
  31. },
  32. error: function(msg){
  33. if(options.loglevel > 0){
  34. console.error(msg);
  35. }
  36. }
  37. },
  38. options = global.options = (function(){
  39. console.log('parsing options');
  40. var defaults = {
  41. port: 80,
  42. loglevel: 3,
  43. threads: require('os').cpus().length,
  44. redis: {
  45. port: 6379,
  46. host: 'localhost'
  47. },
  48. debug: false,
  49. paths: {
  50. www: './www/',
  51. api: './api/',
  52. plugins: './plugins/'
  53. },
  54. irc: {
  55. host: 'irp.irc.omnimaga.org',
  56. port: 6667,
  57. nick: 'oirc3',
  58. name: 'OmnomIRC3',
  59. channels: [
  60. '#omnimaga'
  61. ],
  62. messages: {
  63. quit: 'Server closed'
  64. }
  65. },
  66. origins: [
  67. ['O','OmnomIRC'],
  68. ['#','IRC']
  69. ]
  70. },
  71. i,
  72. options;
  73. try{
  74. options = JSON.parse(fs.readFileSync('./options.json'));
  75. defaults = (function merge(options,defaults){
  76. for(var i in options){
  77. if(typeof defaults[i] != 'object' || defaults[i] instanceof Array){
  78. defaults[i] = options[i];
  79. }else{
  80. defaults[i] = merge(options[i],defaults[i]);
  81. }
  82. }
  83. return defaults
  84. })(options,defaults);
  85. }catch(e){
  86. console.warn('Using default settings. Please create options.json');
  87. console.error(e);
  88. }
  89. defaults.origins.unshift(['S','Server'],['?','Unknown']);
  90. options = {};
  91. for(i in defaults){
  92. Object.defineProperty(options,i,{
  93. value: defaults[i],
  94. enumerable: true,
  95. writable: false
  96. });
  97. }
  98. console.log('done parsing options');
  99. return options;
  100. })(),
  101. origin = function(name){
  102. for(var i in options.origins){
  103. if(options.origins[i][1] == name){
  104. return i;
  105. }
  106. }
  107. return 1;
  108. };
  109. if(typeof fs.existsSync == 'undefined') fs.existsSync = path.existsSync; // legacy support
  110. if(cluster.isMaster){
  111. var iWorker;
  112. cluster.on('exit', function(worker, code, signal) {
  113. console.log('worker ' + worker.process.pid + ' died');
  114. });
  115. iWorker = global.iw = cluster.fork();
  116. iWorker.on('online',function(){
  117. logger.info('First worker online');
  118. iWorker.send('S');
  119. });
  120. for(var i=1;i<options.threads;i++){
  121. cluster.fork().on('online',function(){
  122. logger.info('Child socket worker online');
  123. });
  124. }
  125. for(i in cluster.workers){
  126. var worker = cluster.workers[i];
  127. worker.on('message',function(msg){
  128. var c = msg[0];
  129. msg = msg.substr(1);
  130. logger.debug('Parent recieved command '+c+' with message '+msg);
  131. switch(c){
  132. case 'M':
  133. iWorker.send('M'+msg);
  134. break;
  135. }
  136. });
  137. }
  138. if(options.debug){
  139. require('repl').start({
  140. prompt: '> ',
  141. useGlobal: true
  142. }).on('exit',function(){
  143. for(var i in cluster.workers){
  144. cluster.workers[i].send('Q');
  145. }
  146. process.exit();
  147. });
  148. }
  149. }else{
  150. process.on('message',function(msg){
  151. var c = msg[0];
  152. msg = msg.substr(1);
  153. logger.debug('Child recieved command '+c+' with message '+msg);
  154. switch(c){
  155. case 'Q':
  156. if(typeof app != 'undefined' && typeof irc == 'undefined'){
  157. app.close();
  158. }else if(typeof irc != 'undefined'){
  159. irc.quit(options.irc.messages.quit);
  160. }
  161. break;
  162. case 'M':
  163. if(typeof irc != 'undefined'){
  164. msg = JSON.parse(msg);
  165. if(msg.message){
  166. irc.say(msg.room,'('+options.origins[msg.origin][0]+')'+'<'+msg.from+'> '+msg.message);
  167. }
  168. }
  169. break;
  170. case 'S':
  171. logger.info('Child starting irc');
  172. irc = new ircClient(options.irc.host,options.irc.port,options.irc.nick,options.irc.name);
  173. irc.on('ready',function(){
  174. logger.info('Connected to IRC');
  175. for(var i in options.irc.channels){
  176. irc.join(options.irc.channels[i]);
  177. //irc.client.send('WHO %s\n',options.irc.channels[i]);
  178. }
  179. });
  180. irc.on('CHANMSG',function(d){
  181. console.log(d);
  182. message(d.reciever,d.sender,d.message,origin('IRC'));
  183. });
  184. // Beginnings of names handler
  185. /*irc.on('names',function(chan,nicks){
  186. for(var i in nicks){
  187. logger.debug('[NICKS] Channel '+chan+' '+nicks[i]);
  188. }
  189. });*/
  190. irc.connect();
  191. logger.debug('Connecting to IRC');
  192. break;
  193. }
  194. });
  195. logger.info('Child starting socket.io');
  196. var RedisStore = require('socket.io/lib/stores/redis'),
  197. redis = require('socket.io/node_modules/redis'),
  198. pub = redis.createClient(options.redis.port,options.redis.host),
  199. sub = redis.createClient(options.redis.port,options.redis.host),
  200. client = redis.createClient(options.redis.port,options.redis.host),
  201. mimeTypes = {
  202. 'html': 'text/html',
  203. 'js': 'text/javascript',
  204. 'css': 'text/css',
  205. 'png': 'image/png',
  206. 'jpg': 'image/jpeg'
  207. },
  208. app = require('http').createServer(function(req,res){
  209. if(toobusy()){
  210. res.writeHead(503,{
  211. 'Content-type': 'text/plain'
  212. });
  213. res.write('503 Server busy.\n');
  214. res.end();
  215. return;
  216. }
  217. req.addListener('end',function(){
  218. logger.debug('served static content for '+req.url);
  219. var uri = url.parse(req.url).pathname,
  220. serveFile = function(filename,req,res){
  221. try{
  222. stats = fs.lstatSync(filename);
  223. }catch(e){
  224. res.writeHead(404,{
  225. 'Content-type': 'text/plain'
  226. });
  227. res.write('404 Not Found.\n');
  228. res.end();
  229. return;
  230. }
  231. if(stats.isFile()){
  232. var fileStream,
  233. mimetype = mimeTypes[path.extname(filename).split('.')[1]];
  234. res.writeHead(200,{
  235. 'Content-Type': mimetype
  236. });
  237. fileStream = fs.createReadStream(filename);
  238. fileStream.pipe(res);
  239. }else if(stats.isDirectory()){
  240. if(fs.existsSync(path.join(filename,'index.html'))){
  241. serveFile(path.join(filename,'index.html'),req,res);
  242. }else if(fs.existsSync(path.join(filename,'index.htm'))){
  243. serveFile(path.join(filename,'index.htm'),req,res);
  244. }else if(fs.existsSync(path.join(filename,'index.txt'))){
  245. serveFile(path.join(filename,'index.txt'),req,res);
  246. }else{
  247. res.writeHead(200,{
  248. 'Content-Type': 'text/plain'
  249. });
  250. res.write('Index of '+url+'\n');
  251. res.write('TODO, show index');
  252. res.end();
  253. }
  254. }else{
  255. res.writeHead(500,{
  256. 'Content-Type': 'text/plain'
  257. });
  258. res.write('500 Internal server error\n');
  259. res.end();
  260. }
  261. },
  262. filepath = unescape(uri);
  263. if(filepath.substr(0,5) == '/api/'){
  264. filepath = path.join(options.paths.api,filepath.substr(5));
  265. logger.debug('Attempting to run api script '+filepath);
  266. if(fs.existsSync(filepath)){
  267. fs.readFile(filepath,function(e,data){
  268. if(e){
  269. logger.error(e);
  270. res.end('null;');
  271. }else{
  272. var output = '',
  273. sandbox = {
  274. log: function(text){
  275. output += text;
  276. },
  277. error: function(msg){
  278. logger.error(msg);
  279. },
  280. info: function(msg){
  281. logger.info(msg);
  282. },
  283. debug: function(msg){
  284. logger.debug(msg);
  285. },
  286. head: {
  287. 'Content-Type': 'text/javascript'
  288. },
  289. returnCode: 200,
  290. vm: vm,
  291. fs: fs,
  292. options: options
  293. };
  294. vm.runInNewContext(data,sandbox,filepath);
  295. res.writeHead(sandbox.returnCode,sandbox.head);
  296. res.end(output);
  297. }
  298. });
  299. }else{
  300. res.writeHead(404,{
  301. 'Content-Type': 'text/javascript'
  302. });
  303. res.end('null;');
  304. }
  305. }else{
  306. serveFile(path.join(options.paths.www,filepath),req,res);
  307. }
  308. }).resume();
  309. }).listen(options.port),
  310. io = require('socket.io').listen(app);
  311. io.set('log level',options.loglevel);
  312. io.log = logger;
  313. if(typeof options.redis.password != 'undefined'){
  314. var eh = function(e){
  315. throw e;
  316. };
  317. pub.auth(options.redis.ppassword,eh);
  318. sub.auth(options.redis.ppassword,eh);
  319. client.auth(options.redis.ppassword,eh);
  320. }
  321. io.set('store', new RedisStore({
  322. redisPub : pub,
  323. redisSub : sub,
  324. redisClient : client
  325. }));
  326. io.sockets.on('connection',function(socket){
  327. socket.on('join',function(data){
  328. socket.join(data.name);
  329. data.title = data.name;
  330. socket.emit('join',{
  331. name: data.name
  332. });
  333. sendUserList(data.name);
  334. socket.get('nick',function(e,nick){
  335. logger.debug(nick+' joined '+data.name);
  336. fromServer(data.name,nick+' joined the channel');
  337. });
  338. });
  339. socket.on('part',function(data){
  340. socket.leave(data.name);
  341. socket.get('nick',function(e,nick){
  342. logger.debug(nick+' left '+data.name);
  343. sendUserList(data.name);
  344. });
  345. });
  346. socket.on('disconnect',function(data){
  347. var rooms = io.sockets.manager.rooms,
  348. i,
  349. room;
  350. for(i in rooms){
  351. if(rooms[i] != '' && typeof rooms[i] == 'string'){
  352. try{
  353. room = rooms[i].substr(1);
  354. }catch(e){}
  355. sendUserList(room);
  356. }
  357. }
  358. });
  359. socket.on('message',function(data){
  360. logger.debug('message sent to '+data.room);
  361. io.sockets.in(data.room).emit('message',data);
  362. process.send('M'+JSON.stringify(data));
  363. });
  364. socket.on('echo',function(data){
  365. logger.debug('echoing to '+data.room);
  366. socket.emit('message',data);
  367. });
  368. socket.on('names',function(data){
  369. var sockets = io.sockets.clients(data.name),
  370. i;
  371. runWithUserList(data.name,function(users){
  372. var temp = [],i;
  373. for(i in users) i && i != null && temp.push(users[i]);
  374. users = temp;
  375. fromServer(data.name,data.name+" users:\n\t\t"+users.join("\n\t\t"),socket);
  376. sendUserList(data.name);
  377. });
  378. });
  379. socket.on('auth',function(data){
  380. logger.info(data.nick+' registered');
  381. // TODO - authorize
  382. socket.set('nick',data.nick.substr(0,12));
  383. socket.emit('authorized',{
  384. nick: data.nick.substr(0,12)
  385. });
  386. });
  387. var runWithUserList = function(room,callback){
  388. var sockets = io.sockets.clients(room),
  389. i = 0,
  390. ret = [],
  391. getNext = function(){
  392. if(i < sockets.length){
  393. sockets[i].get('nick',function(e,nick){
  394. if(e){
  395. logger.error(e);
  396. }else if(!inArray(ret,nick)){
  397. logger.debug(room+' '+nick);
  398. ret.push(nick);
  399. }
  400. i++;
  401. getNext();
  402. });
  403. }else{
  404. callback(ret);
  405. }
  406. };
  407. getNext();
  408. },
  409. inArray = function(arr,val){
  410. for(var i in arr){
  411. if(arr[i] == val){
  412. return true;
  413. }
  414. }
  415. return false;
  416. },
  417. sendUserList = function(room){
  418. if(typeof room != 'undefined'){
  419. runWithUserList(room,function(users){
  420. io.sockets.in(room).emit('names',{
  421. room: room,
  422. names: users
  423. });
  424. });
  425. }
  426. },
  427. message = function(room,from,message,origin,socket){
  428. if(typeof socket == 'undefined'){
  429. socket = io.sockets.in(room);
  430. }
  431. socket.emit('message',{
  432. message: message,
  433. room: room,
  434. from: from,
  435. origin: origin
  436. })
  437. },
  438. fromServer = function(room,message,socket){
  439. if(typeof socket == 'undefined'){
  440. socket = io.sockets.in(room);
  441. }
  442. socket.emit('message',{
  443. message: message,
  444. room: room,
  445. from: 0,
  446. origin: 2
  447. });
  448. };
  449. });
  450. }
  451. process.on('uncaughtException',function(e){
  452. if(typeof logger != 'undefined'){
  453. logger.error(e);
  454. }else{
  455. console.error(e);
  456. }
  457. });