OmnomIRC.js 11 KB

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