Advertisement
sparkychild

Untitled

Jun 8th, 2015
231
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. /**
  2. * This is the file where commands get parsed
  3. *
  4. * Some parts of this code are taken from the Pokémon Showdown server code, so
  5. * credits also go to Guangcong Luo and other Pokémon Showdown contributors.
  6. * https://github.com/Zarel/Pokemon-Showdown
  7. *
  8. * @license MIT license
  9. */
  10.  
  11. var fs = require('fs');
  12. var http = require('http');
  13. var https = require('https');
  14. var url = require('url');
  15.  
  16. const ACTION_COOLDOWN = 3 * 1000;
  17. const FLOOD_MESSAGE_NUM = 5;
  18. const FLOOD_PER_MSG_MIN = 500; // this is the minimum time between messages for legitimate spam. It's used to determine what "flooding" is caused by lag
  19. const FLOOD_MESSAGE_TIME = 6 * 1000;
  20. const MIN_CAPS_LENGTH = 12;
  21. const MIN_CAPS_PROPORTION = 0.8;
  22.  
  23. // TODO: move to rooms.js
  24. // TODO: store settings by room, not command/blacklists
  25. var settings;
  26. try {
  27. settings = JSON.parse(fs.readFileSync('settings.json'));
  28. } catch (e) {} // file doesn't exist [yet]
  29. if (!Object.isObject(settings)) settings = {};
  30.  
  31. exports.parse = {
  32.  
  33. actionUrl: url.parse('https://play.pokemonshowdown.com/~~' + Config.serverid + '/action.php'),
  34. 'settings': settings,
  35. // TODO: handle chatdata in users.js
  36. chatData: {},
  37. // TODO: handle blacklists in rooms.js
  38. blacklistRegexes: {},
  39.  
  40. data: function (data) {
  41. if (data.charAt(0) !== 'a') return false;
  42. data = JSON.parse(data.substr(1));
  43. if (Array.isArray(data)) {
  44. for (let i = 0, len = data.length; i < len; i++) {
  45. this.splitMessage(data[i]);
  46. }
  47. } else {
  48. this.splitMessage(data);
  49. }
  50. },
  51. splitMessage: function (message) {
  52. if (!message) return;
  53.  
  54. var room = null;
  55. if (message.indexOf('\n') < 0) return this.message(message, room);
  56.  
  57. var spl = message.split('\n');
  58. if (spl[0].charAt(0) === '>') {
  59. if (spl[1].substr(1, 10) === 'tournament') return false;
  60. let roomid = spl.shift().substr(1);
  61. room = Rooms.get(roomid);
  62. if (spl[0].substr(1, 4) === 'init') {
  63. let users = spl[2].substr(7);
  64. room = Rooms.add(roomid, !Config.rooms.includes(roomid));
  65. room.onUserlist(users);
  66. return ok('joined ' + room.id);
  67. }
  68. }
  69.  
  70. for (let i = 0, len = spl.length; i < len; i++) {
  71. this.message(spl[i], room);
  72. }
  73. },
  74. message: function (message, room) {
  75. var spl = message.split('|');
  76. switch (spl[1]) {
  77. case 'challstr':
  78. info('received challstr, logging in...');
  79. let id = spl[2];
  80. let str = spl[3];
  81.  
  82. let requestOptions = {
  83. hostname: this.actionUrl.hostname,
  84. port: this.actionUrl.port,
  85. path: this.actionUrl.pathname,
  86. agent: false
  87. };
  88.  
  89. let data;
  90. if (!Config.pass) {
  91. requestOptions.method = 'GET';
  92. requestOptions.path += '?act=getassertion&userid=' + toId(Config.nick) + '&challengekeyid=' + id + '&challenge=' + str;
  93. } else {
  94. requestOptions.method = 'POST';
  95. data = 'act=login&name=' + Config.nick + '&pass=' + Config.pass + '&challengekeyid=' + id + '&challenge=' + str;
  96. requestOptions.headers = {
  97. 'Content-Type': 'application/x-www-form-urlencoded',
  98. 'Content-Length': data.length
  99. };
  100. }
  101.  
  102. let req = https.request(requestOptions, function (res) {
  103. res.setEncoding('utf8');
  104. let data = '';
  105. res.on('data', function (chunk) {
  106. data += chunk;
  107. });
  108. res.on('end', function () {
  109. if (data === ';') {
  110. error('failed to log in; nick is registered - invalid or no password given');
  111. process.exit(-1);
  112. }
  113. if (data.length < 50) {
  114. error('failed to log in: ' + data);
  115. process.exit(-1);
  116. }
  117.  
  118. if (data.indexOf('heavy load') !== -1) {
  119. error('the login server is under heavy load; trying again in one minute');
  120. setTimeout(function () {
  121. this.message(message);
  122. }.bind(this), 60 * 1000);
  123. return;
  124. }
  125.  
  126. if (data.substr(0, 16) === '<!DOCTYPE html>') {
  127. error('Connection error 522; trying agian in one minute');
  128. setTimeout(function () {
  129. this.message(message);
  130. }.bind(this), 60 * 1000);
  131. return;
  132. }
  133.  
  134. try {
  135. data = JSON.parse(data.substr(1));
  136. if (data.actionsuccess) {
  137. data = data.assertion;
  138. } else {
  139. error('could not log in; action was not successful: ' + JSON.stringify(data));
  140. process.exit(-1);
  141. }
  142. } catch (e) {}
  143. send('|/trn ' + Config.nick + ',0,' + data);
  144. }.bind(this));
  145. }.bind(this));
  146.  
  147. req.on('error', function (err) {
  148. error('login error: ' + err.stack);
  149. });
  150.  
  151. if (data) req.write(data);
  152. req.end();
  153. break;
  154. case 'updateuser':
  155. if (spl[2] !== Config.nick) return;
  156.  
  157. if (spl[3] !== '1') {
  158. error('failed to log in, still guest');
  159. process.exit(-1);
  160. }
  161.  
  162. ok('logged in as ' + spl[2]);
  163. send('|/blockchallenges');
  164.  
  165. // Now join the rooms
  166. Rooms.join();
  167.  
  168. if (this.settings.blacklist) {
  169. let blacklist = this.settings.blacklist;
  170. for (let room in blacklist) {
  171. this.updateBlacklistRegex(room);
  172. }
  173. }
  174. setInterval(this.cleanChatData.bind(this), 30 * 60 * 1000);
  175. break;
  176. case 'c':
  177. let username = spl[2];
  178. let user = Users.get(username);
  179. if (!user) return false; // various "chat" responses contain other data
  180. if (user === Users.self) return false;
  181. if (this.isBlacklisted(user.id, room.id)) this.say(room, '/roomban ' + user.id + ', Blacklisted user');
  182.  
  183. spl = spl.slice(3).join('|');
  184. if (!user.hasRank(room.id, '%')) this.processChatData(user.id, room.id, spl);
  185. this.chatMessage(spl, user, room);
  186. break;
  187. case 'c:':
  188. let username = spl[3];
  189. let user = Users.get(username);
  190. if (!user) return false; // various "chat" responses contain other data
  191. if (user === Users.self) return false;
  192. if (this.isBlacklisted(user.id, room.id)) this.say(room, '/roomban ' + user.id + ', Blacklisted user');
  193.  
  194. spl = spl.slice(4).join('|');
  195. if (!user.hasRank(room.id, '%')) this.processChatData(user.id, room.id, spl);
  196. this.chatMessage(spl, user, room);
  197. break;
  198. case 'pm':
  199. let username = spl[2];
  200. let user = Users.get(username);
  201. let group = username.charAt(0);
  202. if (!user) user = Users.add(username);
  203. if (user === Users.self) return false;
  204.  
  205. spl = spl.slice(4).join('|');
  206. if (spl.startsWith('/invite ') && user.hasRank(group, '%') &&
  207. !(toId(spl.substr(8)) === 'lobby' && Config.serverid === 'showdown')) {
  208. return send('|/join ' + spl.substr(8));
  209. }
  210. this.chatMessage(spl, user, user);
  211. break;
  212. case 'N':
  213. let username = spl[2];
  214. let oldid = spl[3];
  215. let user = room.onRename(username, oldid);
  216. if (this.isBlacklisted(user.id, room.id)) this.say(room, '/roomban ' + user.id + ', Blacklisted user');
  217. this.updateSeen(oldid, spl[1], user.id);
  218. break;
  219. case 'J': case 'j':
  220. let username = spl[2];
  221. let user = room.onJoin(username, username.charAt(0));
  222. if (user === Users.self) return false;
  223. if (this.isBlacklisted(user.id, room.id)) this.say(room, '/roomban ' + user.id + ', Blacklisted user');
  224. this.updateSeen(user.id, spl[1], room.id);
  225.  
  226. return this.say(room, '/w ' + user.id + ', Welcome to' + room);
  227.  
  228. break;
  229. case 'l': case 'L':
  230. let username = spl[2];
  231. let user = room.onLeave(username);
  232. if (user) {
  233. if (user === Users.self) return false;
  234. this.updateSeen(user.id, spl[1], room.id);
  235. } else {
  236. this.updateSeen(toId(username), spl[1], room.id);
  237. }
  238. break;
  239. }
  240. },
  241. chatMessage: function (message, user, room) {
  242. var cmdrMessage = '["' + room.id + '|' + user.name + '|' + message + '"]';
  243. message = message.trim();
  244. if (message.substr(0, Config.commandcharacter.length) !== Config.commandcharacter) return false;
  245.  
  246. message = message.substr(Config.commandcharacter.length);
  247. var index = message.indexOf(' ');
  248. var arg = '';
  249. var cmd = message;
  250. if (index > -1) {
  251. cmd = cmd.substr(0, index);
  252. arg = message.substr(index + 1).trim();
  253. }
  254.  
  255. if (!!Commands[cmd]) {
  256. let failsafe = 0;
  257. while (typeof Commands[cmd] !== "function" && failsafe++ < 10) {
  258. cmd = Commands[cmd];
  259. }
  260. if (typeof Commands[cmd] === "function") {
  261. cmdr(cmdrMessage);
  262. Commands[cmd].call(this, arg, user, room);
  263. } else {
  264. error("invalid command type for " + cmd + ": " + (typeof Commands[cmd]));
  265. }
  266. }
  267. },
  268. say: function (target, text) {
  269. var targetId = target.id;
  270. if (Rooms.get(targetId)) {
  271. send((targetId !== 'lobby' ? targetId : '') + '|' + text);
  272. } else {
  273. send('|/pm ' + targetId + ', ' + text);
  274. }
  275. },
  276. isBlacklisted: function (userid, roomid) {
  277. var blacklistRegex = this.blacklistRegexes[roomid];
  278. return blacklistRegex && blacklistRegex.test(userid);
  279. },
  280. blacklistUser: function (userid, roomid) {
  281. var blacklist = this.settings.blacklist || (this.settings.blacklist = {});
  282. if (blacklist[roomid]) {
  283. if (blacklist[roomid][userid]) return false;
  284. } else {
  285. blacklist[roomid] = {};
  286. }
  287.  
  288. blacklist[roomid][userid] = 1;
  289. this.updateBlacklistRegex(roomid);
  290. return true;
  291. },
  292. unblacklistUser: function (userid, roomid) {
  293. var blacklist = this.settings.blacklist;
  294. if (!blacklist || !blacklist[roomid] || !blacklist[roomid][userid]) return false;
  295.  
  296. delete blacklist[roomid][userid];
  297. if (Object.isEmpty(blacklist[roomid])) {
  298. delete blacklist[roomid];
  299. delete this.blacklistRegexes[roomid];
  300. } else {
  301. this.updateBlacklistRegex(roomid);
  302. }
  303. return true;
  304. },
  305. updateBlacklistRegex: function (roomid) {
  306. var blacklist = this.settings.blacklist[roomid];
  307. var buffer = [];
  308. for (let entry in blacklist) {
  309. if (entry.startsWith('/') && entry.endsWith('/i')) {
  310. buffer.push(entry.slice(1, -2));
  311. } else {
  312. buffer.push('^' + entry + '$');
  313. }
  314. }
  315. this.blacklistRegexes[roomid] = new RegExp(buffer.join('|'), 'i');
  316. },
  317. uploadToHastebin: function (toUpload, callback) {
  318. if (typeof callback !== 'function') return false;
  319. var reqOpts = {
  320. hostname: 'hastebin.com',
  321. method: 'POST',
  322. path: '/documents'
  323. };
  324.  
  325. var req = http.request(reqOpts, function (res) {
  326. res.on('data', function (chunk) {
  327. // CloudFlare can go to hell for sending the body in a header request like this
  328. if (typeof chunk === 'string' && chunk.substr(0, 15) === '<!DOCTYPE html>') return callback('Error uploading to Hastebin.');
  329. var filename = JSON.parse(chunk.toString()).key;
  330. callback('http://hastebin.com/raw/' + filename);
  331. });
  332. });
  333. req.on('error', function (e) {
  334. callback('Error uploading to Hastebin: ' + e.message);
  335. });
  336.  
  337. req.write(toUpload);
  338. req.end();
  339. },
  340. processChatData: function (userid, roomid, msg) {
  341. // NOTE: this is still in early stages
  342. msg = msg.trim().replace(/[ \u0000\u200B-\u200F]+/g, ' '); // removes extra spaces and null characters so messages that should trigger stretching do so
  343. this.updateSeen(userid, 'c', roomid);
  344. var now = Date.now();
  345. if (!this.chatData[userid]) this.chatData[userid] = {
  346. zeroTol: 0,
  347. lastSeen: '',
  348. seenAt: now
  349. };
  350. var userData = this.chatData[userid];
  351. if (!userData[roomid]) userData[roomid] = {
  352. times: [],
  353. points: 0,
  354. lastAction: 0
  355. };
  356. var roomData = userData[roomid];
  357.  
  358. roomData.times.push(now);
  359.  
  360. // this deals with punishing rulebreakers, but note that the bot can't think, so it might make mistakes
  361. if (Config.allowmute && Users.self.hasRank(roomid, '%') && Config.whitelist.indexOf(userid) < 0) {
  362. let useDefault = !(this.settings.modding && this.settings.modding[roomid]);
  363. let pointVal = 0;
  364. let muteMessage = '';
  365. let modSettings = useDefault ? null : this.settings.modding[roomid];
  366.  
  367. // moderation for banned words
  368. if ((useDefault || !this.settings.banword[roomid]) && pointVal < 2) {
  369. let bannedPhraseSettings = this.settings.bannedphrases;
  370. let bannedPhrases = !!bannedPhraseSettings ? (Object.keys(bannedPhraseSettings[roomid] || {})).concat(Object.keys(bannedPhraseSettings.global || {})) : [];
  371. for (let bannedPhrase of bannedPhrases) {
  372. if (msg.toLowerCase().indexOf(bannedPhrase) > -1) {
  373. pointVal = 2;
  374. muteMessage = ', Automated response: your message contained a banned phrase';
  375. break;
  376. }
  377. }
  378. }
  379. // moderation for flooding (more than x lines in y seconds)
  380. let times = roomData.times;
  381. let timesLen = times.length;
  382. let isFlooding = (timesLen >= FLOOD_MESSAGE_NUM && (now - times[timesLen - FLOOD_MESSAGE_NUM]) < FLOOD_MESSAGE_TIME &&
  383. (now - times[timesLen - FLOOD_MESSAGE_NUM]) > (FLOOD_PER_MSG_MIN * FLOOD_MESSAGE_NUM));
  384. if ((useDefault || !('flooding' in modSettings)) && isFlooding) {
  385. if (pointVal < 2) {
  386. pointVal = 2;
  387. muteMessage = ', Automated response: flooding';
  388. }
  389. }
  390. // moderation for caps (over x% of the letters in a line of y characters are capital)
  391. let capsMatch = msg.replace(/[^A-Za-z]/g, '').match(/[A-Z]/g);
  392. if ((useDefault || !('caps' in modSettings)) && capsMatch && toId(msg).length > MIN_CAPS_LENGTH && (capsMatch.length >= ~~(toId(msg).length * MIN_CAPS_PROPORTION))) {
  393. if (pointVal < 1) {
  394. pointVal = 1;
  395. muteMessage = ', Automated response: caps';
  396. }
  397. }
  398. // moderation for stretching (over x consecutive characters in the message are the same)
  399. let stretchMatch = /(.)\1{7,}/gi.test(msg) || /(..+)\1{4,}/gi.test(msg); // matches the same character (or group of characters) 8 (or 5) or more times in a row
  400. if ((useDefault || !('stretching' in modSettings)) && stretchMatch) {
  401. if (pointVal < 1) {
  402. pointVal = 1;
  403. muteMessage = ', Automated response: stretching';
  404. }
  405. }
  406.  
  407. if (pointVal > 0 && now - roomData.lastAction >= ACTION_COOLDOWN) {
  408. let cmd = 'mute';
  409. // defaults to the next punishment in Config.punishVals instead of repeating the same action (so a second warn-worthy
  410. // offence would result in a mute instead of a warn, and the third an hourmute, etc)
  411. if (roomData.points >= pointVal && pointVal < 4) {
  412. roomData.points++;
  413. cmd = Config.punishvals[roomData.points] || cmd;
  414. } else { // if the action hasn't been done before (is worth more points) it will be the one picked
  415. cmd = Config.punishvals[pointVal] || cmd;
  416. roomData.points = pointVal; // next action will be one level higher than this one (in most cases)
  417. }
  418. if (Config.privaterooms.indexOf(roomid) > -1 && cmd === 'warn') cmd = 'mute'; // can't warn in private rooms
  419. // if the bot has % and not @, it will default to hourmuting as its highest level of punishment instead of roombanning
  420. if (roomData.points >= 4 && !Users.self.hasRank(roomid, '@')) cmd = 'hourmute';
  421. if (userData.zeroTol > 4) { // if zero tolerance users break a rule they get an instant roomban or hourmute
  422. muteMessage = ', Automated response: zero tolerance user';
  423. cmd = Users.self.hasRank(roomid, '@') ? 'roomban' : 'hourmute';
  424. }
  425. if (roomData.points > 1) userData.zeroTol++; // getting muted or higher increases your zero tolerance level (warns do not)
  426. roomData.lastAction = now;
  427. this.say(Rooms.get(roomid), '/' + cmd + ' ' + userid + muteMessage);
  428. }
  429. }
  430. },
  431. cleanChatData: function () {
  432. var chatData = this.chatData;
  433. for (let user in chatData) {
  434. for (let room in chatData[user]) {
  435. let roomData = chatData[user][room];
  436. if (!Object.isObject(roomData)) continue;
  437.  
  438. if (!roomData.times || !roomData.times.length) {
  439. delete chatData[user][room];
  440. continue;
  441. }
  442. let newTimes = [];
  443. let now = Date.now();
  444. let times = roomData.times;
  445. for (let time of times) {
  446. if (now - time < 5 * 1000) newTimes.push(time);
  447. }
  448. newTimes.sort(function (a, b) {
  449. return a - b;
  450. });
  451. roomData.times = newTimes;
  452. if (roomData.points > 0 && roomData.points < 4) roomData.points--;
  453. }
  454. }
  455. },
  456.  
  457. updateSeen: function (user, type, detail) {
  458. if (type !== 'n' && Config.rooms.indexOf(detail) < 0 || Config.privaterooms.indexOf(detail) > -1) return;
  459. var now = Date.now();
  460. if (!this.chatData[user]) this.chatData[user] = {
  461. zeroTol: 0,
  462. lastSeen: '',
  463. seenAt: now
  464. };
  465. if (!detail) return;
  466. var userData = this.chatData[user];
  467. var msg = '';
  468. switch (type) {
  469. case 'j':
  470. case 'J':
  471. msg += 'joining ';
  472. break;
  473. case 'l':
  474. case 'L':
  475. msg += 'leaving ';
  476. break;
  477. case 'c':
  478. case 'c:':
  479. msg += 'chatting in ';
  480. break;
  481. case 'N':
  482. msg += 'changing nick to ';
  483. if (detail.charAt(0) !== ' ') detail = detail.substr(1);
  484. break;
  485. }
  486. msg += detail.trim() + '.';
  487. userData.lastSeen = msg;
  488. userData.seenAt = now;
  489. },
  490. getTimeAgo: function (time) {
  491. time = ~~((Date.now() - time) / 1000);
  492.  
  493. var seconds = time % 60;
  494. var times = [];
  495. if (seconds) times.push(seconds + (seconds === 1 ? ' second': ' seconds'));
  496. if (time >= 60) {
  497. time = ~~((time - seconds) / 60);
  498. let minutes = time % 60;
  499. if (minutes) times.unshift(minutes + (minutes === 1 ? ' minute' : ' minutes'));
  500. if (time >= 60) {
  501. time = ~~((time - minutes) / 60);
  502. let hours = time % 24;
  503. if (hours) times.unshift(hours + (hours === 1 ? ' hour' : ' hours'));
  504. if (time >= 24) {
  505. let days = ~~((time - hours) / 24);
  506. if (days) times.unshift(days + (days === 1 ? ' day' : ' days'));
  507. }
  508. }
  509. }
  510. if (!times.length) return '0 seconds';
  511. return times.join(', ');
  512. },
  513. writeSettings: (function () {
  514. var writing = false;
  515. var writePending = false; // whether or not a new write is pending
  516. var finishWriting = function () {
  517. writing = false;
  518. if (writePending) {
  519. writePending = false;
  520. this.writeSettings();
  521. }
  522. };
  523. return function () {
  524. if (writing) {
  525. writePending = true;
  526. return;
  527. }
  528. writing = true;
  529. var data = JSON.stringify(this.settings);
  530. fs.writeFile('settings.json.0', data, function () {
  531. // rename is atomic on POSIX, but will throw an error on Windows
  532. fs.rename('settings.json.0', 'settings.json', function (err) {
  533. if (err) {
  534. // This should only happen on Windows.
  535. fs.writeFile('settings.json', data, finishWriting);
  536. return;
  537. }
  538. finishWriting();
  539. });
  540. });
  541. };
  542. })(),
  543. uncacheTree: function (root) {
  544. var uncache = [require.resolve(root)];
  545. do {
  546. let newuncache = [];
  547. for (let i of uncache) {
  548. if (require.cache[i]) {
  549. newuncache.push.apply(newuncache,
  550. require.cache[i].children.map(function (module) {
  551. return module.filename;
  552. })
  553. );
  554. delete require.cache[i];
  555. }
  556. }
  557. uncache = newuncache;
  558. } while (uncache.length);
  559. },
  560. getDocMeta: function (id, callback) {
  561. https.get('https://www.googleapis.com/drive/v2/files/' + id + '?key=' + Config.googleapikey, function (res) {
  562. var data = '';
  563. res.on('data', function (part) {
  564. data += part;
  565. });
  566. res.on('end', function (end) {
  567. var json = JSON.parse(data);
  568. if (json) {
  569. callback(null, json);
  570. } else {
  571. callback('Invalid response', data);
  572. }
  573. });
  574. }).on('error', function (e) {
  575. callback('Error connecting to Google Docs: ' + e.message);
  576. });
  577. },
  578. getDocCsv: function (meta, callback) {
  579. https.get('https://docs.google.com/spreadsheet/pub?key=' + meta.id + '&output=csv', function (res) {
  580. var data = '';
  581. res.on('data', function (part) {
  582. data += part;
  583. });
  584. res.on('end', function (end) {
  585. callback(data);
  586. });
  587. }).on('error', function (e) {
  588. callback('Error connecting to Google Docs: ' + e.message);
  589. });
  590. }
  591. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement