Guest User

Untitled

a guest
Jun 8th, 2015
240
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 31.48 KB | None | 0 0
  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. break;
  226. case 'l': case 'L':
  227. let username = spl[2];
  228. let user = room.onLeave(username);
  229. if (user) {
  230. if (user === Users.self) return false;
  231. this.updateSeen(user.id, spl[1], room.id);
  232. } else {
  233. this.updateSeen(toId(username), spl[1], room.id);
  234. }
  235. break;
  236. }
  237. },
  238. chatMessage: function (message, user, room) {
  239. var cmdrMessage = '["' + room.id + '|' + user.name + '|' + message + '"]';
  240. message = message.trim();
  241. if (message.substr(0, Config.commandcharacter.length) !== Config.commandcharacter) return false;
  242.  
  243. message = message.substr(Config.commandcharacter.length);
  244. var index = message.indexOf(' ');
  245. var arg = '';
  246. var cmd = message;
  247. if (index > -1) {
  248. cmd = cmd.substr(0, index);
  249. arg = message.substr(index + 1).trim();
  250. }
  251.  
  252. if (!!Commands[cmd]) {
  253. let failsafe = 0;
  254. while (typeof Commands[cmd] !== "function" && failsafe++ < 10) {
  255. cmd = Commands[cmd];
  256. }
  257. if (typeof Commands[cmd] === "function") {
  258. cmdr(cmdrMessage);
  259. Commands[cmd].call(this, arg, user, room);
  260. } else {
  261. error("invalid command type for " + cmd + ": " + (typeof Commands[cmd]));
  262. }
  263. }
  264. },
  265. say: function (target, text) {
  266. var targetId = target.id;
  267. if (Rooms.get(targetId)) {
  268. send((targetId !== 'lobby' ? targetId : '') + '|' + text);
  269. } else {
  270. send('|/pm ' + targetId + ', ' + text);
  271. }
  272. },
  273. isBlacklisted: function (userid, roomid) {
  274. var blacklistRegex = this.blacklistRegexes[roomid];
  275. return blacklistRegex && blacklistRegex.test(userid);
  276. },
  277. blacklistUser: function (userid, roomid) {
  278. var blacklist = this.settings.blacklist || (this.settings.blacklist = {});
  279. if (blacklist[roomid]) {
  280. if (blacklist[roomid][userid]) return false;
  281. } else {
  282. blacklist[roomid] = {};
  283. }
  284.  
  285. blacklist[roomid][userid] = 1;
  286. this.updateBlacklistRegex(roomid);
  287. return true;
  288. },
  289. unblacklistUser: function (userid, roomid) {
  290. var blacklist = this.settings.blacklist;
  291. if (!blacklist || !blacklist[roomid] || !blacklist[roomid][userid]) return false;
  292.  
  293. delete blacklist[roomid][userid];
  294. if (Object.isEmpty(blacklist[roomid])) {
  295. delete blacklist[roomid];
  296. delete this.blacklistRegexes[roomid];
  297. } else {
  298. this.updateBlacklistRegex(roomid);
  299. }
  300. return true;
  301. },
  302. updateBlacklistRegex: function (roomid) {
  303. var blacklist = this.settings.blacklist[roomid];
  304. var buffer = [];
  305. for (let entry in blacklist) {
  306. if (entry.startsWith('/') && entry.endsWith('/i')) {
  307. buffer.push(entry.slice(1, -2));
  308. } else {
  309. buffer.push('^' + entry + '$');
  310. }
  311. }
  312. this.blacklistRegexes[roomid] = new RegExp(buffer.join('|'), 'i');
  313. },
  314. uploadToHastebin: function (toUpload, callback) {
  315. if (typeof callback !== 'function') return false;
  316. var reqOpts = {
  317. hostname: 'hastebin.com',
  318. method: 'POST',
  319. path: '/documents'
  320. };
  321.  
  322. var req = http.request(reqOpts, function (res) {
  323. res.on('data', function (chunk) {
  324. // CloudFlare can go to hell for sending the body in a header request like this
  325. if (typeof chunk === 'string' && chunk.substr(0, 15) === '<!DOCTYPE html>') return callback('Error uploading to Hastebin.');
  326. var filename = JSON.parse(chunk.toString()).key;
  327. callback('http://hastebin.com/raw/' + filename);
  328. });
  329. });
  330. req.on('error', function (e) {
  331. callback('Error uploading to Hastebin: ' + e.message);
  332. });
  333.  
  334. req.write(toUpload);
  335. req.end();
  336. },
  337. processChatData: function (userid, roomid, msg) {
  338. // NOTE: this is still in early stages
  339. msg = msg.trim().replace(/[ \u0000\u200B-\u200F]+/g, ' '); // removes extra spaces and null characters so messages that should trigger stretching do so
  340. this.updateSeen(userid, 'c', roomid);
  341. var now = Date.now();
  342. if (!this.chatData[userid]) this.chatData[userid] = {
  343. zeroTol: 0,
  344. lastSeen: '',
  345. seenAt: now
  346. };
  347. var userData = this.chatData[userid];
  348. if (!userData[roomid]) userData[roomid] = {
  349. times: [],
  350. points: 0,
  351. lastAction: 0
  352. };
  353. var roomData = userData[roomid];
  354.  
  355. roomData.times.push(now);
  356.  
  357. // this deals with punishing rulebreakers, but note that the bot can't think, so it might make mistakes
  358. if (Config.allowmute && Users.self.hasRank(roomid, '%') && Config.whitelist.indexOf(userid) < 0) {
  359. let useDefault = !(this.settings.modding && this.settings.modding[roomid]);
  360. let pointVal = 0;
  361. let muteMessage = '';
  362. let modSettings = useDefault ? null : this.settings.modding[roomid];
  363.  
  364. // moderation for banned words
  365. if ((useDefault || !this.settings.banword[roomid]) && pointVal < 2) {
  366. let bannedPhraseSettings = this.settings.bannedphrases;
  367. let bannedPhrases = !!bannedPhraseSettings ? (Object.keys(bannedPhraseSettings[roomid] || {})).concat(Object.keys(bannedPhraseSettings.global || {})) : [];
  368. for (let bannedPhrase of bannedPhrases) {
  369. if (msg.toLowerCase().indexOf(bannedPhrase) > -1) {
  370. pointVal = 2;
  371. muteMessage = ', Automated response: your message contained a banned phrase';
  372. break;
  373. }
  374. }
  375. }
  376. // moderation for flooding (more than x lines in y seconds)
  377. let times = roomData.times;
  378. let timesLen = times.length;
  379. let isFlooding = (timesLen >= FLOOD_MESSAGE_NUM && (now - times[timesLen - FLOOD_MESSAGE_NUM]) < FLOOD_MESSAGE_TIME &&
  380. (now - times[timesLen - FLOOD_MESSAGE_NUM]) > (FLOOD_PER_MSG_MIN * FLOOD_MESSAGE_NUM));
  381. if ((useDefault || !('flooding' in modSettings)) && isFlooding) {
  382. if (pointVal < 2) {
  383. pointVal = 2;
  384. muteMessage = ', Automated response: flooding';
  385. }
  386. }
  387. // moderation for caps (over x% of the letters in a line of y characters are capital)
  388. let capsMatch = msg.replace(/[^A-Za-z]/g, '').match(/[A-Z]/g);
  389. if ((useDefault || !('caps' in modSettings)) && capsMatch && toId(msg).length > MIN_CAPS_LENGTH && (capsMatch.length >= ~~(toId(msg).length * MIN_CAPS_PROPORTION))) {
  390. if (pointVal < 1) {
  391. pointVal = 1;
  392. muteMessage = ', Automated response: caps';
  393. }
  394. }
  395. // moderation for stretching (over x consecutive characters in the message are the same)
  396. 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
  397. if ((useDefault || !('stretching' in modSettings)) && stretchMatch) {
  398. if (pointVal < 1) {
  399. pointVal = 1;
  400. muteMessage = ', Automated response: stretching';
  401. }
  402. }
  403.  
  404. if (pointVal > 0 && now - roomData.lastAction >= ACTION_COOLDOWN) {
  405. let cmd = 'mute';
  406. // defaults to the next punishment in Config.punishVals instead of repeating the same action (so a second warn-worthy
  407. // offence would result in a mute instead of a warn, and the third an hourmute, etc)
  408. if (roomData.points >= pointVal && pointVal < 4) {
  409. roomData.points++;
  410. cmd = Config.punishvals[roomData.points] || cmd;
  411. } else { // if the action hasn't been done before (is worth more points) it will be the one picked
  412. cmd = Config.punishvals[pointVal] || cmd;
  413. roomData.points = pointVal; // next action will be one level higher than this one (in most cases)
  414. }
  415. if (Config.privaterooms.indexOf(roomid) > -1 && cmd === 'warn') cmd = 'mute'; // can't warn in private rooms
  416. // if the bot has % and not @, it will default to hourmuting as its highest level of punishment instead of roombanning
  417. if (roomData.points >= 4 && !Users.self.hasRank(roomid, '@')) cmd = 'hourmute';
  418. if (userData.zeroTol > 4) { // if zero tolerance users break a rule they get an instant roomban or hourmute
  419. muteMessage = ', Automated response: zero tolerance user';
  420. cmd = Users.self.hasRank(roomid, '@') ? 'roomban' : 'hourmute';
  421. }
  422. if (roomData.points > 1) userData.zeroTol++; // getting muted or higher increases your zero tolerance level (warns do not)
  423. roomData.lastAction = now;
  424. this.say(Rooms.get(roomid), '/' + cmd + ' ' + userid + muteMessage);
  425. }
  426. }
  427. },
  428. cleanChatData: function () {
  429. var chatData = this.chatData;
  430. for (let user in chatData) {
  431. for (let room in chatData[user]) {
  432. let roomData = chatData[user][room];
  433. if (!Object.isObject(roomData)) continue;
  434.  
  435. if (!roomData.times || !roomData.times.length) {
  436. delete chatData[user][room];
  437. continue;
  438. }
  439. let newTimes = [];
  440. let now = Date.now();
  441. let times = roomData.times;
  442. for (let time of times) {
  443. if (now - time < 5 * 1000) newTimes.push(time);
  444. }
  445. newTimes.sort(function (a, b) {
  446. return a - b;
  447. });
  448. roomData.times = newTimes;
  449. if (roomData.points > 0 && roomData.points < 4) roomData.points--;
  450. }
  451. }
  452. },
  453.  
  454. updateSeen: function (user, type, detail) {
  455. if (type !== 'n' && Config.rooms.indexOf(detail) < 0 || Config.privaterooms.indexOf(detail) > -1) return;
  456. var now = Date.now();
  457. if (!this.chatData[user]) this.chatData[user] = {
  458. zeroTol: 0,
  459. lastSeen: '',
  460. seenAt: now
  461. };
  462. if (!detail) return;
  463. var userData = this.chatData[user];
  464. var msg = '';
  465. switch (type) {
  466. case 'j':
  467. case 'J':
  468. msg += 'joining ';
  469. break;
  470. case 'l':
  471. case 'L':
  472. msg += 'leaving ';
  473. break;
  474. case 'c':
  475. case 'c:':
  476. msg += 'chatting in ';
  477. break;
  478. case 'N':
  479. msg += 'changing nick to ';
  480. if (detail.charAt(0) !== ' ') detail = detail.substr(1);
  481. break;
  482. }
  483. msg += detail.trim() + '.';
  484. userData.lastSeen = msg;
  485. userData.seenAt = now;
  486. },
  487. getTimeAgo: function (time) {
  488. time = ~~((Date.now() - time) / 1000);
  489.  
  490. var seconds = time % 60;
  491. var times = [];
  492. if (seconds) times.push(seconds + (seconds === 1 ? ' second': ' seconds'));
  493. if (time >= 60) {
  494. time = ~~((time - seconds) / 60);
  495. let minutes = time % 60;
  496. if (minutes) times.unshift(minutes + (minutes === 1 ? ' minute' : ' minutes'));
  497. if (time >= 60) {
  498. time = ~~((time - minutes) / 60);
  499. let hours = time % 24;
  500. if (hours) times.unshift(hours + (hours === 1 ? ' hour' : ' hours'));
  501. if (time >= 24) {
  502. let days = ~~((time - hours) / 24);
  503. if (days) times.unshift(days + (days === 1 ? ' day' : ' days'));
  504. }
  505. }
  506. }
  507. if (!times.length) return '0 seconds';
  508. return times.join(', ');
  509. },
  510. writeSettings: (function () {
  511. var writing = false;
  512. var writePending = false; // whether or not a new write is pending
  513. var finishWriting = function () {
  514. writing = false;
  515. if (writePending) {
  516. writePending = false;
  517. this.writeSettings();
  518. }
  519. };
  520. return function () {
  521. if (writing) {
  522. writePending = true;
  523. return;
  524. }
  525. writing = true;
  526. var data = JSON.stringify(this.settings);
  527. fs.writeFile('settings.json.0', data, function () {
  528. // rename is atomic on POSIX, but will throw an error on Windows
  529. fs.rename('settings.json.0', 'settings.json', function (err) {
  530. if (err) {
  531. // This should only happen on Windows.
  532. fs.writeFile('settings.json', data, finishWriting);
  533. return;
  534. }
  535. finishWriting();
  536. });
  537. });
  538. };
  539. })(),
  540. uncacheTree: function (root) {
  541. var uncache = [require.resolve(root)];
  542. do {
  543. let newuncache = [];
  544. for (let i of uncache) {
  545. if (require.cache[i]) {
  546. newuncache.push.apply(newuncache,
  547. require.cache[i].children.map(function (module) {
  548. return module.filename;
  549. })
  550. );
  551. delete require.cache[i];
  552. }
  553. }
  554. uncache = newuncache;
  555. } while (uncache.length);
  556. },
  557. getDocMeta: function (id, callback) {
  558. https.get('https://www.googleapis.com/drive/v2/files/' + id + '?key=' + Config.googleapikey, function (res) {
  559. var data = '';
  560. res.on('data', function (part) {
  561. data += part;
  562. });
  563. res.on('end', function (end) {
  564. var json = JSON.parse(data);
  565. if (json) {
  566. callback(null, json);
  567. } else {
  568. callback('Invalid response', data);
  569. }
  570. });
  571. }).on('error', function (e) {
  572. callback('Error connecting to Google Docs: ' + e.message);
  573. });
  574. },
  575. getDocCsv: function (meta, callback) {
  576. https.get('https://docs.google.com/spreadsheet/pub?key=' + meta.id + '&output=csv', function (res) {
  577. var data = '';
  578. res.on('data', function (part) {
  579. data += part;
  580. });
  581. res.on('end', function (end) {
  582. callback(data);
  583. });
  584. }).on('error', function (e) {
  585. callback('Error connecting to Google Docs: ' + e.message);
  586. });
  587. }
  588. };
Advertisement
Add Comment
Please, Sign In to add comment