· 6 years ago · Oct 17, 2019, 02:40 AM
1// ==UserScript==
2// @name Hotpocket.js
3// @version 2.4.1
4// @description Encode/Decode messages
5// @author (((You)))
6// @match https://play.pokemonshowdown.com/*
7// @match https://*.psim.us/*
8// @match http://*.psim.us/*
9// @grant unsafeWindow
10// @grant GM.getValue
11// @grant GM.setValue
12// @grant GM.info
13// @grant GM.xmlHttpRequest
14// @grant GM_getValue
15// @grant GM_setValue
16// @grant GM_info
17// @grant GM_xmlHttpRequest
18// @run-at document-idle
19// ==/UserScript==
20
21'use strict';
22
23// Edit this to change settings.
24var default_config = {
25 // Message markers for encryption and third-party chat.
26 dec_rank: "\u221e", // Infinity
27 byp_rank: "\u2709", // Envelope
28 dec_byp_rank: "\u2753", // Black Question Mark Ornament
29
30 // Delay used for posting. This is based on a server setting and as low
31 // as it can possibly be. Don't change this without a good reason.
32 spam_delay: 600,
33 // Delay used for long posts in /autodump. I need ten seconds to read a
34 // 300 character post, so with two more seconds everyone should be fine.
35 // Note that shorter lines have a shorter delay.
36 big_dump_delay: 12000,
37 // When non-null, use it as a fixed delay for /autodump and don't adjust
38 // based on line length. Can be temporarily set with /dumpdelay as well.
39 user_dump_delay: null,
40
41 // Website that the /bin command searches in. Must be plaintext.
42 meme_bin_url: "https://pastebin.com/raw/xYSfaMd1",
43
44 // Endpoint for the russiamode protocol.
45 third_party_server: "https://gmakers.xyz/showdown.php",
46 // Check for new posts every X milliseconds. Don't set this too low.
47 third_party_poll_interval: 5000,
48 // Automatically look for russiaposts or only once enabled via `/poll'?
49 poll_by_default: true,
50 // Stop polling for posts when it fails N times. 0 = never.
51 max_poll_fails: 5,
52 // Tell the russiaserver what version you are using?
53 send_script_version: true,
54
55 // Automatically load posted images inline or just show the link?
56 load_images: true,
57 // When loading images, should the images be hidden or shown by default?
58 show_images: true,
59 // Inline image CSS. max-width:100% = fill chat horizontally if too big.
60 image_style: "max-width:100%; max-height:400px",
61 // URIs (especially base64 ones) can be very long. Show how many chars?
62 max_link_len: 50,
63
64 // Convenience function that always sends these posts over PS.
65 unencrypted_posts: ["bg", "fuck off reen"],
66
67 // Display a big warning when a mod joins a battle?
68 mod_warning: true,
69
70 // Add a command reference/FAQ button to the main menu?
71 add_help_menu: true,
72
73 // These options overwrite parts of the PS client code and may break or
74 // block client updates, so they are optional.
75
76 // Allow massive (80KB+) posts for even longer dumping?
77 patch_char_limit: false,
78 // Provide a russia-only chatbox while not logged in?
79 patch_guest_chat: true,
80};
81
82// BASIC DESIGN
83//
84// We intercept all incoming/outgoing messages between client & server and then
85// do various manipulations and bookkeeping. Additionally, we add a few GUI
86// elements and (if so configured) overwrite parts of the GUI code. That's all.
87//
88// The original script was a mess of ad hoc parsers and had a bug where crypto
89// posts that expand to posts with newlines would add all later lines to the
90// server messages. This was a security issue but also the birth of extra chat
91// features. We parse chat messages into their own objects now and emulate the
92// bug for compatibility. A chat message object has the following fields:
93//
94// inject If this is true, all other fields except `text' are ignored and
95// instead the client receives `text' from the "server". Double check
96// all objects with this field. See `processChatMessage' for details.
97//
98// text Content of the message (unless `inject' is set). This string may
99// contain multiple lines, in that case the object stands for
100// multiple messages that share the remaining fields.
101//
102// rank A string of length one that signals the user rank (+, @, ...).
103// Rankless users have a space on PS. Note that any Unicode bullshit
104// beyond the BMP has length two in Javascript.
105//
106// author The author's display name.
107//
108// time Either a unix timestamp or false if the message isn't timestamped.
109//
110// crypto Boolean, signals whether `text' resulted from decryption.
111// bypass Boolean, signals whether this post came from russiaserver.
112// Because of a stupid Zarel unfeature, messages with these flags
113// need safety checks as well, see `processChatMessage'.
114//
115// EXTRA CHAT FEATURES
116//
117// Chat features are added by translating them to something the client already
118// understands so that we don't have to touch the client code. HTML posting is a
119// security issue and thus only available to users through well-defined custom
120// tags, which are interpreted by the script and never by the client. They are:
121//
122// |hjs-image|http://example.com/image.jpg
123// Embeds the image at the URL into the chat. Can be hidden depending
124// on script configuration.
125//
126// The so-called "set posting" is not a separate feature; it's what happens when
127// you inject plaintext without a | in the beginning. I've considered replacing
128// that with a separate command to finally nuke all injection bullshit but it
129// breaks the line-based nature of PS and isn't really that urgent.
130//
131// MISC MAINTAINER NOTES
132//
133// There's a version 3.0 and 3.1, which came somewhere between 2.1 and 2.2 due
134// to miscommunication. Explaining this is a pain in the dick, so use semantic
135// versioning and skip to 3.2 when you make a v3 for some reason.
136//
137// Greasemonkey is popular and stricter than Tampermonkey wrt sandboxing. This
138// is only relevant for hooking (which I already dealt with) and GUI stuff.
139// I found some Violentmonkey/GM3 compat code but can't test it:
140// https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
141//
142// A few fags use Palememe, which doesn't support some newer JS features. So far
143// the only consequence of that is that the toy crypto code doesn't use a CSPRNG
144// to generate the initialization vector. See crypto docs for details.
145//
146// We preserve the style of stolen code to make diffing easier.
147//
148// Kekerina (russia admin) can be reached under `coaxilthedrug@gmail.com`. Or at
149// least he claims so, I never used that address.
150//
151// REJECTED FEATURES
152//
153// - Audio/Video embeds (shit's annoying, just link it)
154// - Automatic jackteam reversing (complex, niche; use console + bruteforce)
155// - Encrypted PMs (just PM your partner a virtual room)
156// - More accurate timestamps (Russia and Zarel mostly agree on the time)
157// - User list in virtual rooms (makes no sense, VRs aren't rooms)
158// - GUI Configuration (code bloat, many already edit the script)
159// - Exponential poll timeout (not necessary yet, complicates code)
160//
161// Not exactly rejected, but sorting old russia messages into normal chat when
162// joining is a longstanding feature request. I have no idea how to do it in a
163// way that isn't "user says what turn he spoke" or "bot watches all games".
164//
165// FEATURE IDEAS THAT ARE COMING SOON (NEVER)
166//
167// - Autodumps should be retardproofed before anything is posted. The problem
168// with this is that we'd need to know the benign "please don't say" filters,
169// and there are a lot of those.
170// - Some sort of permanent display whether polling is on or off, people tend to
171// miss when polling goes down. Maybe the new poll failure message solves it.
172// - Automatically switching champ to the left was a request. Not sure how to do
173// this, maybe look for a player that's in two consecutive battles? Would be
174// tightly married to GUI code too, not sure if that's a great idea.
175// - Requested: Save and restore status on reload for shitposting. With a bit of
176// luck Zarel will implement that for us.
177// - Saving options other than the password between reloads. Not sure which.
178
179// Config variables used to be global variables and I don't want to fix that.
180Object.assign(window, default_config);
181
182// Related to GM sandboxing. We use app for ~everything, so just replace it.
183var app = unsafeWindow.app;
184
185var bHooked = false;
186var old_app_send, old_app_socket_onmessage;
187
188// The timeout causes the script to start after everything is loaded.
189setTimeout(function () {
190 loadPreferences().then(init);
191}, 0);
192
193function init() {
194 // If we hook twice, the "old" functions point to our own functions and
195 // we get an infinite loop.
196 if (bHooked) {
197 console.error("BUG: Tried to hook twice.");
198 return;
199 }
200
201 // Even though we run at document-idle, the client isn't always set up
202 // completely when we run, so we need to try this in a loop.
203 if (app && app.send && app.socket && app.socket.onmessage) {
204 old_app_send = app.send;
205 old_app_socket_onmessage = app.socket.onmessage;
206 exportFunction(app_send, app, {defineAs: "send"});
207 exportFunction(app_socket_onmessage, app.socket,
208 {defineAs: "onmessage"});
209 bHooked = true;
210
211 // Apply all GUI changes
212 if (add_help_menu)
213 addFunscriptMenu();
214 if (patch_char_limit)
215 patchPostLimit();
216 if (patch_guest_chat)
217 patchGuestChat();
218
219 if (poll_by_default)
220 startPolling();
221 } else {
222 // It seems to me like the long timeout could cause a desync
223 // with our roomInfo, but I have never witnessed this yet.
224 setTimeout(init, 1000);
225 }
226}
227
228function loadPreferences() {
229 return Promise.all([
230 GM.getValue('current_pw', default_password).then(setPassword)
231 ]);
232}
233
234function savePreferences() {
235 // Funscript v3.0 didn't JSONify the password so neither do we.
236 return Promise.all([
237 GM.setValue("current_pw", getPassword())
238 ]);
239}
240
241function app_send(data, room) {
242 let post = handleInput(data, room);
243 if (post !== undefined)
244 return old_app_send.call(this, post, room);
245}
246
247function app_socket_onmessage(msg) {
248 msg.data = handleServerMessage(msg.data);
249 return old_app_socket_onmessage.call(this, msg);
250}
251
252// `app.socket.onmessage' comes before `app.receive' so the latter bypasses the
253// script. Prevents infinite loops, but you want recursion sometimes.
254// TODO: This also bypasses logging to the console, so maybe imitate that here.
255function receiveWithProcessing(msg) {
256 let res = handleServerMessage(msg);
257 app.receive(res);
258}
259
260// roomInfo associates to each room ID an object with the following fields:
261//
262// bVirtual Is this room virtual or a real room on PS?
263// users Object that associates to each user ID a user info object.
264// bExpectDeinit If set, ignore the next `|deinit' message. This is used to
265// deal with expiring rooms.
266// russiaRoom Russiamode room that messages will be send to/read from.
267// May differ from the room name the client sees!
268// nextSince `since' parameter to use when looking for new russiaposts.
269//
270// User info objects have the following fields:
271//
272// displayName Name that is shown in chat.
273// rank +, @, &, etc. A space signals a rankless user.
274// id User ID. Beats converting every time, even if it's silly.
275//
276// This data is collected by app_socket_onmessage and used by other functions.
277// We don't reuse client functionality for this because it implicitly fixes many
278// old annoying bugs and avoids a second invite/roomvoice breakage.
279var roomInfo = {};
280
281// We do roomInfo[toRoomID(...)] pretty often otherwise.
282function getRoomInfo(room) {
283 return ("" + room === room && roomInfo[toRoomID(room)]) || null;
284}
285
286function registerRoom(room, bVirtual) {
287 roomInfo[room] = {
288 bVirtual: !!bVirtual,
289 users: {},
290 bExpectDeinit: false,
291 russiaRoom: room,
292 nextSince: 0
293 };
294}
295
296function unregisterRoom(room) {
297 clearRoomSchedule(room);
298 delete roomInfo[room];
299}
300
301function isVirtual(room) {
302 let ri = getRoomInfo(room);
303 return ri && ri.bVirtual;
304}
305
306function isRealRoom(room) {
307 let ri = getRoomInfo(room);
308 return ri && !ri.bVirtual;
309}
310
311function registerUser(rinfo, fullName) {
312 let u = fullName.match(/^(.)(.+)/);
313 if (u) {
314 let id = toUserID(u[2]);
315 rinfo.users[id] = {
316 displayName: u[2],
317 rank: u[1],
318 id: id
319 };
320 } else
321 console.error(`"${fullName}" is not a full user name.`);
322}
323
324function getUsers(room) {
325 let res = [];
326 let ri = getRoomInfo(room);
327 if (ri)
328 for (let u in ri.users)
329 res.push(ri.users[u]);
330 return res;
331}
332
333function findVirtualRoomID(name) {
334 let easyrid = toRoomID(name);
335 // The client completely shits itself if you `app.addRoom("",...)'.
336 if (getRoomInfo(easyrid) || easyrid === "") {
337 let rid;
338 for (let i = 0; i < 1000000; ++i) {
339 rid = `vr${i}-${easyrid}`;
340 if (!(rid in roomInfo))
341 return rid;
342 }
343 throw "JESUS CHRIST NIGGER CLOSE SOME ROOMS";
344 } else {
345 return easyrid;
346 }
347}
348
349// `room' is the room that's used client-side, `russiaRoom' is the one used
350// server-side. `makeFresh' decides whether to reuse an existing room.
351function makeVirtualRoom(room, russiaRoom, makeFresh) {
352 room = toRoomID(room);
353 let ri = getRoomInfo(room);
354
355 if (ri && !makeFresh) {
356 if (!ri.bVirtual)
357 complain(room, `
358This room is now virtual. Chat uses russiamode and commands may not work.`);
359 ri.bVirtual = true;
360 let oldRoom = ri.russiaRoom;
361 if (oldRoom !== russiaRoom) {
362 complain(room, `
363Changing russia room from "${oldRoom}" to "${russiaRoom}". Refetching...`);
364 ri.russiaRoom = russiaRoom;
365 ri.nextSince = 0;
366 }
367 return;
368 }
369
370 let fixedID = findVirtualRoomID(room);
371 // room ID, room type, don't send /join?, room title.
372 // I want to use the russia room as title but that fucks with autojoins.
373 app.addRoom(fixedID, "chat", true, fixedID);
374 receiveWithProcessing(`>${fixedID}
375|init|chat
376|users|0
377|:|${currentTime()}
378|error|This room is virtual. Chat uses russiamode and commands may not work.`);
379 roomInfo[fixedID].bVirtual = true;
380 roomInfo[fixedID].russiaRoom = russiaRoom;
381 if (fixedID !== russiaRoom) {
382 let x = dom("div", null, "class", "chat");
383 let y = dom("pre");
384 x.appendChild(y);
385 y.appendChild(dom("code", russiaRoom));
386 app.receive(`>${fixedID}
387|raw|The russia room you are talking on is not ${fixedID} but: ${x.outerHTML}`);
388 }
389}
390
391// This mostly parses the protocol, so check PROTOCOL.md in the server repo.
392// That said, it wouldn't be zarelcode if the documentation was complete and
393// correct. Parsing is as lenient as possible while still being pretty rigorous.
394// I wrote this against c2e63201c8a8f2687ba1010bf05246a5d4a3463b (2019-08-08).
395function handleServerMessage(msg) {
396 let lines = msg.split('\n');
397 let roomMatch = lines[0].match(/^>(.+)/);
398 // Global (and lobby) packets have no room. We only need room for lobby.
399 let room = roomMatch ? roomMatch[1] : "lobby";
400 let res = [];
401
402 for (let i = 0; i < lines.length; ++i) {
403 let L = lines[i];
404 // Previous lines can change room info, so recompute every time.
405 let ri = getRoomInfo(room);
406
407 let cm = parseChatMessage(L);
408 if (cm) {
409 // Newlines are handled by the join("\n") at the end.
410 let cms = processChatMessage(cm);
411 for (let j = 0; j < cms.length; ++j)
412 res.push(serializeMessage(cms[j]));
413 continue;
414 }
415
416 let init = L.match(/^\|init\|/);
417 let noinit = L.match(/^\|noinit(\|.*)?$/);
418 let deinit = L.match(/^\|deinit(\|.*)?$/);
419 let expire = L.match(/^\|expire(\|)?$/);
420 // May not match on zero users, but then we do nothing anyway.
421 let users = L.match(/^\|users\|([0-9]+),(.*)$/);
422 let join = L.match(/^\|[jJ](oin)?\|((.)([^@]+)(@.*)?)/);
423 let leave = L.match(/^\|[lL](eave)?\|(.+)/);
424 let rename = L.match(/^\|[nN](ame)?\|(.)([^@]+)(@.*)?\|(.+)$/);
425
426 if (init) {
427 registerRoom(room, false);
428 }
429
430 if (noinit) {
431 console.log(`Received noinit: ${msg}`);
432 makeVirtualRoom(room, room, false);
433 res.push(`|error|Couldn't join PS room: ${noinit[1]}`);
434 continue;
435 }
436
437 // Everything from here on requires a roomInfo entry.
438 if (!ri) {
439 res.push(L);
440 continue;
441 }
442
443 // `|deinit' is an undocumented message that causes the client
444 // to close rooms. We need to be a bit lenient about whether the
445 // room exists because room aliases (thp/thehappyplace) cause
446 // the client to get deinits for the alias (but no `|noinit').
447 if (deinit) {
448 if (ri.bExpectDeinit) {
449 ri.bExpectDeinit = false;
450 continue;
451 }
452 unregisterRoom(room);
453 }
454
455 // `|expire|' isn't in the official protocol either but obvious.
456 // Shortly after the expire we get another packet with deinit,
457 // which closes the room. Can this cause a race condition if you
458 // somehow manage to close the room before getting that packet?
459 if (expire) {
460 ri.bExpectDeinit = true;
461 makeVirtualRoom(room, room, false);
462 continue;
463 }
464
465 // Names and statuses are separated by an @, which is forbidden
466 // in names. The first character is the user rank, as usual.
467 // Names can't contain commas and `nostatus' should always match
468 // unless the protocol changed.
469 if (users) {
470 let list = users[2].split(",");
471 for (let j = 0; j < list.length; ++j) {
472 let nostatus = list[j].match(/(.[^@]+)(@.*)?$/);
473 if (!nostatus)
474 continue; // fail gracefully
475 registerUser(ri, nostatus[1]);
476 }
477 }
478
479 if (join) {
480 if (mod_warning && join[3].match(/[%@&~]/)
481 && room.match(/^battle-/)) {
482 res.push(`|bigerror|${join[2]} joined.`);
483 }
484 registerUser(ri, join[2]);
485 }
486
487 // The protocol says that leaves give you the user ID, but in
488 // battles, you get the ranked display name. The pajeets then
489 // paper over this by pointlessly converting to IDs everywhere.
490 // "But if I make it what it should be it crashes!!" Retards.
491 if (leave) {
492 delete ri.users[toUserID(leave[2])];
493 }
494
495 if (rename) {
496 // rename is correct now, but i bet they'll break it
497 let oldid = toUserID(rename[5]); // see above
498 let newid = toUserID(rename[3]);
499 let old = ri.users[oldid];
500
501 if (old) {
502 old.rank = rename[2]; // |n| can change the rank
503 old.id = newid;
504 // delete first in case oldid = newid (statuses)
505 delete ri.users[oldid];
506 ri.users[newid] = old;
507 } else {
508 console.error(`${""
509 }Got rename for nonexistent user ID: ${oldid}`);
510 }
511 }
512
513 res.push(L);
514 }
515
516 if (isShadowPost(msg))
517 res.push(`${""
518}|error|Post didn't go through; you tripped a filter or are shadowbanned.`);
519
520 return res.join("\n");
521}
522
523// Convert a single PS chat line to a message object; false on parse error.
524function parseChatMessage(line) {
525 if (line.includes("\n"))
526 return false;
527
528 let sections = line.split("|");
529 let result = {crypto: false, bypass: false};
530
531 if (sections[0] !== "" || sections.length < 2)
532 return false;
533
534 switch (sections[1]) {
535 case "c":
536 case "chat":
537 if (sections.length < 4 || sections[2].length < 1)
538 return false;
539 result.time = false;
540 result.rank = sections[2][0];
541 result.author = sections[2].slice(1);
542 result.text = sections.slice(3).join("|");
543 return result;
544 case "c:":
545 if (sections.length < 5 || sections[3].length < 1)
546 return false;
547 result.time = sections[2];
548 result.rank = sections[3][0];
549 result.author = sections[3].slice(1);
550 result.text = sections.slice(4).join("|");
551 return result;
552 default:
553 return false;
554 }
555}
556
557// Convert a message object to a string that can be fed to the client.
558// The result may have more than one line.
559function serializeMessage(m) {
560 let prefix = m.time === false ? "|c|" : `|c:|${m.time}|`;
561 let rank;
562
563 if (!m.inject) {
564 if (m.bypass && m.crypto)
565 rank = dec_byp_rank;
566 else if (m.bypass)
567 rank = byp_rank;
568 else if (m.crypto)
569 rank = dec_rank;
570 else
571 rank = m.rank;
572 }
573
574 // Normally, you can't send messages with a single leading slash because
575 // those are treated like commands. You therefore also usually can't
576 // receive them, which gave Zarel the genius idea to do ban logs and
577 // HTML posting this way. Since these restrictions obviously don't apply
578 // to encrypted and russia chat, we have to disarm such posts.
579 return m.text.split("\n").map(function (line) {
580 if (m.inject) {
581 return line;
582 } else {
583 if ((m.crypto || m.bypass)
584 && line.startsWith("/")
585 && !line.startsWith("//")
586 && !line.startsWith("/me ")
587 && line !== "/me") {
588 line = " " + line; // this kills the command
589 }
590
591 return `${prefix}${rank}${m.author}|${line}`;
592 }
593 }).join("\n");
594}
595
596// `scheduleInfo' is a queue of dumps. We regularly look at the first dump with
597// available posts, send one, and then push that entry to the end. Entry fields:
598//
599// room Argument to `app.send'.
600// next Function that consumes and returns the next post. Return values:
601// string The post to send.
602// false Signals that there may be posts later, but not now.
603// null Signals that there are no more posts.
604// May not call `postTick'. If you return false, then the function
605// preparing the new posts should call `startScheduler' because the
606// scheduler may have stopped in the meantime.
607//
608// The API has a few silly wrappers because it's imitating an early version.
609var scheduleInfo = [];
610var scheduler = null;
611
612function startScheduler() {
613 if (scheduler)
614 return;
615 scheduler = setInterval(postTick, spam_delay);
616 // Do one tick immediately to prevent 600ms delays for single posts.
617 setTimeout(postTick, 0);
618}
619
620function stopScheduler() {
621 clearInterval(scheduler);
622 scheduler = null;
623}
624
625function clearSchedule() {
626 scheduleInfo = [];
627}
628
629function clearRoomSchedule(room) {
630 scheduleInfo = scheduleInfo.filter(s => s.room !== room);
631}
632
633function postTick() {
634 for (let i = 0; i < scheduleInfo.length; ++i) {
635 let s = scheduleInfo[i];
636 let p = s.next();
637 switch (p) {
638 case null:
639 scheduleInfo.splice(i, 1);
640 i -= 1; // adjust index for the deleted entry
641 break;
642 case false:
643 break;
644 default:
645 old_app_send.call(app, p, s.room);
646 // Move to end to decrease priority
647 scheduleInfo.splice(i, 1);
648 scheduleInfo.push(s);
649 return;
650 }
651 }
652
653 stopScheduler(); // only called if there are no posts
654}
655
656function schedule(room, next) {
657 scheduleInfo.push({
658 room: room,
659 next: next
660 });
661 startScheduler();
662}
663
664function schedulePost(room, text) {
665 let bDone = false;
666 schedule(room, function () {
667 if (!bDone) {
668 bDone = true;
669 return text;
670 } else {
671 return null;
672 }
673 });
674}
675
676function scheduleLines(room, lines) {
677 // Imitate PS behavior for those posts that already used to work
678 if (lines.length <= 3) {
679 for (let i = 0; i < lines.length; ++i)
680 schedulePost(room, lines[i]);
681 } else {
682 let i = 0;
683 schedule(room, function () {
684 return i < lines.length ? lines[i++] : null;
685 });
686 }
687}
688
689// PS's protocol is line-based and goes to one server, which means you can just
690// split all lines and handle commands one after another. In fact, the client
691// goes even further and lets the server do the splitting. We are not line-based
692// due to injections and go to multiple servers due to Russia.
693//
694// This results in fun questions like "What if multiple parts of the same input
695// block go to different servers?" or "What if a command in the middle takes
696// multiple lines?" which are such a pain in the ass to answer that we simply do
697// not answer them at all. The old code only recognized new commands at the top
698// of the post due to lazy parsing and this is how we keep it. This part never
699// properly worked, so it's not like we mess with behavior people relied on.
700function handleInput(data, room) {
701 function err(msg) {
702 complain(room, msg);
703 }
704
705 // Sends data directly, i.e. `data' has a room prefix. Internal.
706 if (room === true)
707 return data;
708
709 // The PM box sends to "" and prefixes every line with "/pm userid, ".
710 // Seems internal otherwise. PM commands are /pm, /msg, /whisper, /w.
711 // PMs and rooms are distinct enough that supporting commands here would
712 // devolve into two implementations of handleInput. One is bad enough.
713 if (room === "") {
714 let lines = data.split("\n")
715 let result = [];
716
717 for (let i = 0; i < lines.length; ++i) {
718 let m = lines[i].match(/^\/pm ([^,]+), (.*)$/);
719 if (!m) {
720 result.push(lines[i]);
721 } else {
722 let wrap = wrapForDump(m[2], 300);
723 for (let j = 0; j < wrap.length; ++j)
724 result.push(`/pm ${m[1]}, ${wrap[j]}`);
725 }
726 }
727
728 scheduleLines("", result);
729 return;
730 }
731
732 // The GUI usually uses undefined as a room. We don't skip that because
733 // we need it for joins and leaves.
734
735 // Temporary options
736 let bypassThis = bBypassShowdown;
737 let encryptThis = bEncryptPosts;
738 let forceNoPS = false;
739
740 // Parameter parsing. param = all lines, firstparam = first line
741 let words = data.split(/ |\n/);
742 let lines = data.split("\n");
743 let param, firstparam;
744
745 if (words.length == 1) {
746 param = firstparam = null;
747 } else {
748 param = data.substr(words[0].length + 1); // +1 for the space
749 firstparam = lines[0].substr(words[0].length + 1);
750 }
751
752 // Matching on data and then words[0] is ugly but deals with on/off.
753 switch (data) {
754 case "/byc off":
755 case "/byp off": // Disabling russia but not crypto makes little sense.
756 case "/bly":
757 case "/ps":
758 bBypassShowdown = bEncryptPosts = false;
759 err("Third-party chat and encryption disabled.");
760 return;
761 case "/byc":
762 case "/byc on":
763 bBypassShowdown = bEncryptPosts = true;
764 err("Third-party chat and encryption enabled.");
765 return;
766 case "/byp":
767 case "/byp on":
768 bBypassShowdown = true;
769 err("Third-party chat enabled.");
770 return;
771 case "/dec":
772 case "/enc off":
773 bEncryptPosts = false;
774 err("Encryption disabled.");
775 return;
776 case "/enc":
777 case "/enc on":
778 bEncryptPosts = true;
779 err("Encryption enabled.");
780 return;
781 case "/poll":
782 case "/poll on":
783 startPolling();
784 err("Started polling for third-party posts.");
785 return;
786 case "/poll off":
787 case "/unpoll":
788 stopPolling();
789 err("Stopped polling for third-party posts.");
790 return;
791 case "/retired":
792 err(`Retardproofer is ${bRetardProofer?"enabled":"disabled"}.`);
793 return;
794 case "/retired on":
795 bRetardProofer = true;
796 err("Retardproofer enabled.");
797 return;
798 case "/retired off":
799 bRetardProofer = false;
800 err("Retardproofer disabled.");
801 return;
802 case "/dumpdelay off":
803 case "/dd off":
804 user_dump_delay = null;
805 err("Dump delay now depends on line length.");
806 return;
807 case "/db roominfo":
808 // Zarel already uses /debug and lets it bypass `app.send'.
809 console.log(roomInfo);
810 return;
811 }
812
813 switch(words[0]) {
814 case "/raw":
815 return param;
816 case "/autodump": {
817 let bAdjust = user_dump_delay === null;
818 let delay = !bAdjust ? user_dump_delay : big_dump_delay;
819 param = param ? param : "";
820 startDump(wrapForDump(param, 300), room, delay, bAdjust);
821 return;
822 }
823 case "/stopdump":
824 case "/sd":
825 if (firstparam === "all")
826 clearScheduler();
827 else
828 stopDump(room);
829 return;
830 case "/dumpdelay":
831 case "/dd": {
832 if (firstparam === null) {
833 err(`Current dump delay: ${user_dump_delay ?
834 user_dump_delay : "Depends on line length"}`);
835 return;
836 }
837
838 let delay = parseInt(firstparam);
839 if (!isNaN(delay)) {
840 user_dump_delay = delay;
841 err(`Dump delay is now ${delay}ms.`);
842 } else {
843 err(`Use an integer or "off".`);
844 }
845 return;
846 }
847 case "/roomvoice":
848 if (param !== "all")
849 break;
850 // fallthrough
851 case "/voiceall":
852 case "/vall":
853 roomvoiceAll(room);
854 return;
855 // TODO: These two always cause confusion, can they be clarified?
856 case "/inviteallto":
857 case "/inviteto":
858 case "/invto":
859 inviteAll(room, getRoomFromURL(param));
860 return;
861 case "/inviteallfrom":
862 case "/invitefrom":
863 case "/invfrom":
864 case "/invfr":
865 inviteAll(getRoomFromURL(param), room);
866 return;
867 case "/inviteall":
868 err(`Use /invfrom to invite people here.${""
869 }Use /invto to invite everyone here elsewhere.`);
870 return;
871 case "/byping":
872 bypassThis = true;
873 // fallthrough
874 case "/ping": {
875 let users = getUsers(room).map(u => u.id);
876 param = param ? param : "";
877 // 18 chars is the limit for a legit name, prevent spam.
878 pingUsers(users.filter(u => u.length < 19), param, room);
879 return;
880 }
881 case "/checkuser":
882 case "/checkusers": {
883 let names;
884 if (param === null) {
885 names = getUsers(room).filter(u => u.id.length < 19);
886 names.sort((a,b) => a.id > b.id ? 1 : -1);
887 names = names.map(u => u.displayName);
888 } else {
889 names = param.split(/\n|,/).map(x => x.trim()).
890 filter(x => x !== "");
891 }
892 err("Checking users...");
893 checkUsers(names, room);
894 return;
895 }
896 case "/bin":
897 if (firstparam === null) {
898 err("/bin <search term> - find matching mememons");
899 return;
900 }
901 searchParagraphInURL(meme_bin_url, firstparam, room);
902 return;
903 case "/byc":
904 bypassThis = encryptThis = true;
905 data = param;
906 break;
907 case "/bly":
908 case "/ps":
909 bypassThis = encryptThis = false;
910 data = param;
911 break;
912 case "/byp":
913 bypassThis = true;
914 data = param;
915 break;
916 case "/dec":
917 encryptThis = false;
918 data = param;
919 break;
920 case "/enc":
921 encryptThis = true;
922 data = param;
923 break;
924 case "/pass":
925 if (param === null) {
926 err(`Current password is: ${getPassword()}`);
927 return;
928 }
929
930 setPassword(param);
931 savePreferences();
932 err("Password changed. Refresh the page or rejoin this battle" +
933 " to see old messages encrypted with this password.");
934 return;
935 case "/anon":
936 case "/blankname":
937 firstparam = firstparam === "off" ? null : "";
938 // fallthrough
939 case "/fakename":
940 case "/rnick":
941 fakeName = firstparam;
942 switch (fakeName) {
943 case "": err("Anonposting enabled."); break;
944 case null: err("No longer using a fake name."); break;
945 default: err(`Set fake name to "${fakeName}".`); break;
946 }
947 return;
948 case "/withname": {
949 if (lines.length < 2) {
950 err(`
951Put your fake name on the first line and the message on the following lines.`);
952 return;
953 }
954 let oldname = fakeName;
955 let oldbyp = bBypassShowdown;
956 fakeName = firstparam;
957 bBypassShowdown = true;
958 app.send(lines.slice(1).join("\n"), room);
959 bBypassShowdown = oldbyp;
960 fakeName = oldname;
961 return;
962 }
963 case "/img":
964 lines[0] = firstparam;
965 // The link is usually pasted from a browser and thus escaped.
966 // Surrounding whitespace breaks the link, so trim. If it's not
967 // an obvious link, don't wrap so people can post text around
968 // images. It's a hack, but better than revamping commands.
969 lines = lines.map(function (l, idx) {
970 if (l.trim().match(/^http(s?):\/\//)) {
971 l = `|hjs-image|${l.trim()}`;
972 if (idx === 0) // injections can't be first
973 l = `\n${l}`;
974 }
975 return l;
976 });
977 data = lines.join("\n");
978 forceNoPS = true;
979 break;
980 case "/virtualjoin":
981 case "/vjoin":
982 case "/vj": {
983 if (lines.length > 2 || param === null) {
984 err(`
985Put the russia room you want to join on the same line as /vj. If you want to ${
986""}pretend to be in another room because the name is taken, put that on the ${""
987}second line.`);
988 return;
989 }
990 let russiaroom = firstparam;
991 let roomid = lines.length == 1 ? russiaroom : lines[1];
992
993 makeVirtualRoom(roomid, russiaroom, true);
994 return;
995 }
996 case "/leave":
997 case "/part":
998 if (firstparam === null)
999 firstparam = room;
1000 // `/leave' is never sent to undefined
1001 firstparam = toRoomID(firstparam);
1002
1003 // Help tickets and virtual rooms don't get a |deinit. The code
1004 // can deal with double deregs anyway, so this solution is fine.
1005 if (getRoomInfo(firstparam)) {
1006 unregisterRoom(firstparam);
1007 // Don't send for virtuals to hide our tracks
1008 if (isVirtual(firstparam))
1009 return;
1010 }
1011 // fallthrough
1012 case "/join":
1013 case "/joim": // This npm isntall tier thing really exists.
1014 case "/j":
1015 // In PS, joins/leaves are actually sent to rooms instead of the
1016 // server. If the room doesn't exist, you don't join/leave.
1017 if (isVirtual(room)) {
1018 // Technically does handleInput processing twice.
1019 app.send(data, "");
1020 return;
1021 }
1022 break;
1023 }
1024
1025 if (isCommand(data) || unencrypted_posts.includes(data.toLowerCase()))
1026 bypassThis = encryptThis = false;
1027
1028 if (forceNoPS && !encryptThis && !bypassThis)
1029 bypassThis = true;
1030
1031 if (isVirtual(room))
1032 bypassThis = true;
1033
1034 if (!isCommand(data) && shouldUseGuestChat())
1035 bypassThis = true;
1036
1037 // Because idiots keep fucking doing it
1038 if (data.includes("// ==UserScript==")) {
1039 err("Pok\u00e9mon Showdown! BETA doesn't preserve code.");
1040 return;
1041 }
1042
1043 if (encryptThis)
1044 data = encrypt(data);
1045 if (bypassThis) {
1046 let ri = getRoomInfo(room);
1047 if (ri && ri.russiaRoom !== room)
1048 room = ri.russiaRoom;
1049 sendRussiaPost(room, data);
1050 return;
1051 }
1052
1053 // Afaict there are no banfilters outside of battles. A global retard
1054 // proofer interferes with team validation (which is technically chat).
1055 if (bRetardProofer && !isCommand(data) && room.startsWith("battle-")) {
1056 let e = retardProofer(data);
1057 if (e !== null) {
1058 err(e);
1059 return;
1060 }
1061 }
1062
1063 // `/code' is the only PS command that allows more than three lines.
1064 // The decryption code can't deal with split posts so keep those intact.
1065 if (!encryptThis && !data.startsWith("/code ")) {
1066 let result = [];
1067 let lines = data.split("\n");
1068 for (let i = 0; i < lines.length; ++i) {
1069 if (!isCommand(lines[i])) {
1070 let wrap = wrapForDump(lines[i], 300);
1071 result = result.concat(wrap);
1072 } else
1073 result.push(lines[i]);
1074 }
1075 scheduleLines(room, result);
1076 return;
1077 }
1078
1079 return data;
1080}
1081
1082
1083
1084//==============================================================================
1085// Chat Features
1086//==============================================================================
1087
1088// Harmless but fun commands that will not be escaped. |c| is mostly obsoleted
1089// by /fakename, but is kept around to enable one trick:
1090//
1091// 1. Make a russiapost and inject a |c| message that pings a cuckchanneler
1092// pretending to not have russia. It will look like it was sent over PS.
1093// 2. Watch the cuckchanneler respond like a retard and expose itself.
1094//
1095// This used to be a configuration option, but there's really no point to that.
1096var injectable_commands = ["|error|","|bigerror|","|message|","|c|"];
1097
1098// A message may yield multiple messages (injection!), so this returns an array.
1099function processChatMessage(m) {
1100 try {
1101 m.text = decrypt(m.text);
1102 m.crypto = true;
1103 } catch(err) {
1104 // We check everything for crypto, so don't spam the log. Real
1105 // Chinese and crypto with a different key can trigger either
1106 // checksum or UTF-8 errors. See the crypto docs for details.
1107 if (err !== "not a chink message")
1108 console.error(`err: ${err}; ${m.text}`);
1109 }
1110
1111 // The text can have newlines (and thus injections) now because it may
1112 // have come from a cryptogram or russiaserver. Allowed injections are
1113 // kept, the rest is turned into setposting.
1114
1115 let wrap = function (t, inj) {
1116 // The original code copied timestamping state here. Useful?
1117 return Object.assign({}, m, {text: t, inject: inj});
1118 };
1119 let lines = m.text.split("\n");
1120 // We emulate a newline escape bug, so the first line is never injected.
1121 let res = [wrap(lines[0], false)];
1122
1123 for (let i = 1; i < lines.length; ++i) {
1124 if (lines[i].startsWith("|hjs-image|")) {
1125 res.push(wrap(makeImage(lines[i].substr(11)), true));
1126 } else {
1127 let inject = false;
1128
1129 for (let j = 0; j < injectable_commands.length; ++j)
1130 if (lines[i].startsWith(injectable_commands[j]))
1131 inject = true;
1132
1133 // Commands lose their effect if prefixed by a space,
1134 // but humans don't see that space thanks to HTML rules.
1135 res.push(wrap((inject ? "" : " ") + lines[i], true));
1136 }
1137 }
1138 return res;
1139}
1140
1141// DOM manip functions are verbose dogshit, so we use this wrapper.
1142function dom(type, text, attr, val) {
1143 let res = document.createElement(type);
1144 if (text)
1145 res.appendChild(document.createTextNode(text));
1146 if (attr)
1147 res.setAttribute(attr, val === undefined ? "" : val);
1148 return res;
1149}
1150
1151// Makes HTML that looks like the old box for the /code command.
1152// inside Child node array, the content of the box.
1153// optSummary Optional child node array. If provided, the box can be hidden
1154// and shows the argument as summary.
1155// bOpen If box can be hidden, determines initial state.
1156function makeBox(inside, optSummary, bOpen) {
1157 let res, det, summ, chat;
1158
1159 res = dom("div", null, "class", "infobox");
1160 chat = dom("div", null, "class", "chat"); // necessary for autoscroll?
1161
1162 if (optSummary) {
1163 det = dom("details", null, bOpen ? "open" : null, "");
1164 summ = dom("summary");
1165 for (let i = 0; i < optSummary.length; ++i)
1166 summ.appendChild(optSummary[i]);
1167 det.appendChild(summ);
1168 det.appendChild(chat);
1169 res.appendChild(det);
1170 } else {
1171 res.appendChild(chat);
1172 }
1173
1174 for (let i = 0; i < inside.length; ++i)
1175 chat.appendChild(inside[i]);
1176
1177 // `outerHTML' can in fact contain newlines.
1178 return `|html|${res.outerHTML.replace(/\n/g, "<br>")}`;
1179}
1180
1181function makeImage(src) {
1182 let len = Math.max(max_link_len, 3);
1183 let shortlink = src.length > len ? `${src.substr(0, len-3)}...` : src;
1184 let summary = [
1185 document.createTextNode("Image: "),
1186 dom("a", shortlink, "href", src)
1187 ];
1188
1189 if (load_images) {
1190 let img = dom("img", null, "src", src);
1191 img.setAttribute("style", image_style);
1192 return makeBox([img], summary, show_images);
1193 } else {
1194 return makeBox(summary);
1195 }
1196}
1197
1198
1199
1200//==============================================================================
1201// General PS functions
1202//==============================================================================
1203
1204var bRetardProofer = true;
1205
1206function complain(room, msg) {
1207 app.receive(`>${room}
1208${msg.split("\n").map(line => "|error|" + line).join("\n")}`);
1209}
1210
1211// PS does ~everything through chat commands, so we need to ignore these in
1212// certain functions. /me and !! are allowed because they are really chat.
1213function isCommand(msg) {
1214 return !msg.match(/^\/(me|\/)/i) && !!msg.match(/^((\/|!)[a-zA-Z0-9])/);
1215}
1216
1217// This isn't 100% reliable, but the shadowbanned will trigger it very often.
1218// Compare normal/filtered posts in console to see how it works, likely a bug.
1219function isShadowPost(data) {
1220 try {
1221 let lines = data.split("\n");
1222 let chatname = parseChatMessage(lines[1]).author;
1223 return lines.length == 2 && getUserName() == chatname;
1224 } catch(err) { // no point in checking, just catch
1225 return false;
1226 }
1227}
1228
1229// The ID functions are lifted from the client code. No clue if the difference
1230// in call order is due to Unicode bullshit or just general pajeetry.
1231function toUserID(name) {
1232 return ('' + name).toLowerCase().replace(/[^a-z0-9]+/g, '');
1233}
1234
1235function toRoomID(roomid) {
1236 return roomid.replace(/[^a-zA-Z0-9-]+/g, '').toLowerCase();
1237}
1238
1239function isUserIgnored(displayName) {
1240 return app.ignore && app.ignore[toUserID(displayName)];
1241}
1242
1243function getUserName() {
1244 if (app.user && app.user.attributes)
1245 return app.user.attributes.name;
1246 return null;
1247}
1248
1249// Current time in seconds, to imitate server timestamps
1250function currentTime() {
1251 return ~~(Date.now()/1000);
1252}
1253
1254function getRoomFromURL(url) {
1255 return url.replace(/^(https?:\/\/)?play.pokemonshowdown.com\//, "");
1256}
1257
1258function roomvoiceAll(room) {
1259 if (!isRealRoom(room)) {
1260 complain(room, ">giving voice in a virtual room");
1261 return;
1262 }
1263
1264 // A player with a rank either can't use voice or doesn't need it.
1265 let rankless = getUsers(room).filter(u => u.rank == " ");
1266 scheduleLines(room, rankless.map(u => `/roomvoice ${u.id}`));
1267}
1268
1269function inviteAll(from, to, currentRoom) {
1270 if(!isRealRoom(to) || !isRealRoom(from)) {
1271 complain(currentRoom, "Both rooms must be open and real.");
1272 return;
1273 }
1274
1275 // Last opp might have used modchat, so + must be allowed. Maybe this
1276 // should prompt/check for global auth to avoid inviting minimodlets.
1277 let fungamers = getUsers(from).filter(u => " +".includes(u.rank));
1278 scheduleLines(to, fungamers.map(u => `/invite ${u.id}`));
1279}
1280
1281function pingUsers(users, message, room) {
1282 if (users.length == 0)
1283 return;
1284
1285 let pingString = users[0];
1286 for (let i = 1; i < users.length; ++i) {
1287 pingString += ", ";
1288 pingString += users[i];
1289 }
1290 pingString += ": ";
1291 pingString += message;
1292
1293 // This automatically handles splitting/russia/...
1294 app.send(pingString, room);
1295}
1296
1297let checkuserStyle = [
1298 ["User", "Result", "Script is bugged", "#800080"],
1299
1300 ["perma", "Permabanned", "#A00000"],
1301 ["regged", "Registered", "#008000"],
1302 ["unregged", "Unregistered", "#008080"],
1303 ["disabled", "Disabled", "#808080"],
1304 ["???", "???", "#800080"],
1305 [null, "Error during check", "#800080"]
1306];
1307
1308function makeResultTable(style, names, results) {
1309 let table = dom("TABLE");
1310 let header = dom("TR");
1311 header.appendChild(dom("TD", style[0][0]));
1312 header.appendChild(dom("TD", style[0][1]));
1313 table.appendChild(header);
1314
1315 for (let i = 0; i < results.length; ++i) {
1316 let row = dom("TR");
1317 let text = style[0][2];
1318 let color = style[0][3];
1319 row.appendChild(dom("TD", names[i]));
1320 for (let j = 1; j < style.length; ++j)
1321 if (style[j][0] === results[i]) {
1322 text = style[j][1];
1323 color = style[j][2];
1324 break;
1325 }
1326 row.appendChild(dom("TD", text, "bgcolor", color));
1327 table.appendChild(row);
1328 }
1329
1330 return table;
1331}
1332
1333function checkUsers(names, room) {
1334 let res = new Array(names.length);
1335 let done = new Array(names.length).fill(false);
1336 let finish = function () {
1337 if (done.every(x => x)) {
1338 let table = makeResultTable(checkuserStyle, names, res);
1339 let titleText = `User check (${names.length})`;
1340 let title = document.createTextNode(titleText);
1341 let html = makeBox([table], [title], names.length < 20);
1342 app.receive(`>${room}\n${html}`);
1343 }
1344 };
1345
1346 for (let i = 0; i < names.length; ++i) {
1347 GM.xmlHttpRequest({
1348 // User ID chars are allowed in URLs, no issue here.
1349 url: "https://pokemonshowdown.com/users/" +
1350 toUserID(names[i]),
1351 method: "GET",
1352 onload: function (response) {
1353 let txt = response.responseText;
1354 if (txt.match(/\(Banned indefinitely\)/))
1355 res[i] = "perma";
1356 else if (txt.match(/\(Account disabled\)/))
1357 res[i] = "disabled";
1358 else if (txt.match(/Joined:/))
1359 res[i] = "regged";
1360 else if (txt.match(/\(Unregistered\)/))
1361 res[i] = "unregged";
1362 else
1363 res[i] = "???";
1364 done[i] = true;
1365 finish();
1366 },
1367 onerror: function (e) {
1368 console.error(`${JSON.stringify(e)}`);
1369 res[i] = null;
1370 done[i] = true;
1371 finish();
1372 }
1373 });
1374 }
1375}
1376
1377function searchParagraph(lines, search) {
1378 let result = [];
1379 let paragraph = [];
1380 let bFound = false;
1381
1382 function finishParagraph() {
1383 if (bFound)
1384 result.push(paragraph);
1385 bFound = false;
1386 paragraph = [];
1387 }
1388
1389 for (let i = 0; i < lines.length; ++i) {
1390 if (lines[i].trim() === "") {
1391 finishParagraph();
1392 continue;
1393 }
1394
1395 paragraph.push(lines[i]);
1396 if (lines[i].toLowerCase().includes(search.toLowerCase()))
1397 bFound = true;
1398 }
1399 finishParagraph();
1400
1401 return result;
1402}
1403
1404function searchParagraphInURL(url, search, room) {
1405 function onload (response) {
1406 let lines = response.responseText.split("\n");
1407 let match = searchParagraph(lines, search);
1408
1409 if (match.length == 0) {
1410 complain(room, `No results for "${search}".`);
1411 } else {
1412 let res = [];
1413 match = match.map(p => p.join("\n"));
1414
1415 res.push(dom("div", match[0]));
1416 for (let i = 1; i < match.length; ++i) {
1417 res.push(dom("hr"));
1418 res.push(dom("div", match[i]));
1419 }
1420
1421 let title = `Search (${match.length}): ${search}`;
1422 let summary = document.createTextNode(title);
1423 let box = makeBox(res, [summary], match.length < 10);
1424 app.receive(`>${room}\n${box}`);
1425 }
1426 }
1427
1428 GM.xmlHttpRequest({
1429 url: url,
1430 method: "GET",
1431 onload: onload,
1432 onerror: function (e) {
1433 complain(room, `Request error: ${JSON.stringify(e)}`);
1434 }
1435 });
1436}
1437
1438function startDump(lines, room, baseDelay, bAdjust) {
1439 let bNextReady = true;
1440 let i = 0;
1441
1442 function delayFor(line) {
1443 let delay = baseDelay;
1444 let interp = line.length / 300;
1445 if (bAdjust) {
1446 // Basic linear interpolation
1447 delay = Math.max(0, delay - spam_delay);
1448 delay = ~~(delay * interp);
1449 delay += spam_delay;
1450 }
1451 return Math.max(spam_delay, delay);
1452 }
1453
1454 schedule(room, function () {
1455 if (i >= lines.length)
1456 return null;
1457
1458 if (bNextReady) {
1459 setTimeout(function () {
1460 bNextReady = true;
1461 startScheduler();
1462 }, delayFor(lines[i]));
1463 bNextReady = false;
1464 return lines[i++];
1465 } else
1466 return false;
1467 });
1468}
1469
1470function stopDump(room) {
1471 let ri = getRoomInfo(room);
1472 if (ri && ri.dump) {
1473 clearTimeout(ri.dump);
1474 ri.dump = null;
1475 }
1476}
1477
1478function wrapForDump(text, len) {
1479 let result = [];
1480 let lines = text.split("\n");
1481 // Dumping commands is okay, but we must be careful not to accidentally
1482 // add spurious commands through word wrap.
1483 let removeCmd = function (l) {
1484 if (l[0] == "/" || l[0] == "!")
1485 return ` ${l}`;
1486 return l;
1487 };
1488
1489 for (let i = 0; i < lines.length; ++i) {
1490 // If the original line is a command, that's likely intentional,
1491 // so don't change anything. We don't check for overlength lines
1492 // because commands can't be split in general.
1493 if (isCommand(lines[i])) {
1494 result.push(lines[i]);
1495 continue;
1496 }
1497
1498 let words = lines[i].split(" "); // fuck Unicode
1499
1500 // Splitting at word boundaries doesn't work if a single word is
1501 // already too long, so we first split very long words up here.
1502 // We check for (len-1) so that removeCmd(word) is a valid line.
1503 let niceWords = [];
1504 for (let j = 0; j < words.length; ++j) {
1505 while (words[j].length > len - 1) {
1506 niceWords.push(words[j].substr(0, len - 1));
1507 words[j] = words[j].substr(len - 1);
1508 }
1509 niceWords.push(words[j]);
1510 }
1511 words = niceWords;
1512
1513 let curLine = words[0];
1514 for (let j = 1; j < words.length; ++j) {
1515 if (curLine.length + 1 + words[j].length > len) {
1516 result.push(curLine);
1517 curLine = removeCmd(words[j]);
1518 } else {
1519 curLine = curLine + " " + words[j];
1520 }
1521 }
1522 result.push(curLine);
1523 }
1524
1525 return result;
1526}
1527
1528
1529let retardFilters = [
1530 ["Posts containing %s autolock.", "nospace",
1531 [/paheal/, /ctrl[+-]?w/]],
1532 ["Posts containing %s alert mods.", "nospace",
1533 [/ctrl/, /control(\+|plus|and)/]],
1534 ["Posts starting with %s shadowban for the rest of the battle.", "text",
1535 [/^((you're|youre|ur) )?(pathetic|afraid|scared|chink)/]],
1536 ["Posts containing %s shadowban for the rest of the battle.", "text",
1537 [/\b(retard|bitch|pussy|kys|kill (yo)?urself|get cancer)\b/]],
1538 ["Posts containing %s globally shadowban.", "nospace",
1539 [/\xfdfd/]]
1540];
1541
1542// Returns an error message if text should be retard-proofed or null if it's ok.
1543// We don't have the filter list so these are only approximations. You can find
1544// the filter system in chat-plugins/chat-monitor.js and `canTalk' in chat.js.
1545function retardProofer(text) {
1546 text = text.replace(/^>+/, ""); // They really strip greentext.
1547 text = text.toLowerCase(); // Lowercase eases matching.
1548 text = text.trim(); // Trimming is done by the server.
1549
1550 let nospace = text.replace(/\s/g, "");
1551
1552 // Special case because unicode in regex is weird
1553 if (text.includes("\ufdfd"))
1554 return "Posts containing \ufdfd globally shadowban.";
1555
1556 for (let i = 0; i < retardFilters.length; ++i) {
1557 let f = retardFilters[i];
1558 let base;
1559 switch (f[1]) {
1560 case "text": base = text; break;
1561 case "nospace": base = nospace; break;
1562 default:
1563 console.error(`Unknown retard filter type: ${f[1]}`);
1564 return null;
1565 }
1566
1567 for (let j = 0; j < f[2].length; ++j) {
1568 let m = base.match(f[2][j]);
1569 if (m)
1570 return `${f[0].replace("%s", m[0])}
1571Use /raw to post it anyway or /retired off to disable this check permanently.`;
1572 }
1573 }
1574
1575 return null;
1576}
1577
1578
1579
1580//==============================================================================
1581// Third-Party Server
1582//==============================================================================
1583//
1584// The third-party protocol is basic, but has a few dark corners because it's
1585// not completely specified. In general, you send a JSON object via POST with
1586// the following fields:
1587//
1588// room Required non-empty string, specifies the room to post to.
1589// Maximal length is 255.
1590// text Optional. If set to non-empty string, posts this text as a
1591// comment. Maximal length is 32767.
1592// name Optional, specifies the name to post under. Treated as "" if
1593// not set. Maximal length is 255.
1594// since Optional number. If you don't want all posts from a room, set
1595// this to the ID of the first post you want to see. This is ass-
1596// backwards and should be the ID of the last seen post instead,
1597// but we can't change this now without breakage.
1598// version Optional string that works like a user agent string.
1599// Originally introduced to only allow the newest version to use
1600// the server, but this idea was (thankfully) scrapped.
1601//
1602// Not applicable fields as well as other extraneous fields are ignored. What
1603// happens when a field is, say, a boolean instead of a string is decided more
1604// by coincidence than protocol (lolphp), so just treat it like undef. behavior.
1605// The response is again a JSON object with the following fields:
1606//
1607// room The room this is about, should mirror the request.
1608// posts An array of post objects, which have the following fields:
1609// postID Sequential post ID to use for the since field.
1610// time Unix timestamp, used to emulate PS timestamps.
1611// name User name.
1612// text Message body.
1613// room Optional field that specifies what room the message is in.
1614// Only meaningful in replies for multi-room polls and ignored
1615// for compatibility otherwise.
1616//
1617// Right now the response object itself has the same fields as a post object,
1618// but this is only used in dead code for a version-related error message. There
1619// was a protocol in the works that allowed you to poll multiple rooms at once,
1620// but the server implementation is buggy (it only returns posts for the first
1621// room) and the protocol has the (minor) problem that you can only give a
1622// global since value, not one for every room.
1623//
1624// The server source can be found at: https://gmakers.xyz/showdown.php.txt
1625//
1626// The field lengths are not accurate in the presense of non-ASCII characters.
1627// I'm not exactly sure what the problem is, but but both MySQL and PHP truncate
1628// the string, so I suspect that there is some encoding mismatch. If a character
1629// is non-ASCII, it takes a (consistent, but really irregular) number of bytes;
1630// let's call this N.
1631//
1632// As long as the field is short enough or can be cut exactly at the limit
1633// without splitting a character, everything works normally, but if a Unicode
1634// character is split up, strange things happen:
1635// - If there's space for only one more ASCII char, the Unicode char is removed.
1636// - If there is space for 1 < k < N ASCII chars, the post is accepted, but the
1637// field becomes null. That's why the parser allows null fields.
1638//
1639// Here are a bunch of characters and their corresponding N. I have no idea what
1640// the pattern is. If you solve this problem, you can easily add automatic
1641// splitting to russia, which is useful in itself but was originally intended to
1642// be used for hosting the script on russia. Since the script is now ASCII-only
1643// this is no longer a blocker, but self-hosting fails for another reason:
1644// Everything turns into HTML, so the whitespace gets lost, breaking some string
1645// literals, and some of the template strings accidentally cause injections.
1646//
1647// charhex | N | name of character | why I checked this
1648// --------+---+-----------------------------+----------------------------------
1649// 25 | 1 | Percent | Thought it was related to URL
1650// 26 | 1 | Ampersand | encoding. Turns out it wasn't.
1651// 3a | 1 | Colon |
1652// --------+---+-----------------------------+----------------------------------
1653// e4 | 4 | Latin a with Diaeresis | Non-ASCII char.
1654// --------+---+-----------------------------+----------------------------------
1655// ff | 4 | Latin y with Diaeresis | Maybe it's encoding the codepoint
1656// 100 | 5 | Latin A with Macron | in hexadecimal?
1657// --------+---+-----------------------------+----------------------------------
1658// 270f | 6 | Pencil (#9999) | Thought it was encoding the CP
1659// 2710 | 6 | Upper Right Pencil (#10000) | in decimal, but apparently not.
1660// --------+---+-----------------------------+----------------------------------
1661// 3042 | 7 | Hiragana A | I predicted this would have N=6.
1662// --------+---+-----------------------------+----------------------------------
1663// 1f914 | 9 | Thinking Face | Non-BMP char.
1664//
1665
1666var bBypassShowdown = false; // bypass every message?
1667var fakeName = null; // current TP fake name, null if none
1668var pollFails = 0;
1669
1670var currentPoll = null;
1671let htmlPollButton =
1672 " <button class='button' name='send' value='/poll'>Start Polling</button>";
1673let msgPollError =
1674 "Third-party chat keeps failing, not polling for new posts."+htmlPollButton;
1675
1676// This is basically scheduling, so I wonder if we could hijack the scheduler.
1677function startPolling() {
1678 if (currentPoll)
1679 clearInterval(currentPoll);
1680 currentPoll = setInterval(function () {
1681 for (let r in roomInfo)
1682 pollRoom(r);
1683 }, third_party_poll_interval);
1684}
1685
1686function stopPolling() {
1687 clearInterval(currentPoll);
1688 currentPoll = null;
1689}
1690
1691function sendTP(fields, onload, onerror) {
1692 return GM.xmlHttpRequest({
1693 data: JSON.stringify(Object.assign(
1694 send_script_version ?
1695 {version: GM.info.script.version} : {},
1696 fields)),
1697 method: "POST",
1698 headers: {
1699 "Content-Type": "application/x-www-form-urlencoded"
1700 },
1701 url: third_party_server,
1702 onload: onload,
1703 onerror: onerror
1704 });
1705}
1706
1707function recvTP(res) {
1708 let parsed = false;
1709 let json;
1710 pollFails = 0;
1711 try {
1712 json = JSON.parse(res.responseText);
1713 parsed = true;
1714 if ("" + json.room !== json.room)
1715 throw `invalid/missing room field ${JSON.stringify(r)}`;
1716
1717 for (let r in roomInfo)
1718 if (roomInfo[r].russiaRoom === json.room)
1719 processTP(json, r);
1720 } catch(e) {
1721 console.error(`TP error: ${e}
1722${parsed ? JSON.stringify(json) : JSON.stringify(res.responseText)}`);
1723 }
1724}
1725
1726function pollRoom(room) {
1727 let ri = getRoomInfo(room);
1728 if (!ri) {
1729 console.error(`Tried to poll non-existent room: ${room}`);
1730 return;
1731 }
1732
1733 sendTP({
1734 room: ri.russiaRoom,
1735 since: ri.nextSince
1736 }, recvTP, function (e) {
1737 if (!currentPoll)
1738 return;
1739 if (max_poll_fails > 0 && ++pollFails >= max_poll_fails) {
1740 stopPolling();
1741 for (let r in roomInfo) {
1742 app.receive(`>${r}\n|raw|${msgPollError}`);
1743 }
1744 } else {
1745 console.error(`Poll error (${pollFails}/${max_poll_fails
1746 }) in room: ${room}`);
1747 }
1748 });
1749}
1750
1751function sendRussiaPost(room, msg) {
1752 sendTP({
1753 room: room,
1754 text: msg,
1755 name: fakeName === null ? getUserName() : fakeName
1756 }, recvTP, function (e) {
1757 app.receive(`>${room}
1758|error|Error during TP post: ${JSON.stringify(e)}`);
1759 });
1760}
1761
1762function processTP(json, r) {
1763 let messages = [];
1764 let posts = json.posts;
1765 let ri = getRoomInfo(r);
1766
1767 if (!Array.isArray(posts))
1768 throw `invalid/missing posts array: ${JSON.stringify(posts)}`;
1769
1770 for (let i = 0; i < posts.length; ++i) {
1771 let m;
1772
1773 try {
1774 m = parseRussiaPost(posts[i]);
1775 m.postID = posts[i].postID; // temporary for sorting
1776 if (!isUserIgnored(m.author))
1777 messages.push(m);
1778 } catch(err) {
1779 console.error(`Skipping bad TP post ${i} (${err}:
1780${JSON.stringify(posts[i])}`);
1781 }
1782 }
1783
1784 messages.sort(function (a, b) { return a.postID - b.postID; });
1785
1786 let payload = `>${r}`;
1787 let lastPost = ri.nextSince - 1;
1788
1789 for (let i = 0; i < messages.length; ++i) {
1790 let M = messages[i];
1791 let pid = M.postID;
1792 delete M.postID;
1793 lastPost = Math.max(lastPost, pid);
1794
1795 // We fake joins in VRs to keep a useful pseudo user list. Since
1796 // we skip script processing, users are manually registered.
1797 // Joins use |J| so they are hidden when possible.
1798 if (!getUsers(r).map(u => u.id).includes(toUserID(M.author))) {
1799 if (ri.bVirtual) {
1800 payload += `\n|J| ${M.author}`;
1801 registerUser(ri, ` ${M.author}`);
1802 }
1803 }
1804
1805 if (pid >= ri.nextSince) {
1806 let cms = processChatMessage(messages[i]);
1807 let txt = cms.map(serializeMessage).join("\n");
1808 payload += `\n${txt}`;
1809 }
1810 }
1811 ri.nextSince = lastPost + 1;
1812
1813 // Don't shit up the console if nothing happened.
1814 if (messages.length > 0) {
1815 console.log(`<<RU-JSON<<\n${JSON.stringify(json)}`);
1816 console.log(`<<RU<< ${payload}`);
1817 app.receive(payload);
1818 }
1819}
1820
1821function parseRussiaPost(tpm) {
1822 // String fields can be null due to a server bug, see above.
1823 let name = tpm.name === null ? "server bug lole" : tpm.name;
1824 let text = tpm.text === null ? "server bug lole" : tpm.text;
1825 // Some people use no name, but those make Zarel's client shit itself
1826 // and drop the rank. Space names work and are just as invisible.
1827 if (name === "")
1828 name = " ";
1829
1830 if (tpm.postID !== ~~tpm.postID)
1831 throw "postID is not an integer";
1832 if (tpm.time !== ~~tpm.time)
1833 throw "time is not an integer";
1834 if (name !== "" + name)
1835 throw "name is not a string";
1836 if (name.includes("\n"))
1837 throw "name contains newlines";
1838 if (name.includes("|"))
1839 throw "name contains pipes";
1840 if (text !== "" + text)
1841 throw "text is not a string";
1842 if (tpm.room !== undefined) {
1843 if (tpm.room !== "" + tpm.room)
1844 throw "room is not a string";
1845 if (tpm.room.includes("\n"))
1846 throw "room contains newlines";
1847 }
1848
1849 return {
1850 text: text,
1851 author: name,
1852 rank: byp_rank,
1853 time: tpm.time,
1854 crypto: false,
1855 bypass: true,
1856 inject: false
1857 };
1858}
1859
1860
1861
1862//==============================================================================
1863// GUI changes
1864//==============================================================================
1865
1866function getHelpHTML() {
1867 return `
1868<h1>Hotpocket.js v${GM.info.script.version}</h1>
1869The single most toxic userscript on Pokémon Showdown! BETA!
1870
1871<h2>Features</h2>
1872<ul>
1873<li>Hotpocket-proof <a href="https://gmakers.xyz">third-party</a> chat</li>
1874<li>Toy message encryption that gets you banned on sight</li>
1875<li>Automatic shadowban detection</li>
1876<li>Protection from common autolock phrases</li>
1877<li>Automatic shitflooding if your post is too long</li>
1878<li>QoL commands to mass voice, mass invite, check for regs/permabans, ...</li>
1879</ul>
1880
1881<h2>Command reference</h2>
1882
1883These commands are only recognized outside of PMs and at the start of the post.
1884No fancy stuff like multi line posts with multiple commands.
1885
1886<h3>General</h3>
1887<ul>
1888<li><code>/raw post</code> -
1889 Sends <code>post</code> normally, as if you had no script installed.
1890 Use this if the script blocked some behavior you liked.
1891<li><code>/retired on/off</code> -
1892 Toggles autolock protection.</li>
1893<li><code>/autodump long wall of text</code> -
1894 Dumps an extremely long post in the background with enough delays so that
1895 everyone can read it.</li>
1896<li><code>/dumpdelay delay</code>, <code>/dd delay</code> -
1897 Customizes the delay for <code>/autodump</code> posts.
1898 Takes an argument in milliseconds or "off" to use the default behavior.
1899<li><code>/roomvoice all</code>, <code>/voiceall</code>, <code>/vall</code> -
1900 Exactly what it says on the tin.</li>
1901<li><code>/inviteallto new-room</code>, <code>/inviteto new-room</code>,
1902 <code>/invto new-room</code> -
1903 Invite everyone in the current room to <code>new-room</code></li>
1904<li><code>/inviteallfrom old-room</code>, <code>/invitefrom old-room</code>,
1905 <code>/invfrom old-room</code>, <code>/invfr old-room</code> -
1906 Invite everyone from <code>old-room</code> to your current room</li>
1907<li><code>/ping message</code> -
1908 Send <code>message</code> while highlighting every user in the room.<[i>
1909<li><code>/checkuser</code>, <code>/checkusers</code> -
1910 Check all users in the room for permabans and registrations.
1911 Can also be given a comma/line-separated list of usernames to check.</li>
1912<li><code>/bin term</code> -
1913 Search the OU bin for <code>term</code>.
1914 Simple case-insensitive wordsearch, no fancy <code>/dt</code> matching.</li>
1915</ul>
1916
1917<h3>Mode switching</h3>
1918<p>
1919All of these commands can be used in the forms
1920<code>/cmd</code> and <code>/cmd post</code>.
1921The first switches posting modes permanently,
1922the second only makes that one post in the respective mode.
1923</p>
1924<ul>
1925<li><code>/byc</code> - Enable russiamode and encryption</li>
1926<li><code>/byp</code> - Enable russiamode</li>
1927<li><code>/bly</code>, <code>/ps</code> - Use unencrypted PS chat</li>
1928<li><code>/enc</code> - Enable encryption</li>
1929<li><code>/dec</code> - Disable encryption</li>
1930</ul>
1931
1932<h3>Russia-specific commands</h3>
1933<ul>
1934<li><code>/poll</code> -
1935 Regularly check your active rooms for new russiaposts.
1936 This is ${poll_by_default ? "enabled" : "disabled"} by default.</li>
1937<li><code>/unpoll</code> -
1938 Stop looking for new russiaposts.</li>
1939<li><code>/anon</code>, <code>/blankname</code> -
1940 Don't use a name when russiaposting.
1941 Use your PS name again by using <code>/anon off</code>, etc.</li>
1942<li><code>/fakename Zarel</code>, <code>/rnick Zarel</code> -
1943 Pretend to be Zarel when russiaposting.</li>
1944<li><code>/fakename</code>, <code>/rnick</code> -
1945 Use your PS name when russiaposting.</li>
1946<li><code>/withname name<br>post goes here</code> -
1947 Send a single russiapost under the name <code>name</code>,
1948 but don't permanently set it. The post is on the second+ lines</li>
1949<li><code>/img link</code> -
1950 Post an image in russiamode (works with crypto).
1951 You can add text as well, but image links must be on their own line.</li>
1952<li><code>/virtualjoin room</code>, <code>/vjoin room</code>,
1953 <code>/vj room</code> - <p>
1954 Join the russia room <code>room</code>
1955 without attempting to join the PS room of the same name.
1956 You can use this to avoid redirects like thp → thehappyplace and to not
1957 tell the server what russiarooms you are in.
1958 </p><b>Note:</b> Russia room names are sensitive to spaces and case,
1959 so make sure you write the correct name without any extra spaces, etc.
1960</li>
1961</ul>
1962
1963<h3>Encryption-specific commands</h3>
1964<ul>
1965<li><code>/pass</code> -
1966 Display current encryption password</li>
1967<li><code>/pass benis</code> -
1968 Set encryption password to <code>benis</code>.</li>
1969</ul>
1970
1971<h2>Symbol explanation</h2>
1972<p>
1973Chat messages that use encryption or the third-party server are marked with
1974a symbol where hotpockets usually have their good goy star.
1975<ul>
1976<li>${dec_rank} is a message that used encryption.</li>
1977<li>${byp_rank} is a third-party message.</li>
1978<li>${dec_byp_rank} is a third-party message that used encryption.</li>
1979</ul>
1980</p>
1981
1982<h1>Farting Autistic Queers</h1>
1983
1984<h2>Why is russiamode so laggy?</h2>
1985<p>
1986Russiamode is basically an anonymous imageboard. The auto update just dumps new
1987posts into your chat rooms rather than threads.
1988Since you only check for new messages every few seconds and use a completely
1989different server, there can be a few seconds of lag before you see a post.
1990</p>
1991
1992<h2>Why is somebody pretending to be me in russiamode?</h2>
1993<p>
1994See above. The russiaserver has no idea who you are on PS, by design.
1995</p>
1996
1997<h2>Will you remove /fakename?</h2>
1998<p>
1999No. Removing it from the script doesn't make impersonation impossible,
2000it just lures people into a false sense of security. It's also f*n.
2001</p>
2002
2003<h2>Why do I get a shitflood of old posts when I join late?</h2>
2004<p>
2005Because the third-party server is completely unrelated to PS, it has no idea
2006on what turn a post was made or what posts were made on PS in the meantime.
2007It only knows how the russiaposts were ordered, and at least this is preserved.
2008</p>
2009<p>
2010The timestamps can't be used for this because the PS client doesn't support
2011sorting new messages into old ones; it has no need for that since there is only
2012ever one server at a time. Additionally, battle chat isn't timestamped anyway
2013and the two servers' clocks aren't close enough to allow turn-precise timing.
2014</p>
2015
2016<h2>What data does this send to others?</h2>
2017<p>
2018The owner of the third-party server (Kekerina as of 2019-08-22)
2019can everything your browser leaks to sites (so IP, user agent, ...)
2020as well as the posts you make there and the rooms you check for posts.
2021Your name is only sent when you make a post.
2022I haven't seen him in months, he probably forgot he's even hosting this.
2023</p>
2024<p>
2025Another minor thing is that people can post images which you load by default,
2026so you automatically connect to the image host with the usual IP/UAS caveats.
2027If connecting to e621 etc is a problem, say because you are shitposting at work,
2028disable <code>load_images</code> in the config. You will only see links then.
2029</p>
2030
2031<h2>What do PS mods see?</h2>
2032<p>
2033If they pay very close attention to server logs,
2034they might see that you are joining nonexistent rooms like fullderp
2035or hahabenis and conclude that you are using funscript.
2036The existence of funscript isn't exactly a secret, so this does little for them.
2037Use <code>/vjoin</code> if it's a problem.
2038</p>
2039
2040<h2>Will the encryption protect me from glowniggers?</h2>
2041<p>
2042<font size="large" color="red">No.</font>
2043It has always been made for secretclubfagging and nothing else.
2044It's enough to keep cryptohobbyist hotpockets and cuckchannelers at bay,
2045but it's not a good example of how to do crypto.
2046</p>
2047
2048<h2>I have a very urgent business proposal. How do I contact you?</h2>
2049<p>
2050I'm online pretty often, so just ask during a game
2051or make a noticeable post (images, redtext) in the thread.
2052If you absolutely need to share your furry porn now,
2053I have an <a href="mailto:ledy@cock.li">email address</a> as well.
2054</p>
2055`;
2056}
2057
2058function patchClient(js) {
2059 let patch = document.createElement("script");
2060 patch.type = "text/javascript";
2061 patch.innerHTML = js;
2062 document.body.appendChild(patch);
2063}
2064
2065function addFunscriptMenu() {
2066 let helpElement = document.createElement("p");
2067 let helpButton = document.createElement("button");
2068
2069 helpElement.appendChild(helpButton);
2070 helpButton.appendChild(document.createTextNode("Hotpocket.js"));
2071 helpButton.setAttribute("class", "button");
2072
2073 // Check the RulesPopup in js/client.js to see what we are doing here.
2074 let popupDescription = cloneInto({}, unsafeWindow);
2075 exportFunction(function () {
2076 // this.el = the popup div. The style is from the rules.
2077 this.el.setAttribute("style", "max-width:760px");
2078 this.el.innerHTML = getHelpHTML();
2079 }, popupDescription, {defineAs: "initialize"});
2080 let HJSPopup = unsafeWindow.Popup.extend(popupDescription);
2081
2082 // We can't portably assign to onclick directly because of sandboxing.
2083 helpButton.addEventListener("click", exportFunction(function () {
2084 app.addPopup(HJSPopup);
2085 }, unsafeWindow));
2086
2087 // We add the Hotpocket.js button below the battle button.
2088 let m = document.getElementsByClassName("mainmenu")[0];
2089 m.children[0].appendChild(helpElement);
2090}
2091
2092function patchPostLimit() {
2093 // The original function is from js/client-chat.js.
2094 patchClient(`ChatRoom.prototype.submit = function (e) {
2095 e.preventDefault();
2096 e.stopPropagation();
2097 var text = this.$chatbox.val();
2098 if (!text) return;
2099 if (!$.trim(text)) {
2100 this.$chatbox.val('');
2101 return;
2102 }
2103 this.tabComplete.reset();
2104 this.chatHistory.push(text);
2105 text = this.parseCommand(text);
2106 if (this.battle && this.battle.ignoreSpects && app.user.get('userid') !== this.battle.p1.id && app.user.get('userid') !== this.battle.p2.id) {
2107 this.add("You can't chat in this battle as you're currently ignoring spectators");
2108// } else if (text.length > 80000) {
2109// app.addPopupMessage("Your message is too long.");
2110// return;
2111 } else if (text) {
2112 this.send(text);
2113 }
2114 this.$chatbox.val('');
2115 this.$chatbox.trigger('keyup'); // force a resize
2116};
2117`);
2118}
2119
2120// The "You have disconnected and cannot chat" thing is done via some event BS
2121// on `app' and seems difficult to patch out.
2122function patchGuestChat() {
2123 patchClient(`ConsoleRoom.prototype.updateUser = function () {
2124 var name = app.user.get('name');
2125 var userid = app.user.get('userid');
2126 if (this.expired) {
2127 this.$chatAdd.html(this.expired === true ? 'This room is expired' : BattleLog.sanitizeHTML(this.expired));
2128 this.$chatbox = null;
2129 } else if (!name) {
2130 this.$chatAdd.html('Connecting...');
2131 this.$chatbox = null;
2132// } else if (!app.user.get('named')) {
2133// this.$chatAdd.html('<form><button name="login">Join chat</button></form>');
2134// this.$chatbox = null;
2135 } else {
2136 this.$chatAdd.html('<form class="chatbox"><label style="' + BattleLog.hashColor(userid) + '">' + BattleLog.escapeHTML(name) + ':</label> <textarea class="textbox" type="text" size="70" autocomplete="off"></textarea></form>');
2137 this.$chatbox = this.$chatAdd.find('textarea');
2138 this.$chatbox.autoResize({
2139 animate: false,
2140 extraSpace: 0
2141 });
2142 if (this === app.curSideRoom || this === app.curRoom) {
2143 this.$chatbox.focus();
2144 }
2145 }
2146}`);
2147}
2148
2149function shouldUseGuestChat() {
2150 try {
2151 if (patch_guest_chat && !app.user.get("named"))
2152 return true;
2153 } catch (e) {
2154 // nothing
2155 }
2156 return false;
2157}
2158
2159
2160
2161//==============================================================================
2162// Encryption
2163//==============================================================================
2164
2165var bEncryptPosts = false;
2166
2167var getPassword = function () { return current_pw; };
2168var setPassword = function (pw) { current_pw = pw; };
2169var encrypt = function (x) { return x; };
2170var decrypt = function (y) { throw "not a chink message"; };
2171
2172// If you don't want the encryption code because you don't understand it and
2173// nobody uses encryption anymore, delete everything from here onwards. The /enc
2174// command will then just send the post normally and encrypted posts remain
2175// unreadable to you. The above code is needed to degrade cleanly.
2176
2177// The general idea is as follows. We are able to send up to 300 bytes of data
2178// over PS by replacing the bytes 0-255 with chinese characters; this is
2179// efficient because PS's chat limit relies on UCS-2 characters, not bytes.
2180//
2181// The actual packet looks like this:
2182//
2183// [1 byte] [4 bytes] [4*k bytes]
2184// checksum initvec payload
2185//
2186// The checksum is a simple sum over the other bytes so that real Chinese won't
2187// end up decrypted to nonsense. Arguably this was a missed opportunity; if I
2188// had chosen a secure hash over the plaintext or something, this would have
2189// given us a password check for free. Oh well.
2190//
2191// The payload is RC5-encrypted (CBC mode) and the initvec is its initialization
2192// vector. I chose CBC because it is easy to implement and doesn't suffer from
2193// the blatant problems of ECB. The parameters for RC5 are w=16, r=16 and b=32;
2194// I chose w=16 because this gives us a 32-bit blocksize that is convenient to
2195// implement in JS. The other parameters were chosen so that I could use public
2196// test vectors, though this ended up unnecessary since RC5 is easy to code.
2197// Obviously, a 32-bit blocksize is ridiculously low, but the point of this is
2198// to thwart crypto-inclined amateurs and hotpockets, not any luminescents. It's
2199// not a tutorial for good cryptographic code either. Read a book, nigger.
2200//
2201// These parameters leave us with a keysize of 256-bits, so salted SHA256 seemed
2202// like a good choice for a key derivation function. It also had a free online
2203// implementation I could steal.
2204//
2205// Finally, the text is encoded in UTF-8, but you probably already guessed that.
2206// The Chinese is taken from http://zein.se/patrick/3000char.html, in order.
2207
2208// Used to be a config option, but we load the password from settings now.
2209var default_password = "benis";
2210var current_pw = default_password;
2211var current_keytable;
2212
2213// Thanks, JS. Actually not perfectly random, but this doesn't matter much here.
2214function randomInt(max) {
2215 return Math.floor(Math.random() * Math.floor(max));
2216}
2217
2218encrypt = function (x) {
2219 let S = current_keytable;
2220 x = toUTF8(x);
2221 while (x.length % 4 != 0)
2222 x.push(32);
2223 x = bytesToBlocks(x);
2224
2225 // Palememe doesn't have the secure RNG, but it's not really important.
2226 let IV = randomInt(Math.pow(2, 32));
2227
2228 let y = CBC_enc(x => RC5_encrypt(x, S, RC5_rounds), x, [IV]);
2229 y = blocksToBytes([IV]).concat(blocksToBytes(y));
2230
2231 let checksum = 0;
2232 for (var i = 0; i < y.length; ++i)
2233 checksum = (checksum + y[i]) & 0xff;
2234
2235 return chinkify([checksum].concat(y));
2236};
2237
2238decrypt = function (y) {
2239 let S = current_keytable;
2240 y = dechinkify(y);
2241
2242 // Empty posts show up as failed checksums because JS is retarded and
2243 // doesn't care that we are addressing an empty array. It might be
2244 // useful to be able to dechinkify empty messages so we do this check
2245 // here and still get better error messages.
2246 if (y.length == 0)
2247 throw "not a chink message"; // same error as dechinkify
2248
2249 let checksum = y[0];
2250 y = y.slice(1);
2251
2252 for (var i = 0; i < y.length; ++i)
2253 checksum = (checksum - y[i]) & 0xff;
2254 if (checksum != 0)
2255 throw "invalid checksum";
2256
2257 y = bytesToBlocks(y);
2258 let x = CBC_dec(y => RC5_decrypt(y, S, RC5_rounds), y.slice(1), y[0]);
2259 return fromUTF8(blocksToBytes(x));
2260};
2261
2262getPassword = function () {
2263 return current_pw;
2264};
2265
2266setPassword = function (pw) {
2267 // This was originally supposed to use the SHA256 output as key, but
2268 // accidentally flips endianness because blocks are in LE and SHA output
2269 // is in BE. We can't change this now because that would change how
2270 // passwords are interpreted.
2271 let derivedKey = blocksToBytes(SHA256("REMOVE HOTPOCKET" + pw));
2272 current_pw = pw;
2273 current_keytable = RC5_expand(derivedKey, RC5_rounds);
2274};
2275
2276var chink_binary =
2277 "\u7684\u4e00\u662f\u4e0d\u4e86\u4eba\u6211\u5728\u6709\u4ed6\u8fd9\u4e2d" +
2278 "\u5927\u6765\u4e0a\u56fd\u4e2a\u5230\u8bf4\u4eec\u4e3a\u5b50\u548c\u4f60" +
2279 "\u5730\u51fa\u9053\u4e5f\u65f6\u5e74\u5f97\u5c31\u90a3\u8981\u4e0b\u4ee5" +
2280 "\u751f\u4f1a\u81ea\u7740\u53bb\u4e4b\u8fc7\u5bb6\u5b66\u5bf9\u53ef\u5979" +
2281 "\u91cc\u540e\u5c0f\u4e48\u5fc3\u591a\u5929\u800c\u80fd\u597d\u90fd\u7136" +
2282 "\u6ca1\u65e5\u4e8e\u8d77\u8fd8\u53d1\u6210\u4e8b\u53ea\u4f5c\u5f53\u60f3" +
2283 "\u770b\u6587\u65e0\u5f00\u624b\u5341\u7528\u4e3b\u884c\u65b9\u53c8\u5982" +
2284 "\u524d\u6240\u672c\u89c1\u7ecf\u5934\u9762\u516c\u540c\u4e09\u5df2\u8001" +
2285 "\u4ece\u52a8\u4e24\u957f\u77e5\u6c11\u6837\u73b0\u5206\u5c06\u5916\u4f46" +
2286 "\u8eab\u4e9b\u4e0e\u9ad8\u610f\u8fdb\u628a\u6cd5\u6b64\u5b9e\u56de\u4e8c" +
2287 "\u7406\u7f8e\u70b9\u6708\u660e\u5176\u79cd\u58f0\u5168\u5de5\u5df1\u8bdd" +
2288 "\u513f\u8005\u5411\u60c5\u90e8\u6b63\u540d\u5b9a\u5973\u95ee\u529b\u673a" +
2289 "\u7ed9\u7b49\u51e0\u5f88\u4e1a\u6700\u95f4\u65b0\u4ec0\u6253\u4fbf\u4f4d" +
2290 "\u56e0\u91cd\u88ab\u8d70\u7535\u56db\u7b2c\u95e8\u76f8\u6b21\u4e1c\u653f" +
2291 "\u6d77\u53e3\u4f7f\u6559\u897f\u518d\u5e73\u771f\u542c\u4e16\u6c14\u4fe1" +
2292 "\u5317\u5c11\u5173\u5e76\u5185\u52a0\u5316\u7531\u5374\u4ee3\u519b\u4ea7" +
2293 "\u5165\u5148\u5c71\u4e94\u592a\u6c34\u4e07\u5e02\u773c\u4f53\u522b\u5904" +
2294 "\u603b\u624d\u573a\u5e08\u4e66\u6bd4\u4f4f\u5458\u4e5d\u7b11\u6027\u901a" +
2295 "\u76ee\u534e\u62a5\u7acb\u9a6c\u547d\u5f20\u6d3b\u96be\u795e\u6570\u4ef6" +
2296 "\u5b89\u8868\u539f\u8f66\u767d\u5e94\u8def\u671f\u53eb\u6b7b\u5e38\u63d0" +
2297 "\u611f\u91d1\u4f55\u66f4\u53cd\u5408\u653e\u505a\u7cfb\u8ba1\u6216\u53f8" +
2298 "\u5229\u53d7\u5149\u738b";
2299
2300function chinkify(bytes) {
2301 let res = "";
2302 for (var i = 0; i < bytes.length; ++i) {
2303 if (bytes[i] >= 0 && bytes[i] < 256)
2304 res += chink_binary[bytes[i]];
2305 else
2306 throw "not an array of bytes";
2307 }
2308 return res;
2309}
2310
2311function dechinkify(chink) {
2312 let res = [];
2313 for (var i = 0; i < chink.length; ++i) {
2314 let idx = chink_binary.indexOf(chink[i]);
2315 if (idx >= 0)
2316 res.push(idx);
2317 else
2318 // If you change this error message, remember to change
2319 // the catch block in serializeMessage(), and decrypt().
2320 throw "not a chink message";
2321 }
2322
2323 return res;
2324}
2325
2326// We store things as 32-bit little-endian blocks.
2327function bytesToBlocks(x) {
2328 let res = [];
2329 if (x.length % 4 != 0)
2330 throw "bytes aren't padded to 32-bit blocks";
2331 for (let i = 0; i < x.length; i += 4)
2332 res.push(x[i] | (x[i+1] << 8) |
2333 (x[i+2] << 16) | (x[i+3] << 24));
2334 return res;
2335}
2336
2337function blocksToBytes(b) {
2338 let res = [];
2339 for (let i = 0; i < b.length; ++i)
2340 res.push(b[i] & 0xff, (b[i]>>8) & 0xff,
2341 (b[i]>>16) & 0xff, b[i]>>24 & 0xff);
2342 return res;
2343}
2344
2345// The functions for CBC mode operate on arrays of 32-bit blocks.
2346// Blocks are stored in little-endian.
2347function CBC_enc(enc, x, iv) {
2348 let res = [];
2349 for (let i = 0; i < x.length; ++i) {
2350 iv = enc(x[i] ^ iv);
2351 res.push(iv);
2352 }
2353 return res;
2354}
2355
2356function CBC_dec(dec, y, iv) {
2357 let res = [];
2358 for (let i = 0; i < y.length; ++i) {
2359 res.push(dec(y[i]) ^ iv);
2360 iv = y[i];
2361 }
2362 return res;
2363}
2364
2365function reduce(x) {
2366 return x & 0xFFFF;
2367}
2368
2369function rotl(x, n) {
2370 n &= 0xF;
2371 return reduce(x << n) | (reduce(x) >> (16 - n));
2372}
2373
2374function rotr(x, n) {
2375 n &= 0xF;
2376 return (reduce(x) >> n) | reduce(x << (16 - n));
2377}
2378
2379// Magic constants taken directly from the RC5 paper.
2380var RC5_P = 0xb7e1;
2381var RC5_Q = 0x9e37;
2382var RC5_rounds = 16; // Number of rounds.
2383
2384// `rotl' and `rotr' reduce their arguments so that for many
2385// additions modulo 2^16 we don't have to call `reduce'.
2386function RC5_encrypt(x, S, r) {
2387 let A = reduce(S[0] + x);
2388 let B = reduce(S[1] + (x >> 16));
2389
2390 for(let i = 1; i <= r; ++i) {
2391 A = reduce(rotl(A ^ B, B) + S[2*i ]);
2392 B = reduce(rotl(A ^ B, A) + S[2*i+1]);
2393 }
2394
2395 return A | (B << 16);
2396}
2397
2398function RC5_decrypt(y, S, r) {
2399 let A = reduce(y);
2400 let B = reduce(y >> 16);
2401
2402 for(let i = r; i >= 1; --i) {
2403 B = A ^ rotr(B - S[2*i+1], A);
2404 A = B ^ rotr(A - S[2*i ], B);
2405 }
2406 B = reduce(B - S[1]);
2407 A = reduce(A - S[0]);
2408
2409 return A | (B << 16);
2410}
2411
2412function RC5_expand(K, r) {
2413 let b = K.length;
2414 let c = Math.max(1, Math.ceil(b / 2));
2415 let t = 2 * (r + 1);
2416 let L = new Array(c);
2417 let S = new Array(t);
2418
2419 for (let i = 0; i < L.length; ++i)
2420 L[i] = 0;
2421 for (let i = 0; i < S.length; ++i)
2422 S[i] = 0;
2423
2424 // Taken from the original RC5 paper. Copies the bytes of K into words
2425 // as if we copied memory on a little endian machine.
2426 for (let i = b-1; i >= 0; --i)
2427 L[i>>1] = (L[i>>1] << 8) + (K[i] & 255);
2428
2429 S[0] = RC5_P;
2430 for (let i = 1; i < t; ++i)
2431 S[i] = S[i-1] + RC5_Q;
2432
2433 let i = 0, j = 0, A = 0, B = 0;
2434 for (let k = 0; k < 3 * Math.max(t, c); ++k) {
2435 A = S[i] = rotl(S[i] + A + B, 3);
2436 B = L[j] = rotl(L[j] + A + B, (A+B));
2437 i = (i + 1) % t;
2438 j = (j + 1) % c;
2439 }
2440
2441 return S;
2442}
2443
2444function toUTF8(str) {
2445 let result = [];
2446 let mask = (1 << 6) - 1;
2447 let cp = [...str].map(x => x.codePointAt(0));
2448
2449 for (let i = 0; i < cp.length; ++i) {
2450 if (cp[i] < 0x80)
2451 result.push(cp[i]);
2452 else if (cp[i] < 0x800)
2453 result.push(0xc0 | (cp[i] >> 6),
2454 0x80 | ( cp[i] & mask));
2455 else if (cp[i] < 0x10000)
2456 result.push(0xe0 | (cp[i] >> 12),
2457 0x80 | ((cp[i] >> 6) & mask),
2458 0x80 | ( cp[i] & mask));
2459 else
2460 result.push(0xf0 | (cp[i] >> 18),
2461 0x80 | ((cp[i] >> 12) & mask),
2462 0x80 | ((cp[i] >> 6) & mask),
2463 0x80 | ( cp[i] & mask));
2464 }
2465
2466 return result;
2467}
2468
2469function fromUTF8(str) {
2470 let isCont = (c => c >= 0x80 && c < 0xc0);
2471 let cps = [];
2472 let mask = (1 << 6) - 1;
2473 let need = function (n, i) {
2474 if (str.length <= i+n || !str.slice(i+1, i+n).every(isCont))
2475 throw `incomplete ${n}-byte sequence at ${i}`;
2476 }
2477
2478 for (let i = 0; i < str.length; ++i) {
2479 let c = str[i];
2480 let temp;
2481
2482 if (isCont(c))
2483 throw "stray continuation byte ${c} at ${i}";
2484
2485 if (c < 0x80)
2486 temp = c;
2487 else if (c < 0xe0) {
2488 need(1, i);
2489 temp = (c - 0xc0) << 6;
2490 temp |= str[++i] & mask;
2491 } else if (c < 0xf0) {
2492 need(2, i);
2493 temp = (c - 0xe0) << 12;
2494 temp |= (str[++i] & mask) << 6;
2495 temp |= str[++i] & mask;
2496 } else {
2497 need(3, i);
2498 temp = (c - 0xf0) << 18;
2499 temp |= (str[++i] & mask) << 12;
2500 temp |= (str[++i] & mask) << 6;
2501 temp |= str[++i] & mask;
2502 }
2503 cps.push(temp);
2504 }
2505
2506 return String.fromCodePoint(...cps);
2507}
2508
2509// This code is preserved in all its godawful formatting glory for easy diffing.
2510// We only remove the I/O, and convert the input to big endian because that's
2511// what the implementation uses. It also returns the result in big endian.
2512function SHA256(s){
2513 function safe_add (x, y) {
2514 var lsw = (x & 0xFFFF) + (y & 0xFFFF);
2515 var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
2516 return (msw << 16) | (lsw & 0xFFFF);
2517 }
2518
2519 function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
2520 function R (X, n) { return ( X >>> n ); }
2521 function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
2522 function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); }
2523 function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); }
2524 function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
2525 function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
2526 function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }
2527
2528 function core_sha256 (m, l) {
2529 var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
2530 var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
2531 var W = new Array(64);
2532 var a, b, c, d, e, f, g, h;
2533 var T1, T2;
2534
2535 m[l >> 5] |= 0x80 << (24 - l % 32);
2536 m[((l + 64 >> 9) << 4) + 15] = l;
2537
2538 for ( var i = 0; i<m.length; i+=16 ) {
2539 a = HASH[0];
2540 b = HASH[1];
2541 c = HASH[2];
2542 d = HASH[3];
2543 e = HASH[4];
2544 f = HASH[5];
2545 g = HASH[6];
2546 h = HASH[7];
2547
2548 for ( var j = 0; j<64; j++) {
2549 if (j < 16) W[j] = m[j + i];
2550 else W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);
2551
2552 T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
2553 T2 = safe_add(Sigma0256(a), Maj(a, b, c));
2554
2555 h = g;
2556 g = f;
2557 f = e;
2558 e = safe_add(d, T1);
2559 d = c;
2560 c = b;
2561 b = a;
2562 a = safe_add(T1, T2);
2563 }
2564
2565 HASH[0] = safe_add(a, HASH[0]);
2566 HASH[1] = safe_add(b, HASH[1]);
2567 HASH[2] = safe_add(c, HASH[2]);
2568 HASH[3] = safe_add(d, HASH[3]);
2569 HASH[4] = safe_add(e, HASH[4]);
2570 HASH[5] = safe_add(f, HASH[5]);
2571 HASH[6] = safe_add(g, HASH[6]);
2572 HASH[7] = safe_add(h, HASH[7]);
2573 }
2574 return HASH;
2575 }
2576
2577 let x = toUTF8(s);
2578 let blocks = [];
2579
2580 for (let i = 0; i < x.length; i += 4) {
2581 let v = 0;
2582 for (let j = 0; j < 4; ++j) {
2583 v <<= 8;
2584 if (i+j < x.length)
2585 v |= x[i+j];
2586 }
2587 blocks.push(v);
2588 }
2589
2590 return core_sha256(blocks, x.length * 8);
2591}