· 7 years ago · Dec 02, 2018, 09:04 PM
1@Zarel @cathyjf @panpawn @bumbadadabum @HoeenCoder @Joimer @bmelts @whalemer @TheImmortal @sirDonovan @MJB47 @Morfent @RisingDusk @asgdf @Lionyx @sparkychildcharlie @Marty-D @Slayer95 @AustinXII @verbiage @Hidden50 @ibojangles @patrickroberts @CheeseMuffin @scholzy @ZestOfLife
2
34219 lÃneas (3715 sloc) 164 KB
4/ **
5 * Comandos del sistema
6 * Pokemon Showdown - http://pokemonshowdown.com/
7 *
8 * Estos son comandos del sistema - comandos requeridos para Pokemon Showdown
9* correr. Muchos de estos son enviados por el cliente.
10 *
11* Los comandos del sistema no deben modificarse, agregarse o eliminarse. Si tu
12 * Me gusta modificar o agregar comandos, agregar o editar archivos en los complementos de chat /
13 *
14 * Para la API, consulte los complementos de chat / COMMANDS.md
15 *
16* @license licencia MIT
17 * /
18
19' uso estricto ' ;
20
21/ * eslint no-else-return: "error" * /
22
23const crypto = require ( ' crypto ' );
24const FS = require ( ' ./lib/fs ' );
25
26const MAX_REASON_LENGTH = 300 ;
27const MUTE_LENGTH = 7 * 60 * 1000 ;
28const HOURMUTE_LENGTH = 60 * 60 * 1000 ;
29
30const MAX_CHATROOM_ID_LENGTH = 225 ;
31
32/ ** Requerir razones * /
33const REQUIRE_REASONS = true ;
34
35/ ** @type {ChatCommands} * /
36 comandos const = {
37
38 ' ! version ' : verdadero ,
39 versión : función ( objetivo , sala , usuario ) {
40 if ( ! this . runBroadcast ()) return ;
41 esta . sendReplyBox ( ` Versión del servidor: <b> $ { Chat . package . version } </b> ` );
42 }
43
44 ' ! autoridad ' : verdadero ,
45 auth : ' autoridad ' ,
46 lista de personal : ' autoridad ' ,
47 globalauth : ' autoridad ' ,
48 authlist : ' autoridad ' ,
49 autoridad : función ( objetivo , sala , usuario , conexión ) {
50 if (target) {
51 dejar que targetRoom = habitaciones . búsqueda (objetivo);
52 deje availableRoom = targetRoom && targetRoom . checkModjoin (usuario);
53 if (targetRoom && availableRoom) devuelve esto . parse ( ` / roomauth1 $ { target } ` );
54 devolver este . parse ( ` / userauth $ { target } ` );
55 }
56 vamos a rankLists = {};
57 dejar rangos = objeto . llaves ( Config . grupos );
58 para ( dejar u en Usuarios . Grupos de Usuarios ) {
59 dejar rango = Usuarios . Grupos de Usuarios [u]. carácter ( 0 );
60 si (rango === ' ' || rango === ' + ' ) continúa ;
61 // En caso de que el archivo usergroups.csv no sea correcto, verificamos los rangos del servidor.
62 if ( rangos . incluye (rango)) {
63 dejar nombre = Usuarios . Grupos de Usuarios [u]. substr ( 1 );
64 if ( ! rankLists [rank]) rankLists [rank] = [];
65 if (nombre) rankLists [rango]. empujar (nombre);
66 }
67 }
68
69 let buffer = Object . teclas (rankLists). ordenar (( a , b ) =>
70 ( Config . Grupos [b] || {rango : 0 }). rango - ( Config . grupos [a] || {rango : 0 }). rango
71 ). mapa ( r =>
72 ` $ { ( Config . Grupos [r] ? ` ** $ { Config . Grupos . [R] Nombre } es ** ( $ { r } ) ` : r) } : \ n $ { rankLists [r]. sort (( a , b ) => toId (a). localeCompare ( toId (b))). unirse ( " , " ) } `
73 );
74
75 if ( ! buffer . length ) conexión de retorno . ventana emergente ( " Este servidor no tiene autoridad global " );
76 la conexión . ventana emergente ( buffer . join ( " \ n \ n " ));
77 }
78 authhelp : [
79 ` / auth - Mostrar personal global para el servidor. ` ,
80 ` / auth [sala]: muestra qué habitación tiene una sala. ` ,
81 ` / auth [usuario]: muestra lo que tiene global y roomauth que tiene un usuario. ` ,
82 ]
83
84 userlist: function (target, room, user) {
85 let userList = [];
86
87 for (let i in room.users) {
88 let curUser = Users(room.users[i]);
89 if (!curUser || !curUser.named) continue;
90 userList.push(Chat.escapeHTML(curUser.getIdentity(room.id)));
91 }
92
93 let output = `There ${Chat.plural(userList, "are", "is")} <strong style="color:#24678d">${Chat.count(userList, "</strong> users")} in this room:<br />`;
94 salida + = lista de usuarios . unirse ( ` , ` );
95
96 esta . sendReplyBox (salida);
97 }
98 userlisthelp : [ ` / userlist - Muestra una lista de los usuarios que están actualmente en la sala. ` ],
99
100 ' ! yo ' : cierto ,
101 mee : ' yo ' ,
102 yo : función ( objetivo , sala , usuario ) {
103 if ( este . cmd === ' mee ' && / [ AZ - a-z0-9 /] / . test ( target . charAt ( 0 ))) {
104 devolver este . errorReply ( ` / mee - no debe comenzar con una letra o número` );
105 }
106 objetivo = esto . canTalk ( ` / $ { this . cmd } $ { target || ' ' } ` );
107 if ( ! target) return ;
108
109 si ( este . mensaje . startsWith ( ` / ME ` )) {
110 const mayúsculaIdentidad = usuario . getIdentity (habitación). toUpperCase ();
111 si (habitación) {
112 esta . add ( ` | c | $ { uppercaseIdentity } | $ { target } ` );
113 } else {
114 let msg = ` | pm | $ { mayúsculaIdentidad } | $ { esto . pmTarget . getIdentity () } | $ { target } ` ;
115 de usuario . enviar (msg);
116 if ( this . pmTarget ! == usuario) esto . pmTarget . enviar (msg);
117 }
118 volver ;
119 }
120
121 objetivo de retorno
122 }
123
124 ' ! batalla ' : cierto ,
125 ' batalla! ' : ' batalla ' ,
126 batalla : función ( objetivo , sala , usuario , conexión , cmd ) {
127 si (cmd === ' batalla ' ) devuelve esto . sendReply ( "¡ ¿Qué? ¡¿Cómo no estás más emocionado de luchar ?! Intenta / combate! para mostrarme que estás listo " );
128 if ( ! target) target = " randombattle " ;
129 devolver este . parse ( ` / search $ { target } ` );
130 }
131
132 ' ! pi ' : verdadero ,
133 pi : función ( objetivo , sala , usuario ) {
134 devolver este . sendReplyBox (
135 ' Quiso decir: 1. 3.1415926535897932384626 ... (Decimal) <br /> ' +
136 ' 2. 3.184809493B91866 ... (Duodecimal) <br /> ' +
137 ' 3. 3.243F6A8885A308D ... (Hexadecimal) <br /> <br /> ' +
138 ' ¿Cuántos dÃgitos de pi sabe usted? ¡Pruébalo <a href="http://guangcongluo.com/mempi/"> aquà </a>! ' );
139 }
140
141 código : función ( objetivo , sala , usuario ) {
142 si ( ! target) devuelve esto . parse ( ' / código de ayuda ' );
143 if ( ! this . canTalk ()) return ;
144 if ( target . startsWith ( ' \ n ' )) target = target . rebanada ( 1 );
145 if ( target . length > = 8192 ) devuelve esto . errorReply ( "¡ Su código debe tener menos de 8192 caracteres! " );
146 separador de const = ' \ n ' ;
147 if ( target . includes (separator) || target . length > 150 ) {
148 const params = target . split (separador);
149 dejar salida = [];
150 para ( const param de params) {
151 de salida . empujar ( Chat . escapeHTML (param));
152 }
153 let code = `<div class="chat"><code style="white-space: pre-wrap; display: table">${output.join('<br />')}</code></div>`;
154 if (output.length > 3) code = `<details><summary>See code...</summary>${code}</details>`;
155
156 if (!this.canBroadcast(true, '!code')) return;
157 if (this.broadcastMessage && !this.can('broadcast', null, room)) return false;
158
159 if (!this.runBroadcast(true, '!code')) return;
160
161 this.sendReplyBox(code);
162 } else {
163 return this.errorReply("You can simply use ``[code]`` for code messages that are only one line.");
164 }
165 },
166 codehelp: [
167 `!code [code] - Broadcasts code to a room. Accepts multi-line arguments. Requires: + % @ & # ~`,
168 `/code [code] - Shows you code. Accepts multi-line arguments.`,
169 ],
170
171 '!avatar': true,
172 avatar: function (target, room, user) {
173 if (!target) return this.parse(`${this.cmdToken}avatars`);
174 let parts = target.split(',');
175 let avatarid = toId(parts[0]);
176 let avatar = 0;
177 let avatarTable = {
178 lucas: 1,
179 dawn: 2,
180 youngster: 3,
181 lass: 4,
182 camper: 5,
183 picnicker: 6,
184 bugcatcher: 7,
185 aromalady: 8,
186 twins: 9,
187 hiker: 10,
188 battlegirl: 11,
189 fisherman: 12,
190 cyclist: 13,
191 cyclistf: 14,
192 blackbelt: 15,
193 artist: 16,
194 pokemonbreeder: 17,
195 pokemonbreederf: 18,
196 cowgirl: 19,
197 jogger: 20,
198 pokefan: 21,
199 pokefanf: 22,
200 pokekid: 23,
201 youngcouple: 24,
202 acetrainer: 25,
203 acetrainerf: 26,
204 waitress: 27,
205 veteran: 28,
206 ninjaboy: 29,
207 dragontamer: 30,
208 birdkeeper: 31,
209 doubleteam: 32,
210 richboy: 33,
211 lady: 34,
212 gentleman: 35,
213 socialite: 36,
214 madame: 36,
215 beauty: 37,
216 collector: 38,
217 policeman: 39,
218 pokemonranger: 40,
219 pokemonrangerf: 41,
220 scientist: 42,
221 swimmer: 43,
222 swimmerf: 44,
223 tubérculo : 45 ,
224 tubérculo : 46 ,
225 marinero : 47 ,
226 sisandbro : 48 ,
227 ruinmaniac : 49 ,
228 psÃquico : 50 ,
229 psychicf : 51 ,
230 jugador : 52 ,
231 dppguitarista : 53 ,
232 acetrainersnow : 54 ,
233 acetrainersnowf : 55 ,
234 esquiador : 56 ,
235 esquiador : 57 ,
236 matón : 58 ,
237 payaso : 59 ,
238 trabajador : 60 ,
239 niño de la escuela : 61 ,
240 schoolkidf : 62 ,
241 Roark : 63 ,
242 barry : 64 ,
243 byron : 65 ,
244 Aaron : 66 ,
245 Bertha : 67 ,
246 pedernal : 68 ,
247 lucian : 69 ,
248 dppcynthia : 70 ,
249 bellepa : 71 ,
250 ganadero : 72 ,
251 mars : 73 ,
252 galacticgrunt : 74 ,
253 gardenia : 75 ,
254 crashwake : 76 ,
255 maylene : 77 ,
256 fantina : 78 ,
257 Candice : 79 ,
258 volkner : 80 ,
259 parasollady : 81 ,
260 camarero : 82 ,
261 Entrevistadores : 83 ,
262 camarógrafo : 84 ,
263 oli : 84 ,
264 reportero : 85 ,
265 roxy : 85 ,
266 Ãdolo : 86 ,
267 gracia : 86 ,
268 cyrus : 87 ,
269 Júpiter : 88 ,
270 Saturno : 89 ,
271 galacticgruntf : 90 ,
272 argenta : 91 ,
273 palmer : 92 ,
274 Thorton : 93 ,
275 dólar : 94 ,
276 darach : 95 ,
277 Marley : 96 ,
278 mira : 97 ,
279 cheryl : 98 ,
280 riley : 99 ,
281 dalia : 100 ,
282 ethan : 101 ,
283 lyra : 102 ,
284
285 arquero : 132 ,
286 ariana : 133 ,
287 protón : 134 ,
288 petrel : 135 ,
289 el hombre misterioso : 136 ,
290 eusine : 136 ,
291 ptlucas : 137 ,
292 ptdawn : 138 ,
293
294 Falkner : 141 ,
295 bugsy : 142 ,
296 Whitney : 143 ,
297 Morty : 144 ,
298 Chuck : 145 ,
299 jazmÃn : 146 ,
300 precio : 147 ,
301 clair : 148 ,
302 voluntad : 149 ,
303 koga : 150 ,
304 bruno : 151 ,
305 Karen : 152 ,
306 lanza : 153 ,
307 Brock : 154 ,
308 brumoso : 155 ,
309 Salidas : 156 ,
310 erika : 157 ,
311 Janine : 158 ,
312 Sabrina : 159 ,
313 Blaine : 160 ,
314 azul : 161 ,
315 red2 : 162 ,
316 rojo : 163 ,
317 plata : 164 ,
318 giovanni : 165 ,
319 desconocidof : 166 ,
320 unknownm : 167 ,
321 desconocido : 168 ,
322 hilbert : 169 ,
323 hilda : 170 ,
324
325 chile : 179 ,
326 Cilan : 180 ,
327 berro : 181 ,
328
329 lenora : 188 ,
330 Burgh : 189 ,
331 elesa : 190 ,
332 arcilla : 191 ,
333 skyla : 192 ,
334
335 Cheren : 206 ,
336 bianca : 207 ,
337
338 n : 209 ,
339
340 brycen : 222 ,
341 iris : 223 ,
342 drayden : 224 ,
343
344 shauntal : 246 ,
345 mariscal : 247 ,
346 Grimsley : 248 ,
347 caitlin : 249 ,
348 ghetsis : 250 ,
349
350 ingo : 256 ,
351 aliso : 257 ,
352
353 cynthia : 260 ,
354 emmet : 261 ,
355 Dueldiskhilbert : 262 ,
356 Dueldiskhilda : 263 ,
357 hugh : 264 ,
358 rosa : 265 ,
359 nate : 266 ,
360 Colress : 267 ,
361 bw2beauty : 268 ,
362 bw2ghetsis : 269 ,
363 bw2plasmagrunt : 270 ,
364 bw2plasmagruntf : 271 ,
365 bw2iris : 272 ,
366 brycenman : 273 ,
367 SombrÃo : 274 ,
368 sangre : 275 ,
369 zinzolin : 276 ,
370 bw2cheren : 277 ,
371 marlon : 278 ,
372 Roxie : 279 ,
373 Roxanne : 280 ,
374 peludo : 281 ,
375 wattson : 282 ,
376 flanneria : 283 ,
377 norman : 284 ,
378 winona : 285 ,
379 tate : 286 ,
380 liza : 287 ,
381 juan : 288 ,
382 guitarrista : 289 ,
383 Steven : 290 ,
384 Wallace : 291 ,
385 magicqueen : 292 ,
386 Bellelba : 292 ,
387 Benga : 293 ,
388
389 bw2elesa : ' # bw2elesa ' ,
390 teamrocket: '#teamrocket',
391 yellow: '#yellow',
392 zinnia: '#zinnia',
393 clemont: '#clemont',
394 wally: '#wally',
395 };
396 if (avatarTable.hasOwnProperty(avatarid)) {
397 avatar = avatarTable[avatarid];
398 } else {
399 avatar = parseInt(avatarid);
400 }
401 if (typeof avatar === 'number' && (!avatar || avatar > 294 || avatar < 1)) {
402 if (!parts[1]) {
403 this.errorReply("Invalid avatar.");
404 }
405 return false;
406 }
407
408 user.avatar = avatar;
409 if (!parts[1]) {
410 this.sendReply(`Avatar changed to:\n|raw|<img src="//play.pokemonshowdown.com/sprites/trainers/${(typeof avatar === 'string' ? avatar.substr(1) : avatar)}.png" alt="" width="80" height="80" />`);
411 }
412 },
413 avatarhelp: [`/avatar [avatar number 1 to 293] - Change your trainer sprite.`],
414
415 '!logout': true,
416 signout: 'logout',
417 logout: function (target, room, user) {
418 user.resetName();
419 },
420
421 r: 'reply',
422 reply: function (target, room, user) {
423 if (!target) return this.parse('/help reply');
424 if (!user.lastPM) {
425 return this.errorReply("No one has PMed you yet.");
426 }
427 return this.parse(`/msg ${user.lastPM || ''}, ${target}`);
428 },
429 replyhelp: [`/reply OR /r [message] - Send a private message to the last person you received a message from, or sent a message to.`],
430
431 '!msg': true,
432 pm: 'msg',
433 whisper: 'msg',
434 w: 'msg',
435 msg: function (target, room, user, connection) {
436 if (!target) return this.parse('/help msg');
437 target = this.splitTarget(target);
438 let targetUser = this.targetUser;
439 if (this.targetUsername === '~') {
440 this.room = Rooms.global;
441 this.pmTarget = null;
442 } else if (!target) {
443 this.errorReply("You forgot the comma.");
444 return this.parse('/help msg');
445 } else if (!targetUser) {
446 dejar error = ` Usuario $ { esto . targetUsername } no encontrado. ¿Has escrito mal su nombre? ` ;
447 error = ` | pm | $ { esto . de usuario . getIdentity () } | $ { esto . targetUsername } | / error $ { error } ` ;
448 la conexión . enviar (error);
449 volver ;
450 } else {
451 esta . pmTarget = targetUser;
452 esta . habitación = indefinido ;
453 }
454
455 if (targetUser && ! targetUser . conectado ) {
456 devolver este . errorReply ( ` usuario $ { esto . TargetUserName } no está en lÃnea. ' );
457 }
458
459 esta . parse (objetivo);
460 }
461 msghelp : [ ` / msg OR / whisper OR / w [nombre de usuario], [mensaje] - EnvÃa un mensaje privado. ` ],
462
463 ' ! invite ' : verdadero ,
464 inv : ' invitar ' ,
465 invite : function ( target , room , user ) {
466 si ( ! target) devuelve esto . parse ( ' / help invite ' );
467 if ( ! this . canTalk ()) return ;
468 if (room) target = this . splitTarget (target) || habitación . Identificación ;
469 dejar que targetRoom = habitaciones . búsqueda (objetivo);
470 if (targetRoom && ! targetRoom . checkModjoin (usuario)) {
471 targetRoom = undefined ;
472 }
473
474 si (habitación) {
475 if ( ! this . targetUser ) devuelve esto . errorReply ( ` Los usuario " $ { esto . TargetUserName } " no ha sido encontrado. ' );
476 si ( ! targetRoom) devuelve esto . errorReply ( ` La sala de " $ { objetivo } " no se encontró. ' );
477
478 devolver este . parse ( ` / pm $ { este . targetUsername } , / invite $ { targetRoom . id } ` );
479 }
480
481 deja targetUser = esto . pmTarget ;
482
483 if ( ! targetRoom || targetRoom === Rooms . global ) devuelve esto . errorReply ( ` La sala de " $ { objetivo } " no se encontró. ' );
484 if ( targetRoom . staffRoom && ! targetUser . isStaff ) devuelve esto . errorReply ( ` "usuario $ { targetUser . Nombre } " requiere autenticación global para unirse a la habitación " $ { targetRoom . ID } ". ' );
485 si ( ! targetUser) devuelve esto . errorReply ( ` Los usuario " $ { targetUser . name } " no ha sido encontrado. ' );
486
487 if ( ! targetRoom . checkModjoin (targetUser)) {
488 esta . room = targetRoom;
489 esta . parse ( ` / roomvoice $ { targetUser . name } ` );
490 if ( ! targetRoom . checkModjoin (targetUser)) {
491 devolver este . errorReply ( ` Usted no tiene permiso para invitar a la gente en esta sala. ' );
492 }
493 }
494 if (targetUser en targetRoom . users ) devuelve esto . errorReply ( ` Este usuario ya está en " $ { targetRoom . tÃtulo } ". ' );
495
496 devuelve ` / invite $ { targetRoom . id } ` ;
497 }
498 invitehelp : [
499 ` / invite [nombre de usuario] - Invita al jugador [nombre de usuario] a unirse a la sala a la que envió el comando. ` ,
500 ` (en un PM) / invite [roomname]: invita al jugador al que estás PMing a unirse a la sala [roomname]. ` ,
501 ]
502
503 pminfobox : función ( destino , sala , usuario , conexión ) {
504 if ( ! this . canTalk ()) return ;
505 if ( ! this . can ( ' addhtml ' , null , room)) devuelve false ;
506 si ( ! target) devuelve esto . parse ( " / help pminfobox " );
507
508 objetivo = esto . canHTML ( this . splitTarget (target));
509 if ( ! target) return ;
510 deja targetUser = esto . targetUser ;
511
512 si ( ! targetUser || ! targetUser . conectado ) devuelve esto . errorReply ( ` usuario $ { esto . TargetUserName } no está en lÃnea. ' );
513 if ( ! (targetUser in room . users ) && ! user . can ( ' addhtml ' )) devuelve esto . errorReply ( " No tiene permiso para usar este comando para usuarios que no están en esta sala. " );
514 if ( targetUser . ignorePMs && targetUser . ignorePMs ! == user . group && ! user . can ( ' lock ' )) devuelve esto . errorReply ( " Este usuario actualmente ignora los PMs. " );
515 if ( targetUser . locked && ! user . can ( ' lock ' )) devuelve esto . errorReply ( " Este usuario está bloqueado actualmente, por lo que no puede enviarles un pminfobox. " );
516
517 // Aplicar el infobox al mensaje
518 target = ` / raw <div class =" infobox "> $ { target } </div> ` ;
519 dejar mensaje = ` | pm | $ { usuario . getIdentity () } | $ { targetUser . getIdentity () } | $ { target } ` ;
520
521 de usuario . enviar (mensaje);
522 if (targetUser ! == usuario) targetUser . enviar (mensaje);
523 targetUser . lastPM = usuario . ID de usuario ;
524 de usuario . lastPM = targetUser . ID de usuario ;
525 }
526 pminfoboxhelp : [ ` / pminfobox [user], [html] - PMs e [html] infobox para [user]. Requiere * ~ ` ],
527
528 pmuhtmlchange : ' pmuhtml ' ,
529 pmuhtml : function ( target , room , user , connection , cmd ) {
530 if ( ! this . canTalk ()) return ;
531 if ( ! this . can ( ' addhtml ' , null , room)) devuelve false ;
532 si ( ! target) devuelve esto . parse ( " / help " + cmd);
533
534 objetivo = esto . canHTML ( this . splitTarget (target));
535 if ( ! target) return ;
536 deja targetUser = esto . targetUser ;
537
538 si ( ! targetUser || ! targetUser . conectado ) devuelve esto . errorReply ( ` usuario $ { esto . TargetUserName } no está en lÃnea. ' );
539 if ( ! (targetUser in room . users ) && ! user . can ( ' addhtml ' )) devuelve esto . errorReply ( " No tiene permiso para usar este comando para usuarios que no están en esta sala. " );
540 if ( targetUser . ignorePMs && targetUser . ignorePMs ! == user . group && ! user . can ( ' lock ' )) devuelve esto . errorReply ( " Este usuario actualmente ignora los PMs. " );
541 if ( targetUser . locked && ! user . can ( ' lock ' )) devuelve esto . errorReply ( " Este usuario está bloqueado actualmente, por lo que no puede enviarlos UHTML. " );
542
543 dejar mensaje = ` | pm | $ { usuario . getIdentity () } | $ { targetUser . getIdentity () } | / uhtml $ { (cmd === ' pmuhtmlchange ' ? ' change ' : ' ' ) } $ { target } ` ;
544
545 de usuario . enviar (mensaje);
546 if (targetUser ! == usuario) targetUser . enviar (mensaje);
547 targetUser . lastPM = usuario . ID de usuario ;
548 de usuario . lastPM = targetUser . ID de usuario ;
549 }
550 pmuhtmlhelp : [ ` / pmuhtml [usuario], [nombre], [html] - PMs [html] que puede cambiar a [usuario]. Requiere * ~ ` ],
551 pmuhtmlchangehelp : [ ` / pmuhtmlchange [usuario], [nombre], [html] - Cambia el código html que se envió previamente a [usuario] a [html]. Requiere * ~ ` ],
552
553 ' ! ignorepms ' : verdadero ,
554 blockpm : ' ignorepms ' ,
555 blockpms : ' ignorepms ' ,
556 ignorepm : ' ignorepms ' ,
557 ignorepms : function ( target , room , user ) {
558 if ( usuario . ignorePMs === (target || true )) devuelve esto . errorReply ( "¡ Ya está bloqueando mensajes privados! Para desbloquear, use / unblockpms " );
559 de usuario . ignorePMs = true ;
560 if (target in Config.groups) {
561 user.ignorePMs = target;
562 return this.sendReply(`You are now blocking private messages, except from staff and ${target}.`);
563 }
564 return this.sendReply("You are now blocking private messages, except from staff.");
565 },
566 ignorepmshelp: [`/blockpms - Blocks private messages. Unblock them with /unignorepms.`],
567
568 '!unignorepms': true,
569 unblockpm: 'unignorepms',
570 unblockpms: 'unignorepms',
571 unignorepm: 'unignorepms',
572 unignorepms: function (target, room, user) {
573 if (!user.ignorePMs) return this.errorReply("You are not blocking private messages! To block, use /blockpms");
574 de usuario . ignorePMs = falso ;
575 devolver este . sendReply ( " Ya no estás bloqueando mensajes privados " );
576 }
577 unignorepmshelp : [ ` / unblockpms - Desbloquea mensajes privados. Bloquealas con / blockpms. ` ],
578
579 ' ! away ' : cierto ,
580 inactivo : ' lejos ' ,
581 afk : ' lejos ' ,
582 distancia : función ( objetivo , sala , usuario ) {
583 esta . parse ( ' / blockchallenges ' );
584 esta . parse ( ` / blockpms $ { target } ` );
585 }
586 awayhelp : [ ` / away - Bloquea desafÃos y mensajes privados. Desbloquearlos con / back. ` ],
587
588 ' ! atrás ' : cierto ,
589 Unaway : ' volver ' ,
590 unafk : ' espalda ' ,
591 back : function () {
592 esta . parse ( ' / unblockpms ' );
593 esta . parse ( ' / unblockchallenges ' );
594 }
595 backhelp : [ ` / back - Desbloquea desafÃos y / o mensajes privados, si alguno de los dos está bloqueado. ` ],
596
597 ' ! rank ' : verdadero ,
598 rango : función ( objetivo , sala , usuario ) {
599 if ( ! target) target = usuario . nombre ;
600
601 Las escaleras . visualizeAll (target). entonces ( valores => {
602 let buffer = ` <div class =" ladder "> <table> ` ;
603 buffer + = ` <tr> <td colspan =" 8 "> Usuario: <strong> $ { Chat . escapeHTML (target) } </strong></td> </tr> ` ;
604
605 dejar calificaciones = valores . unirse ( ` ` );
606 si ( ! calificaciones) {
607 buffer + = ` <tr> <td colspan =" 8 "> <em> Este usuario todavÃa no ha jugado ningún juego de escalera. </em> </td> </tr> ` ;
608 } else {
609 buffer + = ` <tr><th>Format</th> <th> <abbr title =" Elo rating "> Elo </abbr> </th> <th> W </th> <th> L </ th> <th> Total </th> ` ;
610 buffer + = calificaciones;
611 }
612 buffer + = ` </table> </div> ` ;
613
614 esta . sendReply ( ` | raw | $ { buffer } ` );
615 });
616 }
617
618 makeprivatechatroom : ' makechatroom ' ,
619 makechatroom : función ( destino , sala , usuario , conexión , cmd ) {
620 if ( ! this . can ( ' makeroom ' )) devolver ;
621
622 // `,` es un delimitador utilizado por muchos / comandos
623 // `|` y `[` son delimitadores utilizados por el protocolo
624 // `-` tiene un significado especial en los roomids
625 if ( target . includes ( ' , ' ) || target . includes ( ' | ' ) || target . includes ( ' [ ' ) || target . includes ( ' - ' )) {
626 devolver este . errorReply ( "Los tÃtulos de las salas no pueden contener ninguno de:, | [- " );
627 }
628
629 let id = toId (target);
630 si ( ! id) devuelve esto . parse ( ' / help makechatroom ' );
631 if ( id . length > MAX_CHATROOM_ID_LENGTH ) devuelve esto . errorReply ( " El tÃtulo de la habitación es demasiado largo " );
632 // Comprueba si el nombre ya existe como sala o alias
633 if (Rooms.search(id)) return this.errorReply(`The room '${target}' already exists.`);
634 if (!Rooms.global.addChatRoom(target)) return this.errorReply(`An error occurred while trying to create the room '${target}'.`);
635
636 if (cmd === 'makeprivatechatroom') {
637 let targetRoom = Rooms.search(target);
638 targetRoom.isPrivate = true;
639 targetRoom.chatRoomData.isPrivate = true;
640 Rooms.global.writeChatRoomData();
641 if (Rooms.get('upperstaff')) {
642 Rooms.get('upperstaff').add(`|raw|<div class="broadcast-green">Private chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
643 }
644 this.sendReply(`The private chat room '${target}' was created.`);
645 } else {
646 if (Rooms.get('staff')) {
647 Rooms.get('staff').add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
648 }
649 if (Rooms.get('upperstaff')) {
650 Rooms.get('upperstaff').add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Chat.escapeHTML(target)}</b></div>`).update();
651 }
652 this.sendReply(`The chat room '${target}' was created.`);
653 }
654 },
655 makechatroomhelp: [`/makechatroom [roomname] - Creates a new room named [roomname]. Requires: & ~`],
656
657 subroomgroupchat: 'makegroupchat',
658 makegroupchat: function (target, room, user, connection, cmd) {
659 if (!this.canTalk()) return;
660 if (!user.autoconfirmed) {
661 return this.errorReply("You must be autoconfirmed to make a groupchat.");
662 }
663 if (cmd === 'subroomgroupchat') {
664 if (!user.can('mute', null, room)) return this.errorReply("You can only create subroom groupchats for rooms you're staff in.");
665 if (room.battle) return this.errorReply("You cannot create a subroom of a battle.");
666 if (room.isPersonal) return this.errorReply("You cannot create a subroom of a groupchat.");
667 }
668 let parent = cmd === 'subroomgroupchat' ? room.id : null;
669 // if (!this.can('makegroupchat')) return false;
670
671 // Title defaults to a random 8-digit number.
672 let title = target.trim();
673 if (title.length >= 32) {
674 return this.errorReply("Title must be under 32 characters long.");
675 } else if (!title) {
676 title = (`${Math.floor(Math.random() * 100000000)}`);
677 } else if (Config.chatfilter) {
678 let filterResult = Config.chatfilter.call(this, title, user, null, connection);
679 if (!filterResult) return;
680 if (title !== filterResult) {
681 return this.errorReply("Invalid title.");
682 }
683 }
684 // `,` is a delimiter used by a lot of /commands
685 // `|` and `[` are delimiters used by the protocol
686 // `-` has special meaning in roomids
687 if (title.includes(',') || title.includes('|') || title.includes('[') || title.includes('-')) {
688 return this.errorReply("Room titles can't contain any of: ,|[-");
689 }
690
691 // Even though they're different namespaces, to cut down on confusion, you
692 // can't share names with registered chatrooms.
693 let existingRoom = Rooms.search(toId(title));
694 if (existingRoom && !existingRoom.modjoin) return this.errorReply(`The room '${title}' already exists.`);
695 // Room IDs for groupchats are groupchat-TITLEID
696 let titleid = toId(title);
697 if (!titleid) {
698 titleid = `${Math.floor(Math.random() * 100000000)}`;
699 }
700 let roomid = `groupchat-${parent || user.userid}-${titleid}`;
701 // Titles must be unique.
702 if (Rooms.search(roomid)) return this.errorReply(`A group chat named '${title}' already exists.`);
703 // Tab title is prefixed with '[G]' to distinguish groupchats from
704 // registered chatrooms
705
706 if (Monitor.countGroupChat(connection.ip)) {
707 this.errorReply("Due to high load, you are limited to creating 4 group chats every hour.");
708 return;
709 }
710
711 let titleMsg = Chat.html `Welcome to ${parent ? room.title : user.name}'s${!/^[0-9]+$/.test(title) ? ` ${title}` : ''}${parent ? ' subroom' : ''} groupchat!`;
712 let targetRoom = Rooms.createChatRoom(roomid, `[G] ${title}`, {
713 isPersonal: true,
714 isPrivate: 'hidden',
715 modjoin: parent ? null : '+',
716 parentid: parent,
717 auth: {},
718 introMessage: `<div style="text-align: center"><table style="margin:auto;"><tr><td><img src="//play.pokemonshowdown.com/fx/groupchat.png" width=120 height=100></td><td><h2>${titleMsg}</h2><p>Follow the <a href="/rules">Pokémon Showdown Global Rules</a>!<br>Don't be disruptive to the rest of the site.</p></td></tr></table></div>`,
719 staffMessage: `<p>You can invite new users using <code>/invite</code>. Be careful with who you invite!</p><p>Commands: <button class="button" name="send" value="/roomhelp">Room Management</button> | <button class="button" name="send" value="/tournaments help">Tournaments</button></p><p>As creator of this groupchat, <u>you are entirely responsible for what occurs in this chatroom</u>. Global rules apply at all times.</p><p>If this room is used to break global rules or disrupt other areas of the server, <strong>you as the creator will be held accountable and punished</strong>.</p>`,
720 });
721 if (targetRoom) {
722 // The creator is a Room Owner in subroom groupchats and a Host otherwise..
723 targetRoom.auth[user.userid] = parent ? '#' : '\u2605';
724 // Join after creating room. No other response is given.
725 user.joinRoom(targetRoom.id);
726 user.popup(`You've just made a groupchat; it is now your responsibility, regardless of whether or not you actively partake in the room. For more info, read your groupchat's staff intro.`);
727 if (parent) this.modlog('SUBROOMGROUPCHAT', null, title);
728 return;
729 }
730 return this.errorReply(`An unknown error occurred while trying to create the room '${title}'.`);
731 },
732 makegroupchathelp: [
733 `/makegroupchat [roomname] - Creates an invite-only group chat named [roomname].`,
734 `/subroomgroupchat [roomname] - Creates a subroom groupchat of the current room. Can only be used in a public room you have staff in.`,
735 ],
736
737 deregisterchatroom: function (target, room, user) {
738 if (!this.can('makeroom')) return;
739 this.errorReply("NOTE: You probably want to use `/deleteroom` now that it exists.");
740 let id = toId(target);
741 if (!id) return this.parse('/help deregisterchatroom');
742 let targetRoom = Rooms.search(id);
743 if (!targetRoom) return this.errorReply(`The room '${target}' doesn't exist.`);
744 target = targetRoom.title || targetRoom.id;
745 const isPrivate = targetRoom.isPrivate;
746 const staffRoom = Rooms('staff');
747 const upperStaffRoom = Rooms('upperstaff');
748 if (Rooms.global.deregisterChatRoom(id)) {
749 this.sendReply(`The room '${target}' was deregistered.`);
750 this.sendReply("It will be deleted as of the next server restart.");
751 target = Chat.escapeHTML(target);
752 if (isPrivate) {
753 if (upperStaffRoom) upperStaffRoom.add(`|raw|<div class="broadcast-red">Private chat room deregistered by ${user.userid}: <b>${target}</b></div>`).update();
754 } else {
755 if (staffRoom) staffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered: <b>${target}</b></div>`).update();
756 if (upperStaffRoom) upperStaffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered by ${user.userid}: <b>${target}</b></div>`).update();
757 }
758 return;
759 }
760 return this.errorReply(`The room "${target}" isn't registered.`);
761 },
762 deregisterchatroomhelp: [`/deregisterchatroom [roomname] - Deletes room [roomname] after the next server restart. Requires: & ~`],
763
764 deletechatroom: 'deleteroom',
765 deletegroupchat: 'deleteroom',
766 deleteroom: function (target, room, user, connection, cmd) {
767 let roomid = target.trim();
768 if (!roomid) {
769 // allow deleting personal rooms without typing out the room name
770 if (!room.isPersonal || cmd !== "deletegroupchat") {
771 return this.parse(`/help deleteroom`);
772 }
773 } else {
774 let targetRoom = Rooms.search(roomid);
775 if (targetRoom !== room) {
776 return this.parse(`/help deleteroom`);
777 }
778 }
779
780 if (room.isPersonal) {
781 if (!this.can('gamemanagement', null, room)) return;
782 } else {
783 if (!this.can('makeroom')) return;
784 }
785
786 const title = room.title || room.id;
787
788 if (room.id === 'global') {
789 return this.errorReply(`This room can't be deleted.`);
790 }
791
792 if (room.chatRoomData) {
793 if (room.isPrivate) {
794 if (Rooms.get('upperstaff')) {
795 Rooms.get('upperstaff').add(Chat.html`|raw|<div class="broadcast-red">Private chat room deleted by ${user.userid}: <b>${title}</b></div>`).update();
796 }
797 } else {
798 if (Rooms.get('staff')) {
799 Rooms.get('staff').add(Chat.html`|raw|<div class="broadcast-red">Public chat room deleted: <b>${title}</b></div>`).update();
800 }
801 if (Rooms.get('upperstaff')) {
802 Rooms.get('upperstaff').add(Chat.html`|raw|<div class="broadcast-red">Public chat room deleted by ${user.userid}: <b>${title}</b></div>`).update();
803 }
804 }
805 }
806
807 if (room.subRooms) {
808 for (const subRoom of room.subRooms.values()) subRoom.parent = null;
809 }
810
811 room.add(`|raw|<div class="broadcast-red"><b>This room has been deleted.</b></div>`);
812 room.update(); // |expire| needs to be its own message
813 room.add(`|expire|This room has been deleted.`);
814 this.sendReply(`The room "${title}" was deleted.`);
815 room.update();
816 room.destroy();
817 },
818 deleteroomhelp: [
819 `/deleteroom [roomname] - Deletes room [roomname]. Must be typed in the room to delete. Requires: & ~`,
820 `/deletegroupchat - Deletes the current room, if it's a groupchat. Requires: & ~ #`,
821 ],
822
823 hideroom: 'privateroom',
824 hiddenroom: 'privateroom',
825 secretroom: 'privateroom',
826 publicroom: 'privateroom',
827 privateroom: function (target, room, user, connection, cmd) {
828 if (room.isPersonal) {
829 if (!this.can('editroom', null, room)) return;
830 } else if (room.battle) {
831 if (!this.can('editprivacy', null, room)) return;
832 } else {
833 // registered chatrooms show up on the room list and so require
834 // higher permissions to modify privacy settings
835 if (!this.can('makeroom')) return;
836 }
837 let setting;
838 switch (cmd) {
839 case 'privateroom':
840 return this.parse('/help privateroom');
841 case 'publicroom':
842 setting = false;
843 break;
844 case 'secretroom':
845 setting = true;
846 break;
847 default:
848 if (room.isPrivate === true && target !== 'force') {
849 return this.sendReply(`This room is a secret room. Use "/publicroom" to make it public, or "/hiddenroom force" to force it hidden.`);
850 }
851 setting = 'hidden';
852 break;
853 }
854
855 if ((setting === true || room.isPrivate === true) && !room.isPersonal) {
856 if (!this.can('makeroom')) return;
857 }
858
859 if (this.meansNo(target) || !setting) {
860 if (!room.isPrivate) {
861 return this.errorReply(`This room is already public.`);
862 }
863 if (room.isPersonal) return this.errorReply(`This room can't be made public.`);
864 if (room.privacySetter && user.can('nooverride', null, room) && !user.can('makeroom')) {
865 if (!room.privacySetter.has(user.userid)) {
866 const privacySetters = Array.from(room.privacySetter).join(', ');
867 return this.errorReply(`You can't make the room public since you didn't make it private - only ${privacySetters} can.`);
868 }
869 room.privacySetter.delete(user.userid);
870 if (room.privacySetter.size) {
871 const privacySetters = Array.from(room.privacySetter).join(', ');
872 return this.sendReply(`You are no longer forcing the room to stay private, but ${privacySetters} also need${Chat.plural(room.privacySetter, "", "s")} to use /publicroom to make the room public.`);
873 }
874 }
875 delete room.isPrivate;
876 room.privacySetter = null;
877 this.addModAction(`${user.name} made this room public.`);
878 this.modlog('PUBLICROOM');
879 if (room.chatRoomData) {
880 delete room.chatRoomData.isPrivate;
881 Rooms.global.writeChatRoomData();
882 }
883 } else {
884 const settingName = (setting === true ? 'secret' : setting);
885 if (room.subRooms) return this.errorReply("Private rooms cannot have subrooms.");
886 if (room.isPrivate === setting) {
887 if (room.privacySetter && !room.privacySetter.has(user.userid)) {
888 room.privacySetter.add(user.userid);
889 return this.sendReply(`This room is already ${settingName}, but is now forced to stay that way until you use /publicroom.`);
890 }
891 return this.errorReply(`This room is already ${settingName}.`);
892 }
893 room.isPrivate = setting;
894 this.addModAction(`${user.name} made this room ${settingName}.`);
895 this.modlog(`${settingName.toUpperCase()}ROOM`);
896 if (room.chatRoomData) {
897 room.chatRoomData.isPrivate = setting;
898 Rooms.global.writeChatRoomData();
899 }
900 room.privacySetter = new Set([user.userid]);
901 }
902 },
903 privateroomhelp: [
904 `/secretroom - Makes a room secret. Secret rooms are visible to & and up. Requires: & ~`,
905 `/hiddenroom [on/off] - Makes a room hidden. Hidden rooms are visible to % and up, and inherit global ranks. Requires: \u2606 & ~`,
906 `/publicroom - Makes a room public. Requires: \u2606 & ~`,
907 ],
908
909 officialchatroom: 'officialroom',
910 officialroom: function (target, room, user) {
911 if (!this.can('makeroom')) return;
912 if (!room.chatRoomData) {
913 return this.errorReply(`/officialroom - This room can't be made official`);
914 }
915 if (this.meansNo(target)) {
916 if (!room.isOfficial) return this.errorReply(`This chat room is already unofficial.`);
917 delete room.isOfficial;
918 this.addModAction(`${user.name} made this chat room unofficial.`);
919 this.modlog('UNOFFICIALROOM');
920 delete room.chatRoomData.isOfficial;
921 Rooms.global.writeChatRoomData();
922 } else {
923 if (room.isOfficial) return this.errorReply(`This chat room is already official.`);
924 room.isOfficial = true;
925 this.addModAction(`${user.name} made this chat room official.`);
926 this.modlog('OFFICIALROOM');
927 room.chatRoomData.isOfficial = true;
928 Rooms.global.writeChatRoomData();
929 }
930 },
931
932 psplwinnerroom: function (target, room, user) {
933 if (!this.can('makeroom')) return;
934 if (!room.chatRoomData) {
935 return this.errorReply(`/psplwinnerroom - This room can't be marked as a PSPL Winner room`);
936 }
937 if (this.meansNo(target)) {
938 if (!room.pspl) return this.errorReply(`This chat room is already not a PSPL Winner room.`);
939 delete room.pspl;
940 this.addModAction(`${user.name} made this chat room no longer a PSPL Winner room.`);
941 this.modlog('PSPLROOM');
942 delete room.chatRoomData.pspl;
943 Rooms.global.writeChatRoomData();
944 } else {
945 if (room.pspl) return this.errorReply("This chat room is already a PSPL Winner room.");
946 room.pspl = true;
947 this.addModAction(`${user.name} made this chat room a PSPL Winner room.`);
948 this.modlog('UNPSPLROOM');
949 room.chatRoomData.pspl = true;
950 Rooms.global.writeChatRoomData();
951 }
952 },
953
954 setsubroom: 'subroom',
955 subroom: function (target, room, user) {
956 if (!user.can('makeroom')) return this.errorReply(`/subroom - Access denied. Did you mean /subrooms?`);
957 if (!target) return this.parse('/help subroom');
958
959 if (!room.chatRoomData) return this.errorReply(`Temporary rooms cannot be subrooms.`);
960 if (room.parent) return this.errorReply(`This room is already a subroom. To change which room this subroom belongs to, remove the subroom first.`);
961 if (room.subRooms) return this.errorReply(`This room is already a parent room, and a parent room cannot be made as a subroom.`);
962
963 const main = Rooms.search(target);
964
965 if (!main) return this.errorReply(`The room '${target}' does not exist.`);
966 if (main.isPrivate || !main.chatRoomData) return this.errorReply(`Only public rooms can have subrooms.`);
967 if (room === main) return this.errorReply(`You cannot set a room to be a subroom of itself.`);
968
969 room.parent = main;
970 if (!main.subRooms) main.subRooms = new Map();
971 main.subRooms.set(room.id, room);
972
973 const mainIdx = Rooms.global.chatRoomDataList.findIndex(r => r.title === main.title);
974 const subIdx = Rooms.global.chatRoomDataList.findIndex(r => r.title === room.title);
975
976 // This is needed to ensure that the main room gets loaded before the subroom.
977 if (mainIdx > subIdx) {
978 const tmp = Rooms.global.chatRoomDataList[mainIdx];
979 Rooms.global.chatRoomDataList[mainIdx] = Rooms.global.chatRoomDataList[subIdx];
980 Rooms.global.chatRoomDataList[subIdx] = tmp;
981 }
982
983 room.chatRoomData.parentid = main.id;
984 Rooms.global.writeChatRoomData();
985
986 for (let userid in room.users) {
987 room.users[userid].updateIdentity(room.id);
988 }
989
990 this.modlog('SUBROOM', null, `of ${main.title}`);
991 return this.addModAction(`This room was set as a subroom of ${main.title} by ${user.name}.`);
992 },
993
994 removesubroom: 'unsubroom',
995 desubroom: 'unsubroom',
996 unsubroom: function (target, room, user) {
997 if (!this.can('makeroom')) return;
998 if (!room.parent || !room.chatRoomData) return this.errorReply(`This room is not currently a subroom of a public room.`);
999
1000 const parent = room.parent;
1001 if (parent && parent.subRooms) {
1002 parent.subRooms.delete(room.id);
1003 if (!parent.subRooms.size) parent.subRooms = null;
1004 }
1005
1006 room.parent = null;
1007
1008 delete room.chatRoomData.parentid;
1009 Rooms.global.writeChatRoomData();
1010
1011 for (let userid in room.users) {
1012 room.users[userid].updateIdentity(room.id);
1013 }
1014
1015 this.modlog('UNSUBROOM');
1016 return this.addModAction(`This room was unset as a subroom by ${user.name}.`);
1017 },
1018
1019 parentroom: 'subrooms',
1020 subrooms: function (target, room, user, connection, cmd) {
1021 if (cmd === 'parentroom') {
1022 if (!room.parent) return this.errorReply(`This room is not a subroom.`);
1023 return this.sendReply(`This is a subroom of ${room.parent.title}.`);
1024 }
1025 if (!room.chatRoomData) return this.errorReply(`Temporary rooms cannot have subrooms.`);
1026
1027 if (!this.runBroadcast()) return;
1028
1029 const showSecret = !this.broadcasting && user.can('mute', null, room);
1030
1031 const subRooms = room.getSubRooms(showSecret);
1032
1033 if (!subRooms.length) return this.sendReply(`This room doesn't have any subrooms.`);
1034
1035 const subRoomText = subRooms.map(room => Chat.html`<a href="/${room.id}">${room.title}</a><br/><small>${room.desc}</small>`);
1036
1037 return this.sendReplyBox(`<p style="font-weight:bold;">${Chat.escapeHTML(room.title)}'s subroom${Chat.plural(subRooms)}:</p><ul><li>${subRoomText.join('</li><br/><li>')}</li></ul></strong>`);
1038 },
1039
1040 subroomhelp: [
1041 `/subroom [room] - Marks the current room as a subroom of [room]. Requires: & ~`,
1042 `/unsubroom - Unmarks the current room as a subroom. Requires: & ~`,
1043 `/subrooms - Displays the current room's subrooms.`,
1044 `/parentroom - Displays the current room's parent room.`,
1045 ],
1046
1047 roomdesc: function (target, room, user) {
1048 if (!target) {
1049 if (!this.runBroadcast()) return;
1050 if (!room.desc) return this.sendReply(`This room does not have a description set.`);
1051 this.sendReplyBox(Chat.html`The room description is: ${room.desc}`);
1052 return;
1053 }
1054 if (!this.can('declare')) return false;
1055 if (target.length > 80) return this.errorReply(`Error: Room description is too long (must be at most 80 characters).`);
1056 let normalizedTarget = ' ' + target.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + ' ';
1057
1058 if (normalizedTarget.includes(' welcome ')) {
1059 return this.errorReply(`Error: Room description must not contain the word "welcome".`);
1060 }
1061 if (normalizedTarget.slice(0, 9) === ' discuss ') {
1062 return this.errorReply(`Error: Room description must not start with the word "discuss".`);
1063 }
1064 if (normalizedTarget.slice(0, 12) === ' talk about ' || normalizedTarget.slice(0, 17) === ' talk here about ') {
1065 return this.errorReply(`Error: Room description must not start with the phrase "talk about".`);
1066 }
1067
1068 room.desc = target;
1069 this.sendReply(`(The room description is now: ${target})`);
1070
1071 this.privateModAction(`(${user.name} changed the roomdesc to: "${target}".)`);
1072 this.modlog('ROOMDESC', null, `to "${target}"`);
1073
1074 if (room.chatRoomData) {
1075 room.chatRoomData.desc = room.desc;
1076 Rooms.global.writeChatRoomData();
1077 }
1078 },
1079
1080 topic: 'roomintro',
1081 roomintro: function (target, room, user, connection, cmd) {
1082 if (!target) {
1083 if (!this.runBroadcast()) return;
1084 if (!room.introMessage) return this.sendReply("This room does not have an introduction set.");
1085 this.sendReply('|raw|<div class="infobox infobox-limited">' + room.introMessage.replace(/\n/g, '') + '</div>');
1086 if (!this.broadcasting && user.can('declare', null, room) && cmd !== 'topic') {
1087 this.sendReply('Source:');
1088 const code = Chat.escapeHTML(room.introMessage).replace(/\n/g, '<br />');
1089 this.sendReplyBox(`<code style="white-space: pre-wrap">/roomintro ${code}</code>`);
1090 }
1091 return;
1092 }
1093 if (!this.can('editroom', null, room)) return false;
1094 if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deleteroomintro"?');
1095 target = this.canHTML(target);
1096 if (!target) return;
1097 if (!/</.test(target)) {
1098 // not HTML, do some simple URL linking
1099 let re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
1100 target = target.replace(re, '<a href="$1">$1</a>');
1101 }
1102 if (target.substr(0, 11) === '/roomintro ') target = target.substr(11);
1103
1104 room.introMessage = target.replace(/\r/g, '');
1105 this.sendReply("(The room introduction has been changed to:)");
1106 this.sendReply(`|raw|<div class="infobox infobox-limited">${room.introMessage.replace(/\n/g, '')}</div>`);
1107
1108 this.privateModAction(`(${user.name} changed the roomintro.)`);
1109 this.modlog('ROOMINTRO');
1110 this.roomlog(room.introMessage.replace(/\n/g, ''));
1111
1112 if (room.chatRoomData) {
1113 room.chatRoomData.introMessage = room.introMessage;
1114 Rooms.global.writeChatRoomData();
1115 }
1116 },
1117
1118 deletetopic: 'deleteroomintro',
1119 deleteroomintro: function (target, room, user) {
1120 if (!this.can('declare', null, room)) return false;
1121 if (!room.introMessage) return this.errorReply("This room does not have a introduction set.");
1122
1123 this.privateModAction(`(${user.name} deleted the roomintro.)`);
1124 this.modlog('DELETEROOMINTRO');
1125 this.roomlog(target);
1126
1127 delete room.introMessage;
1128 if (room.chatRoomData) {
1129 delete room.chatRoomData.introMessage;
1130 Rooms.global.writeChatRoomData();
1131 }
1132 },
1133
1134 stafftopic: 'staffintro',
1135 staffintro: function (target, room, user, connection, cmd) {
1136 if (!target) {
1137 if (!this.can('mute', null, room)) return false;
1138 if (!room.staffMessage) return this.sendReply("This room does not have a staff introduction set.");
1139 this.sendReply(`|raw|<div class="infobox">${room.staffMessage.replace(/\n/g, ``)}</div>`);
1140 if (user.can('ban', null, room) && cmd !== 'stafftopic') {
1141 this.sendReply('Source:');
1142 const code = Chat.escapeHTML(room.staffMessage).replace(/\n/g, '<br />');
1143 this.sendReplyBox(`<code style="white-space: pre-wrap">/staffintro ${code}</code>`);
1144 }
1145 return;
1146 }
1147 if (!this.can('ban', null, room)) return false;
1148 if (!this.canTalk()) return;
1149 if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deletestaffintro"?');
1150 target = this.canHTML(target);
1151 if (!target) return;
1152 if (!/</.test(target)) {
1153 // not HTML, do some simple URL linking
1154 let re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
1155 target = target.replace(re, '<a href="$1">$1</a>');
1156 }
1157 if (target.substr(0, 12) === '/staffintro ') target = target.substr(12);
1158
1159 room.staffMessage = target.replace(/\r/g, '');
1160 this.sendReply("(The staff introduction has been changed to:)");
1161 this.sendReply(`|raw|<div class="infobox">${target.replace(/\n/g, ``)}</div>`);
1162
1163 this.privateModAction(`(${user.name} changed the staffintro.)`);
1164 this.modlog('STAFFINTRO');
1165 this.roomlog(room.staffMessage.replace(/\n/g, ``));
1166
1167 if (room.chatRoomData) {
1168 room.chatRoomData.staffMessage = room.staffMessage;
1169 Rooms.global.writeChatRoomData();
1170 }
1171 },
1172
1173 deletestafftopic: 'deletestaffintro',
1174 deletestaffintro: function (target, room, user) {
1175 if (!this.can('ban', null, room)) return false;
1176 if (!room.staffMessage) return this.errorReply("This room does not have a staff introduction set.");
1177
1178 this.privateModAction(`(${user.name} deleted the staffintro.)`);
1179 this.modlog('DELETESTAFFINTRO');
1180 this.roomlog(target);
1181
1182 delete room.staffMessage;
1183 if (room.chatRoomData) {
1184 delete room.chatRoomData.staffMessage;
1185 Rooms.global.writeChatRoomData();
1186 }
1187 },
1188
1189 roomalias: function (target, room, user) {
1190 if (!target) {
1191 if (!this.runBroadcast()) return;
1192 if (!room.aliases || !room.aliases.length) return this.sendReplyBox("This room does not have any aliases.");
1193 return this.sendReplyBox(`This room has the following aliases: ${room.aliases.join(", ")}`);
1194 }
1195 if (!this.can('makeroom')) return false;
1196 if (target.includes(',')) {
1197 this.errorReply(`Invalid room alias: ${target.trim()}`);
1198 return this.parse('/help roomalias');
1199 }
1200
1201 let alias = toId(target);
1202 if (!alias.length) return this.errorReply("Only alphanumeric characters are valid in an alias.");
1203 if (Rooms(alias) || Rooms.aliases.has(alias)) return this.errorReply("You cannot set an alias to an existing room or alias.");
1204 if (room.isPersonal) return this.errorReply("Personal rooms can't have aliases.");
1205
1206 Rooms.aliases.set(alias, room.id);
1207 this.privateModAction(`(${user.name} added the room alias '${alias}'.)`);
1208 this.modlog('ROOMALIAS', null, alias);
1209
1210 if (!room.aliases) room.aliases = [];
1211 room.aliases.push(alias);
1212 if (room.chatRoomData) {
1213 room.chatRoomData.aliases = room.aliases;
1214 Rooms.global.writeChatRoomData();
1215 }
1216 },
1217 roomaliashelp: [
1218 `/roomalias - displays a list of all room aliases of the room the command was entered in.`,
1219 `/roomalias [alias] - adds the given room alias to the room the command was entered in. Requires: & ~`,
1220 `/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: & ~`,
1221 ],
1222
1223 deleteroomalias: 'removeroomalias',
1224 deroomalias: 'removeroomalias',
1225 unroomalias: 'removeroomalias',
1226 removeroomalias: function (target, room, user) {
1227 if (!room.aliases) return this.errorReply("This room does not have any aliases.");
1228 if (!this.can('makeroom')) return false;
1229 if (target.includes(',')) {
1230 this.errorReply(`Invalid room alias: ${target.trim()}`);
1231 return this.parse('/help removeroomalias');
1232 }
1233
1234 let alias = toId(target);
1235 if (!alias || !Rooms.aliases.has(alias)) return this.errorReply("Please specify an existing alias.");
1236 if (Rooms.aliases.get(alias) !== room.id) return this.errorReply("You may only remove an alias from the current room.");
1237
1238 this.privateModAction(`(${user.name} removed the room alias '${alias}'.)`);
1239 this.modlog('REMOVEALIAS', null, alias);
1240
1241 let aliasIndex = room.aliases.indexOf(alias);
1242 if (aliasIndex >= 0) {
1243 room.aliases.splice(aliasIndex, 1);
1244 Rooms.aliases.delete(alias);
1245 Rooms.global.writeChatRoomData();
1246 }
1247 },
1248 removeroomaliashelp: [`/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: & ~`],
1249
1250 roomowner: function (target, room, user) {
1251 if (!room.chatRoomData) {
1252 return this.sendReply("/roomowner - This room isn't designed for per-room moderation to be added");
1253 }
1254 if (!target) return this.parse('/help roomowner');
1255 target = this.splitTarget(target, true);
1256 if (target) return this.errorReply(`This command does not support specifying a reason.`);
1257 let targetUser = this.targetUser;
1258 let name = this.targetUsername;
1259 let userid = toId(name);
1260
1261 if (!Users.isUsernameKnown(userid)) {
1262 return this.errorReply(`User '${this.targetUsername}' is offline and unrecognized, and so can't be promoted.`);
1263 }
1264
1265 if (!this.can('makeroom')) return false;
1266
1267 if (!room.auth) room.auth = room.chatRoomData.auth = {};
1268
1269 room.auth[userid] = '#';
1270 this.addModAction(`${name} was appointed Room Owner by ${user.name}.`);
1271 this.modlog('ROOMOWNER', userid);
1272 if (targetUser) {
1273 targetUser.popup(`You were appointed Room Owner by ${user.name} in ${room.id}.`);
1274 room.onUpdateIdentity(targetUser);
1275 if (room.subRooms) {
1276 for (const subRoom of room.subRooms.values()) {
1277 subRoom.onUpdateIdentity(targetUser);
1278 }
1279 }
1280 }
1281 Rooms.global.writeChatRoomData();
1282 },
1283 roomownerhelp: [`/roomowner [username] - Appoints [username] as a room owner. Requires: & ~`],
1284
1285 '!roompromote': true,
1286 roomdemote: 'roompromote',
1287 roompromote: function (target, room, user, connection, cmd) {
1288 if (!room) {
1289 // this command isn't marked as room-only because it's usable in PMs through /invite
1290 return this.errorReply("This command is only available in rooms");
1291 }
1292 if (!room.auth) {
1293 this.sendReply("/roompromote - This room isn't designed for per-room moderation.");
1294 return this.sendReply("Before setting room staff, you need to set a room owner with /roomowner");
1295 }
1296 if (!this.canTalk()) return;
1297 if (!target) return this.parse('/help roompromote');
1298
1299 const force = target.startsWith('!!!');
1300 if (force) target = target.slice(3);
1301 target = this.splitTarget(target, true);
1302 let targetUser = this.targetUser;
1303 let userid = toId(this.targetUsername);
1304 let name = targetUser ? targetUser.name : this.targetUsername;
1305
1306 if (!userid) return this.parse('/help roompromote');
1307 if (!targetUser && !Users.isUsernameKnown(userid) && !force) {
1308 return this.errorReply(`User '${name}' is offline and unrecognized, and so can't be promoted.`);
1309 }
1310 if (targetUser && !targetUser.registered) {
1311 return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
1312 }
1313
1314 let currentGroup = room.getAuth({userid, group: (Users.usergroups[userid] || ' ').charAt(0)});
1315 let nextGroup = target;
1316 if (target === 'deauth') nextGroup = Config.groupsranking[0];
1317 if (!nextGroup) {
1318 return this.errorReply("Please specify a group such as /roomvoice or /roomdeauth");
1319 }
1320 if (!Config.groups[nextGroup]) {
1321 return this.errorReply(`Group '${nextGroup}' does not exist.`);
1322 }
1323
1324 if (Config.groups[nextGroup].globalonly || (Config.groups[nextGroup].battleonly && !room.battle)) {
1325 return this.errorReply(`Group 'room${Config.groups[nextGroup].id}' does not exist as a room rank.`);
1326 }
1327
1328 let groupName = Config.groups[nextGroup].name || "regular user";
1329 if ((room.auth[userid] || Config.groupsranking[0]) === nextGroup) {
1330 return this.errorReply(`User '${name}' is already a ${groupName} in this room.`);
1331 }
1332 if (!user.can('makeroom')) {
1333 if (currentGroup !== ' ' && !user.can('room' + (Config.groups[currentGroup] ? Config.groups[currentGroup].id : 'voice'), null, room)) {
1334 return this.errorReply(`/${cmd} - Access denied for promoting/demoting from ${(Config.groups[currentGroup] ? Config.groups[currentGroup].name : "an undefined group")}.`);
1335 }
1336 if (nextGroup !== ' ' && !user.can('room' + Config.groups[nextGroup].id, null, room)) {
1337 return this.errorReply(`/${cmd} - Access denied for promoting/demoting to ${Config.groups[nextGroup].name}.`);
1338 }
1339 }
1340 let nextGroupIndex = Config.groupsranking.indexOf(nextGroup) || 1; // assume voice if not defined (although it should be by now)
1341 if (targetUser && targetUser.locked && !room.isPrivate && !room.battle && !room.isPersonal && nextGroupIndex >= 2) {
1342 return this.errorReply("Locked users can't be promoted.");
1343 }
1344
1345 if (nextGroup === Config.groupsranking[0]) {
1346 delete room.auth[userid];
1347 } else {
1348 room.auth[userid] = nextGroup;
1349 }
1350
1351 // Only show popup if: user is online and in the room, the room is public, and not a groupchat or a battle.
1352 let needsPopup = targetUser && room.users[targetUser.userid] && !room.isPrivate && !room.isPersonal && !room.battle;
1353
1354 if (this.pmTarget && targetUser) {
1355 const text = `${targetUser.name} was invited (and promoted to Room ${groupName}) by ${user.name}.`;
1356 room.add(`|c|${user.getIdentity(room)}|/log ${text}`).update();
1357 this.modlog('INVITE', targetUser, null, {noip: 1, noalts: 1});
1358 } else if (nextGroup in Config.groups && currentGroup in Config.groups && Config.groups[nextGroup].rank < Config.groups[currentGroup].rank) {
1359 if (targetUser && room.users[targetUser.userid] && !Config.groups[nextGroup].modlog) {
1360 // if the user can't see the demotion message (i.e. rank < %), it is shown in the chat
1361 targetUser.send(`>${room.id}\n(You were demoted to Room ${groupName} by ${user.name}.)`);
1362 }
1363 this.privateModAction(`(${name} was demoted to Room ${groupName} by ${user.name}.)`);
1364 this.modlog(`ROOM${groupName.toUpperCase()}`, userid, '(demote)');
1365 if (needsPopup) targetUser.popup(`You were demoted to Room ${groupName} by ${user.name} in ${room.id}.`);
1366 } else if (nextGroup === '#') {
1367 this.addModAction(`${'' + name} was promoted to ${groupName} by ${user.name}.`);
1368 this.modlog('ROOM OWNER', userid);
1369 if (needsPopup) targetUser.popup(`You were promoted to ${groupName} by ${user.name} in ${room.id}.`);
1370 } else {
1371 this.addModAction(`${'' + name} was promoted to Room ${groupName} by ${user.name}.`);
1372 this.modlog(`ROOM${groupName.toUpperCase()}`, userid);
1373 if (needsPopup) targetUser.popup(`You were promoted to Room ${groupName} by ${user.name} in ${room.id}.`);
1374 }
1375
1376 if (targetUser) {
1377 targetUser.updateIdentity(room.id);
1378 if (room.subRooms) {
1379 for (const subRoom of room.subRooms.values()) {
1380 targetUser.updateIdentity(subRoom.id);
1381 }
1382 }
1383 }
1384 if (room.chatRoomData) Rooms.global.writeChatRoomData();
1385 },
1386 roompromotehelp: [
1387 `/roompromote OR /roomdemote [username], [group symbol] - Promotes/demotes the user to the specified room rank. Requires: @ * # & ~`,
1388 `/room[group] [username] - Promotes/demotes the user to the specified room rank. Requires: @ * # & ~`,
1389 `/roomdeauth [username] - Removes all room rank from the user. Requires: @ * # & ~`,
1390 ],
1391
1392 '!roomauth': true,
1393 roomstaff: 'roomauth',
1394 roomauth1: 'roomauth',
1395 roomauth: function (target, room, user, connection, cmd) {
1396 let userLookup = '';
1397 if (cmd === 'roomauth1') userLookup = `\n\nTo look up auth for a user, use /userauth ${target}`;
1398 let targetRoom = room;
1399 if (target) targetRoom = Rooms.search(target);
1400 if (!targetRoom || targetRoom.id === 'global' || !targetRoom.checkModjoin(user)) return this.errorReply(`The room "${target}" does not exist.`);
1401 if (!targetRoom.auth) return this.sendReply(`/roomauth - The room '${targetRoom.title || target}' isn't designed for per-room moderation and therefore has no auth list.${userLookup}`);
1402
1403 let rankLists = {};
1404 for (let u in targetRoom.auth) {
1405 if (!rankLists[targetRoom.auth[u]]) rankLists[targetRoom.auth[u]] = [];
1406 rankLists[targetRoom.auth[u]].push(u);
1407 }
1408
1409 let buffer = Object.keys(rankLists).sort((a, b) =>
1410 (Config.groups[b] || {rank: 0}).rank - (Config.groups[a] || {rank: 0}).rank
1411 ).map(r => {
1412 let roomRankList = rankLists[r].sort();
1413 roomRankList = roomRankList.map(s => s in targetRoom.users ? `**${s}**` : s);
1414 return `${Config.groups[r] ? `${Config.groups[r].name}s (${r})` : r}:\n${roomRankList.join(", ")}`;
1415 });
1416
1417 if (!buffer.length) {
1418 connection.popup(`The room '${targetRoom.title}' has no auth. ${userLookup}`);
1419 return;
1420 }
1421 let curRoom = targetRoom;
1422 while (curRoom.parent) {
1423 const modjoinSetting = curRoom.modjoin === true ? curRoom.modchat : curRoom.modjoin;
1424 const roomType = (modjoinSetting ? `modjoin ${modjoinSetting} ` : '');
1425 const inheritedUserType = (modjoinSetting ? ` of rank ${modjoinSetting} and above` : '');
1426 if (curRoom.parent) {
1427 buffer.push(`${curRoom.title} is a ${roomType}subroom of ${curRoom.parent.title}, so ${curRoom.parent.title} users${inheritedUserType} also have authority in this room.`);
1428 }
1429 curRoom = curRoom.parent;
1430 }
1431 if (!curRoom.isPrivate) {
1432 buffer.push(`${curRoom.title} is a public room, so global auth with no relevant roomauth will have authority in this room.`);
1433 } else if (curRoom.isPrivate === 'hidden' || curRoom.isPrivate === 'voice') {
1434 buffer.push(`${curRoom.title} is a hidden room, so global auth with no relevant roomauth will have authority in this room.`);
1435 }
1436 if (targetRoom !== room) buffer.unshift(`${targetRoom.title} room auth:`);
1437 connection.popup(`${buffer.join("\n\n")}${userLookup}`);
1438 },
1439
1440 '!userauth': true,
1441 userauth: function (target, room, user, connection) {
1442 let targetId = toId(target) || user.userid;
1443 let targetUser = Users.getExact(targetId);
1444 let targetUsername = (targetUser ? targetUser.name : target);
1445
1446 let buffer = [];
1447 let innerBuffer = [];
1448 let group = Users.usergroups[targetId];
1449 if (group) {
1450 buffer.push(`Global auth: ${group.charAt(0)}`);
1451 }
1452 Rooms.rooms.forEach((curRoom, id) => {
1453 if (!curRoom.auth || curRoom.isPrivate) return;
1454 group = curRoom.auth[targetId];
1455 if (!group) return;
1456 innerBuffer.push(group + id);
1457 });
1458 if (innerBuffer.length) {
1459 buffer.push(`Room auth: ${innerBuffer.join(', ')}`);
1460 }
1461 if (targetId === user.userid || user.can('lock')) {
1462 innerBuffer = [];
1463 Rooms.rooms.forEach((curRoom, id) => {
1464 if (!curRoom.auth || !curRoom.isPrivate) return;
1465 if (curRoom.isPrivate === true) return;
1466 let auth = curRoom.auth[targetId];
1467 if (!auth) return;
1468 innerBuffer.push(auth + id);
1469 });
1470 if (innerBuffer.length) {
1471 buffer.push(`Hidden room auth: ${innerBuffer.join(', ')}`);
1472 }
1473 }
1474 if (targetId === user.userid || user.can('makeroom')) {
1475 innerBuffer = [];
1476 for (const chatRoom of Rooms.global.chatRooms) {
1477 if (!chatRoom.auth || !chatRoom.isPrivate) continue;
1478 if (chatRoom.isPrivate !== true) continue;
1479 let auth = chatRoom.auth[targetId];
1480 if (!auth) continue;
1481 innerBuffer.push(auth + chatRoom.id);
1482 }
1483 if (innerBuffer.length) {
1484 buffer.push(`Private room auth: ${innerBuffer.join(', ')}`);
1485 }
1486 }
1487 if (!buffer.length) {
1488 buffer.push("No global or room auth.");
1489 }
1490
1491 buffer.unshift(`${targetUsername} user auth:`);
1492 connection.popup(buffer.join("\n\n"));
1493 },
1494
1495 rb: 'ban',
1496 roomban: 'ban',
1497 b: 'ban',
1498 ban: function (target, room, user, connection) {
1499 if (!target) return this.parse('/help ban');
1500 if (!this.canTalk()) return;
1501
1502 target = this.splitTarget(target);
1503 let targetUser = this.targetUser;
1504 if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
1505 if (target.length > MAX_REASON_LENGTH) {
1506 return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
1507 }
1508 if (!this.can('ban', targetUser, room)) return false;
1509 if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to ban upper staff members.");
1510 if (Punishments.getRoomPunishType(room, this.targetUsername) === 'BLACKLIST') return this.errorReply(`This user is already blacklisted from ${room.id}.`);
1511 let name = targetUser.getLastName();
1512 let userid = targetUser.getLastId();
1513
1514 if (Punishments.isRoomBanned(targetUser, room.id) && !target) {
1515 let problem = " but was already banned";
1516 return this.privateModAction(`(${name} would be banned by ${user.name} ${problem}.)`);
1517 }
1518
1519 if (targetUser.trusted && room.isPrivate !== true && !room.isPersonal) {
1520 Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} ${(targetUser.trusted !== targetUser.userid ? ` (${targetUser.trusted})` : ``)} was roombanned from ${room.id} by ${user.name}, and should probably be demoted.`);
1521 }
1522
1523 if (targetUser in room.users || user.can('lock')) {
1524 targetUser.popup(
1525 `|modal||html|<p>${Chat.escapeHTML(user.name)} has banned you from the room ${room.id} ${(room.subRooms ? ` and its subrooms` : ``)}.</p>${(target ? `<p>Reason: ${Chat.escapeHTML(target)}</p>` : ``)}<p>To appeal the ban, PM the staff member that banned you${(!room.battle && room.auth ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.id}">List Room Staff</button></p>` : `.</p>`)}`
1526 );
1527 }
1528
1529 const reason = (target ? ` (${target})` : ``);
1530 this.addModAction(`${name} was banned from ${room.title} by ${user.name}.${reason}`);
1531
1532 let affected = Punishments.roomBan(room, targetUser, null, null, target);
1533
1534 if (!room.isPrivate && room.chatRoomData) {
1535 let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
1536 let displayMessage = '';
1537 if (affected.length > 1) {
1538 displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : ``)} banned alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
1539 this.privateModAction(displayMessage);
1540 } else if (acAccount) {
1541 displayMessage = `(${name}'s ac account: ${acAccount})`;
1542 this.privateModAction(displayMessage);
1543 }
1544 }
1545 room.hideText([userid, toId(this.inputUsername)]);
1546
1547 if (room.isPrivate !== true && room.chatRoomData) {
1548 this.globalModlog("ROOMBAN", targetUser, ` by ${user.userid}${(target ? `: ${target}` : ``)}`);
1549 } else {
1550 this.modlog("ROOMBAN", targetUser, ` by ${user.userid}${(target ? `: ${target}` : ``)}`);
1551 }
1552 return true;
1553 },
1554 banhelp: [`/roomban [username], [reason] - Bans the user from the room you are in. Requires: @ # & ~`],
1555
1556 unroomban: 'unban',
1557 roomunban: 'unban',
1558 unban: function (target, room, user, connection) {
1559 if (!target) return this.parse('/help unban');
1560 if (!this.can('ban', null, room)) return false;
1561
1562 let name = Punishments.roomUnban(room, target);
1563
1564 if (name) {
1565 this.addModAction(`${name} was unbanned from ${room.title} by ${user.name}.`);
1566 if (room.isPrivate !== true && room.chatRoomData) {
1567 this.globalModlog("UNROOMBAN", name, ` by ${user.userid}`);
1568 }
1569 } else {
1570 this.errorReply(`User '${target}' is not banned from this room.`);
1571 }
1572 },
1573 unbanhelp: [`/roomunban [username] - Unbans the user from the room you are in. Requires: @ # & ~`],
1574
1575 '!autojoin': true,
1576 autojoin: function (target, room, user, connection) {
1577 let targets = target.split(',');
1578 if (targets.length > 11 || connection.inRooms.size > 1) return;
1579 Rooms.global.autojoinRooms(user, connection);
1580 let autojoins = [];
1581 for (const target of targets) {
1582 if (user.tryJoinRoom(target, connection) === Rooms.RETRY_AFTER_LOGIN) {
1583 autojoins.push(target);
1584 }
1585 }
1586 connection.autojoins = autojoins.join(',');
1587 },
1588
1589 '!join': true,
1590 joim: 'join',
1591 j: 'join',
1592 join: function (target, room, user, connection) {
1593 if (!target) return this.parse('/help join');
1594 if (target.startsWith('http://')) target = target.slice(7);
1595 if (target.startsWith('https://')) target = target.slice(8);
1596 if (target.startsWith('play.pokemonshowdown.com/')) target = target.slice(25);
1597 if (target.startsWith('psim.us/')) target = target.slice(8);
1598 if (user.tryJoinRoom(target, connection) === Rooms.RETRY_AFTER_LOGIN) {
1599 connection.sendTo(target, `|noinit|namerequired|The room '${target}' does not exist or requires a login to join.`);
1600 }
1601 },
1602 joinhelp: [`/join [roomname] - Attempt to join the room [roomname].`],
1603
1604 '!part': true,
1605 leave: 'part',
1606 part: function (target, room, user, connection) {
1607 let targetRoom = target ? Rooms.search(target) : room;
1608 if (!targetRoom || targetRoom === Rooms.global) {
1609 if (target.startsWith('view-')) return;
1610 return this.errorReply(`The room '${target}' does not exist.`);
1611 }
1612 user.leaveRoom(targetRoom, connection);
1613 },
1614
1615 /*********************************************************
1616 * Moderating: Punishments
1617 *********************************************************/
1618
1619 kick: 'warn',
1620 k: 'warn',
1621 warn: function (target, room, user) {
1622 if (!target) return this.parse('/help warn');
1623 if (!this.canTalk()) return;
1624 if (room.isPersonal && !user.can('warn')) return this.errorReply("Warning is unavailable in group chats.");
1625
1626 target = this.splitTarget(target);
1627 let targetUser = this.targetUser;
1628 if (!targetUser || !targetUser.connected) return this.errorReply(`User '${this.targetUsername}' not found.`);
1629 if (!(targetUser in room.users)) {
1630 return this.errorReply(`User ${this.targetUsername} is not in the room ${room.id}.`);
1631 }
1632 if (target.length > MAX_REASON_LENGTH) {
1633 return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
1634 }
1635 if (!this.can('warn', targetUser, room)) return false;
1636 if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to warn upper staff members.");
1637
1638 this.addModAction(`${targetUser.name} was warned by ${user.name}.${(target ? ` (${target})` : ``)}`);
1639 this.modlog('WARN', targetUser, target, {noalts: 1});
1640 targetUser.send(`|c|~|/warn ${target}`);
1641 let userid = targetUser.getLastId();
1642 this.add(`|unlink|${userid}`);
1643 if (userid !== toId(this.inputUsername)) this.add(`|unlink|${toId(this.inputUsername)}`);
1644 },
1645 warnhelp: [`/warn OR /k [username], [reason] - Warns a user showing them the Pok\u00e9mon Showdown Rules and [reason] in an overlay. Requires: % @ # & ~`],
1646
1647 redirect: 'redir',
1648 redir: function (target, room, user, connection) {
1649 if (!target) return this.parse('/help redirect');
1650 if (room.isPrivate || room.isPersonal) return this.errorReply("Users cannot be redirected from private or personal rooms.");
1651
1652 target = this.splitTarget(target);
1653 let targetUser = this.targetUser;
1654 let targetRoom = Rooms.search(target);
1655 if (!targetRoom || targetRoom.modjoin || targetRoom.staffRoom) {
1656 return this.errorReply(`The room "${target}" does not exist.`);
1657 }
1658 if (!this.can('warn', targetUser, room) || !this.can('warn', targetUser, targetRoom)) return false;
1659 if (!targetUser || !targetUser.connected) {
1660 return this.errorReply(`User ${this.targetUsername} not found.`);
1661 }
1662 if (targetRoom.id === "global") return this.errorReply(`Users cannot be redirected to the global room.`);
1663 if (targetRoom.isPrivate || targetRoom.isPersonal) {
1664 return this.errorReply(`The room "${target}" is not public.`);
1665 }
1666 if (targetUser.inRooms.has(targetRoom.id)) {
1667 return this.errorReply(`User ${targetUser.name} is already in the room ${targetRoom.title}!`);
1668 }
1669 if (!targetUser.inRooms.has(room.id)) {
1670 return this.errorReply(`User ${this.targetUsername} is not in the room ${room.id}.`);
1671 }
1672 targetUser.leaveRoom(room.id);
1673 targetUser.popup(`You are in the wrong room; please go to <<${targetRoom.id}>> instead`);
1674 this.addModAction(`${targetUser.name} was redirected to room ${targetRoom.title} by ${user.name}.`);
1675 this.modlog('REDIRECT', targetUser, `to ${targetRoom.title}`, {noip: 1, noalts: 1});
1676 targetUser.leaveRoom(room);
1677 },
1678 redirhelp: [`/redirect OR /redir [username], [roomname] - Attempts to redirect the user [username] to the room [roomname]. Requires: % @ & ~`],
1679
1680 m: 'mute',
1681 mute: function (target, room, user, connection, cmd) {
1682 if (!target) return this.parse('/help mute');
1683 if (!this.canTalk()) return;
1684
1685 target = this.splitTarget(target);
1686 let targetUser = this.targetUser;
1687 if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
1688 if (target.length > MAX_REASON_LENGTH) {
1689 return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
1690 }
1691
1692 let muteDuration = ((cmd === 'hm' || cmd === 'hourmute') ? HOURMUTE_LENGTH : MUTE_LENGTH);
1693 if (!this.can('mute', targetUser, room)) return false;
1694 if (targetUser.can('makeroom')) return this.errorReply("You are not allowed to mute upper staff members.");
1695 let canBeMutedFurther = ((room.getMuteTime(targetUser) || 0) <= (muteDuration * 5 / 6));
1696 if (targetUser.locked || (room.isMuted(targetUser) && !canBeMutedFurther) || Punishments.isRoomBanned(targetUser, room.id)) {
1697 let problem = ` but was already ${(targetUser.locked ? "locked" : room.isMuted(targetUser) ? "muted" : "room banned")}`;
1698 if (!target) {
1699 return this.privateModAction(`(${targetUser.name} would be muted by ${user.name} ${problem}.)`);
1700 }
1701 return this.addModAction(`${targetUser.name} would be muted by ${user.name} ${problem}. (${target})`);
1702 }
1703
1704 if (targetUser in room.users) targetUser.popup(`|modal|${user.name} has muted you in ${room.id} for ${Chat.toDurationString(muteDuration)}. ${target}`);
1705 this.addModAction(`${targetUser.name} was muted by ${user.name} for ${Chat.toDurationString(muteDuration)}.${(target ? ` (${target})` : ``)}`);
1706 this.modlog(`${cmd.includes('h') ? 'HOUR' : ''}MUTE`, targetUser, target);
1707 if (targetUser.autoconfirmed && targetUser.autoconfirmed !== targetUser.userid) {
1708 let displayMessage = `(${targetUser.name}'s ac account: ${targetUser.autoconfirmed})`;
1709 this.privateModAction(displayMessage);
1710 }
1711 let userid = targetUser.getLastId();
1712 this.add(`|unlink|${userid}`);
1713 if (userid !== toId(this.inputUsername)) this.add(`|unlink|${toId(this.inputUsername)}`);
1714
1715 room.mute(targetUser, muteDuration, false);
1716 },
1717 mutehelp: [`/mute OR /m [username], [reason] - Mutes a user with reason for 7 minutes. Requires: % @ * # & ~`],
1718
1719 hm: 'hourmute',
1720 hourmute: function (target) {
1721 if (!target) return this.parse('/help hourmute');
1722 this.run('mute');
1723 },
1724 hourmutehelp: [`/hourmute OR /hm [username], [reason] - Mutes a user with reason for an hour. Requires: % @ * # & ~`],
1725
1726 um: 'unmute',
1727 unmute: function (target, room, user) {
1728 if (!target) return this.parse('/help unmute');
1729 target = this.splitTarget(target);
1730 if (target) return this.errorReply(`This command does not support specifying a reason.`);
1731 if (!this.canTalk()) return;
1732 if (!this.can('mute', null, room)) return false;
1733
1734 let targetUser = this.targetUser;
1735 let successfullyUnmuted = room.unmute(targetUser ? targetUser.userid : toId(this.targetUsername), `Your mute in '${room.title}' has been lifted.`);
1736
1737 if (successfullyUnmuted) {
1738 this.addModAction(`${(targetUser ? targetUser.name : successfullyUnmuted)} was unmuted by ${user.name}.`);
1739 this.modlog('UNMUTE', (targetUser || successfullyUnmuted), null, {noip: 1, noalts: 1});
1740 } else {
1741 this.errorReply(`${(targetUser ? targetUser.name : this.targetUsername)} is not muted.`);
1742 }
1743 },
1744 unmutehelp: [`/unmute [username] - Removes mute from user. Requires: % @ * # & ~`],
1745
1746 forcelock: 'lock',
1747 l: 'lock',
1748 ipmute: 'lock',
1749 wl: 'lock',
1750 weeklock: 'lock',
1751 lock: function (target, room, user, connection, cmd) {
1752 let week = cmd === 'wl' || cmd === 'weeklock';
1753
1754 if (!target) {
1755 if (week) return this.parse('/help weeklock');
1756 return this.parse('/help lock');
1757 }
1758
1759 target = this.splitTarget(target);
1760 let targetUser = this.targetUser;
1761 if (!targetUser && !Punishments.search(toId(this.targetUsername))[0].length) return this.errorReply(`User '${this.targetUsername}' not found.`);
1762 if (target.length > MAX_REASON_LENGTH) {
1763 return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
1764 }
1765 if (!this.can('lock', targetUser)) return false;
1766
1767 let name, userid;
1768
1769 if (targetUser) {
1770 name = targetUser.getLastName();
1771 userid = targetUser.getLastId();
1772
1773 if (targetUser.locked && !week) {
1774 return this.privateModAction(`(${name} would be locked by ${user.name} but was already locked.)`);
1775 }
1776
1777 if (targetUser.trusted) {
1778 if (cmd === 'forcelock') {
1779 let from = targetUser.distrust();
1780 Monitor.log(`[CrisisMonitor] ${name} was locked by ${user.name} and demoted from ${from.join(", ")}.`);
1781 this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from.join(", ")}`);
1782 } else {
1783 return this.sendReply(`${name} is a trusted user. If you are sure you would like to lock them use /forcelock.`);
1784 }
1785 } else if (cmd === 'forcelock') {
1786 return this.errorReply(`Use /lock; ${name} is not a trusted user.`);
1787 }
1788
1789 let roomauth = [];
1790 Rooms.rooms.forEach((curRoom, id) => {
1791 if (id === 'global' || !curRoom.auth) return;
1792 // Destroy personal rooms of the locked user.
1793 if (curRoom.isPersonal && curRoom.auth[userid] === '#') {
1794 curRoom.destroy();
1795 } else {
1796 if (curRoom.isPrivate || curRoom.battle) return;
1797
1798 let group = curRoom.auth[userid];
1799
1800 if (group) roomauth.push(`${group}${id}`);
1801 }
1802 });
1803
1804 if (roomauth.length) Monitor.log(`[CrisisMonitor] Locked user ${name} has public roomauth (${roomauth.join(', ')}), and should probably be demoted.`);
1805 } else {
1806 name = this.targetUsername;
1807 userid = toId(this.targetUsername);
1808 }
1809
1810 let proof = '';
1811 let userReason = target;
1812 let targetLowercase = target.toLowerCase();
1813 if (target && (targetLowercase.includes('spoiler:') || targetLowercase.includes('spoilers:'))) {
1814 let proofIndex = (targetLowercase.includes('spoilers:') ? targetLowercase.indexOf('spoilers:') : targetLowercase.indexOf('spoiler:'));
1815 let bump = (targetLowercase.includes('spoilers:') ? 9 : 8);
1816 proof = `(PROOF: ${target.substr(proofIndex + bump, target.length).trim()}) `;
1817 userReason = target.substr(0, proofIndex).trim();
1818 }
1819
1820 let weekMsg = week ? ' for a week' : '';
1821
1822 if (targetUser) {
1823 let appeal = ``;
1824 if (Chat.pages.help) {
1825 appeal += `<a href="view-help-request--appeal"><button class="button"><strong>Appeal your punishment</strong></button></a>`;
1826 } else if (Config.appealurl) {
1827 appeal += `appeal: <a href="${Config.appealurl}">${Config.appealurl}</a>`;
1828 }
1829 targetUser.send(`|popup||html||modal|${user.name} has locked you from talking in chats, battles, and PMing regular users${weekMsg}.${(userReason ? `\n\nReason: ${userReason}` : "")}\n\nIf you feel that your lock was unjustified, you can ${appeal}.\n\nYour lock will expire in a few days.`);
1830 }
1831
1832 let lockMessage = `${name} was locked from talking${weekMsg} by ${user.name}.` + (userReason ? ` (${userReason})` : "");
1833
1834 this.addModAction(lockMessage);
1835
1836 // Notify staff room when a user is locked outside of it.
1837 if (room.id !== 'staff' && Rooms('staff')) {
1838 Rooms('staff').addByUser(user, `<<${room.id}>> ${lockMessage}`);
1839 }
1840
1841 // Use default time for locks.
1842 let duration = week ? Date.now() + 7 * 24 * 60 * 60 * 1000 : null;
1843 let affected = [];
1844
1845 if (targetUser) {
1846 affected = Punishments.lock(targetUser, duration, null, userReason);
1847 } else {
1848 affected = Punishments.lock(null, duration, userid, userReason);
1849 }
1850
1851 let acAccount = (targetUser && targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
1852 let displayMessage = '';
1853 if (affected.length > 1) {
1854 displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount}, ` : "")} locked alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
1855 this.privateModAction(displayMessage);
1856 } else if (acAccount) {
1857 displayMessage = `(${name}'s ac account: ${acAccount})`;
1858 this.privateModAction(displayMessage);
1859 }
1860 room.hideText([userid, toId(this.inputUsername)]);
1861
1862 const globalReason = (target ? `: ${userReason} ${proof}` : '');
1863 this.globalModlog((week ? "WEEKLOCK" : "LOCK"), targetUser || userid, ` by ${user.userid}${globalReason}`);
1864
1865 // Automatically upload replays as evidence/reference to the punishment
1866 if (room.battle) this.parse('/savereplay');
1867 return true;
1868 },
1869 lockhelp: [
1870 `/lock OR /l [username], [reason] - Locks the user from talking in all chats. Requires: % @ * & ~`,
1871 `/weeklock OR /wl [username], [reason] - Same as /lock, but locks users for a week.`,
1872 `/lock OR /l [username], [reason] spoiler: [proof] - Marks proof in modlog only.`,
1873 ],
1874
1875 unlock: function (target, room, user) {
1876 if (!target) return this.parse('/help unlock');
1877 if (!this.can('lock')) return false;
1878
1879 let targetUser = Users.get(target);
1880 if (targetUser && targetUser.namelocked) {
1881 return this.errorReply(`User ${targetUser.name} is namelocked, not locked. Use /unnamelock to unnamelock them.`);
1882 }
1883 let reason = '';
1884 if (targetUser && targetUser.locked && targetUser.locked.charAt(0) === '#') {
1885 reason = ` (${targetUser.locked})`;
1886 }
1887
1888 let unlocked = Punishments.unlock(target);
1889
1890 if (unlocked) {
1891 const unlockMessage = `${unlocked.join(", ")} ${((unlocked.length > 1) ? "were" : "was")} unlocked by ${user.name}.${reason}`;
1892 this.addModAction(unlockMessage);
1893 // Notify staff room when a user is unlocked outside of it.
1894 if (!reason && room.id !== 'staff' && Rooms('staff')) {
1895 Rooms('staff').addByUser(user, `<<${room.id}>> ${unlockMessage}`);
1896 }
1897 if (!reason) this.globalModlog("UNLOCK", toId(target), ` by ${user.userid}`);
1898 if (targetUser) targetUser.popup(`${user.name} has unlocked you.`);
1899 } else {
1900 this.errorReply(`User '${target}' is not locked.`);
1901 }
1902 },
1903 unlockname: function (target, room, user) {
1904 if (!target) return this.parse('/help unlock');
1905 if (!this.can('lock')) return false;
1906
1907 const userid = toId(target);
1908 const punishment = Punishments.userids.get(userid);
1909 if (!punishment) return this.errorReply("This name isn't locked.");
1910 if (punishment[1] === userid) return this.errorReply(`"${userid}" was specifically locked by a staff member (check the global modlog). Use /unlock if you really want to unlock this name.`);
1911
1912 Punishments.userids.delete(userid);
1913 Punishments.savePunishments();
1914
1915 for (const curUser of Users.findUsers([userid], [])) {
1916 if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) {
1917 curUser.locked = false;
1918 curUser.namelocked = false;
1919 curUser.updateIdentity();
1920 }
1921 }
1922 this.globalModlog("UNLOCKNAME", userid, ` by ${user.name}`);
1923
1924 const unlockMessage = `The name '${target}' was unlocked by ${user.name}.`;
1925
1926 this.addModAction(unlockMessage);
1927 if (room.id !== 'staff' && Rooms('staff')) {
1928 Rooms('staff').addByUser(user, `<<${room.id}>> ${unlockMessage}`);
1929 }
1930 },
1931 unlockip: function (target, room, user) {
1932 target = target.trim();
1933 if (!target) return this.parse('/help unlock');
1934 if (!this.can('ban')) return false;
1935 const range = target.charAt(target.length - 1) === '*';
1936 if (range && !this.can('rangeban')) return false;
1937
1938 if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address.");
1939
1940 const punishment = Punishments.ips.get(target);
1941 if (!punishment) return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
1942
1943 Punishments.ips.delete(target);
1944 Punishments.savePunishments();
1945
1946 for (const curUser of Users.findUsers([], [target])) {
1947 if (curUser.locked && !curUser.locked.startsWith('#') && !Punishments.getPunishType(curUser.userid)) {
1948 curUser.locked = false;
1949 curUser.namelocked = false;
1950 curUser.updateIdentity();
1951 }
1952 }
1953 this.globalModlog(`UNLOCK${range ? 'RANGE' : 'IP'}`, target, ` by ${user.name}`);
1954
1955 const broadcastRoom = Rooms('staff') || room;
1956 broadcastRoom.addByUser(user, `${user.name} unlocked the ${range ? "IP range" : "IP"}: ${target}`);
1957 },
1958 unlockhelp: [
1959 `/unlock [username] - Unlocks the user. Requires: % @ * & ~`,
1960 `/unlockname [username] - Unlocks a punished alt while leaving the original punishment intact. Requires: % @ * & ~`,
1961 `/unlockip [ip] - Unlocks a punished ip while leaving the original punishment intact. Requires: @ * & ~`,
1962 ],
1963
1964 forceglobalban: 'globalban',
1965 gban: 'globalban',
1966 globalban: function (target, room, user, connection, cmd) {
1967 if (!target) return this.parse('/help globalban');
1968
1969 target = this.splitTarget(target);
1970 let targetUser = this.targetUser;
1971 if (!targetUser) return this.errorReply(`User '${this.targetUsername}' not found.`);
1972 if (target.length > MAX_REASON_LENGTH) {
1973 return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
1974 }
1975 if (!target && REQUIRE_REASONS) {
1976 return this.errorReply("Global bans require a reason.");
1977 }
1978 if (!this.can('ban', targetUser)) return false;
1979 let name = targetUser.getLastName();
1980 let userid = targetUser.getLastId();
1981
1982 if (targetUser.trusted) {
1983 if (cmd === 'forceglobalban') {
1984 let from = targetUser.distrust();
1985 Monitor.log(`[CrisisMonitor] ${name} was globally banned by ${user.name} and demoted from ${from.join(", ")}.`);
1986 this.globalModlog("CRISISDEMOTE", targetUser, ` from ${from.join(", ")}`);
1987 } else {
1988 return this.sendReply(`${name} is a trusted user. If you are sure you would like to ban them use /forceglobalban.`);
1989 }
1990 } else if (cmd === 'forceglobalban') {
1991 return this.errorReply(`Use /globalban; ${name} is not a trusted user.`);
1992 }
1993
1994 // Destroy personal rooms of the banned user.
1995 for (const roomid of targetUser.inRooms) {
1996 if (roomid === 'global') continue;
1997 let targetRoom = Rooms.get(roomid);
1998 if (targetRoom.isPersonal && targetRoom.auth[userid] === '#') {
1999 targetRoom.destroy();
2000 }
2001 }
2002
2003 let proof = '';
2004 let userReason = target;
2005 let targetLowercase = target.toLowerCase();
2006 if (target && (targetLowercase.includes('spoiler:') || targetLowercase.includes('spoilers:'))) {
2007 let proofIndex = (targetLowercase.includes('spoilers:') ? targetLowercase.indexOf('spoilers:') : targetLowercase.indexOf('spoiler:'));
2008 let bump = (targetLowercase.includes('spoilers:') ? 9 : 8);
2009 proof = `(PROOF: ${target.substr(proofIndex + bump, target.length).trim()}) `;
2010 userReason = target.substr(0, proofIndex).trim();
2011 }
2012
2013 targetUser.popup(`|modal|${user.name} has globally banned you.${(userReason ? `\n\nReason: ${userReason}` : ``)} ${(Config.appealurl ? `\n\nIf you feel that your ban was unjustified, you can appeal:\n${Config.appealurl}` : ``)}\n\nYour ban will expire in a few days.`);
2014
2015 let banMessage = `${name} was globally banned by ${user.name}.${(userReason ? ` (${userReason})` : ``)}`;
2016 this.addModAction(banMessage);
2017
2018 // Notify staff room when a user is banned outside of it.
2019 if (room.id !== 'staff' && Rooms('staff')) {
2020 Rooms('staff').addByUser(user, `<<${room.id}>> ${banMessage}`);
2021 }
2022
2023 let affected = Punishments.ban(targetUser, null, null, userReason);
2024 let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
2025 let displayMessage = '';
2026 if (affected.length > 1) {
2027 let guests = affected.length - 1;
2028 affected = affected.slice(1).map(user => user.getLastName()).filter(alt => alt.substr(0, 7) !== '[Guest ');
2029 guests -= affected.length;
2030 displayMessage = `(${name}'s ${(acAccount ? `ac account: ${acAccount}, ` : ``)} banned alts: ${affected.join(", ")} ${(guests ? ` [${guests} guests]` : ``)})`;
2031 this.privateModAction(displayMessage);
2032 for (const user of affected) {
2033 this.add(`|unlink|${toId(user)}`);
2034 }
2035 } else if (acAccount) {
2036 displayMessage = `(${name}'s ac account: ${acAccount})`;
2037 this.privateModAction(displayMessage);
2038 }
2039
2040 room.hideText([userid, toId(this.inputUsername)]);
2041
2042 const globalReason = (target ? `: ${userReason} ${proof}` : '');
2043 this.globalModlog("BAN", targetUser, ` by ${user.userid}${globalReason}`);
2044 return true;
2045 },
2046 globalbanhelp: [
2047 `/globalban OR /gban [username], [reason] - Kick user from all rooms and ban user's IP address with reason. Requires: @ * & ~`,
2048 `/globalban OR /gban [username], [reason] spoiler: [proof] - Marks proof in modlog only.`,
2049 ],
2050
2051 globalunban: 'unglobalban',
2052 unglobalban: function (target, room, user) {
2053 if (!target) return this.parse(`/help unglobalban`);
2054 if (!this.can('ban')) return false;
2055
2056 let name = Punishments.unban(target);
2057
2058 let unbanMessage = `${name} was globally unbanned by ${user.name}.`;
2059
2060 if (name) {
2061 this.addModAction(unbanMessage);
2062 // Notify staff room when a user is unbanned outside of it.
2063 if (room.id !== 'staff' && Rooms('staff')) {
2064 Rooms('staff').addByUser(user, `<<${room.id}>> ${unbanMessage}`);
2065 }
2066 this.globalModlog("UNBAN", name, ` by ${user.userid}`);
2067 } else {
2068 this.errorReply(`User '${target}' is not globally banned.`);
2069 }
2070 },
2071 unglobalbanhelp: [`/unglobalban [username] - Unban a user. Requires: @ * & ~`],
2072
2073 unbanall: function (target, room, user) {
2074 if (!this.can('rangeban')) return false;
2075 if (!target) {
2076 user.lastCommand = '/unbanall';
2077 this.errorReply("THIS WILL UNBAN AND UNLOCK ALL USERS.");
2078 this.errorReply("To confirm, use: /unbanall confirm");
2079 return;
2080 }
2081 if (user.lastCommand !== '/unbanall' || target !== 'confirm') {
2082 return this.parse('/help unbanall');
2083 }
2084 user.lastCommand = '';
2085 Punishments.userids.clear();
2086 Punishments.ips.clear();
2087 Punishments.savePunishments();
2088 this.addModAction(`All bans and locks have been lifted by ${user.name}.`);
2089 this.modlog('UNBANALL');
2090 },
2091 unbanallhelp: [`/unbanall - Unban all IP addresses. Requires: & ~`],
2092
2093 deroomvoiceall: function (target, room, user) {
2094 if (!this.can('editroom', null, room)) return false;
2095 if (!room.auth) return this.errorReply("Room does not have roomauth.");
2096 if (!target) {
2097 user.lastCommand = '/deroomvoiceall';
2098 this.errorReply("THIS WILL DEROOMVOICE ALL ROOMVOICED USERS.");
2099 this.errorReply("To confirm, use: /deroomvoiceall confirm");
2100 return;
2101 }
2102 if (user.lastCommand !== '/deroomvoiceall' || target !== 'confirm') {
2103 return this.parse('/help deroomvoiceall');
2104 }
2105 user.lastCommand = '';
2106 let count = 0;
2107 for (let userid in room.auth) {
2108 if (room.auth[userid] === '+') {
2109 delete room.auth[userid];
2110 if (userid in room.users) room.users[userid].updateIdentity(room.id);
2111 count++;
2112 }
2113 }
2114 if (!count) {
2115 return this.sendReply("(This room has zero roomvoices)");
2116 }
2117 if (room.chatRoomData) {
2118 Rooms.global.writeChatRoomData();
2119 }
2120 this.addModAction(`All ${count} roomvoices have been cleared by ${user.name}.`);
2121 this.modlog('DEROOMVOICEALL');
2122 },
2123 deroomvoiceallhelp: [`/deroomvoiceall - Devoice all roomvoiced users. Requires: # & ~`],
2124
2125 rangeban: 'banip',
2126 banip: function (target, room, user) {
2127 const [ip, reason] = this.splitOne(target);
2128 if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip)) return this.parse('/help banip');
2129 if (!reason) return this.errorReply("/banip requires a ban reason");
2130
2131 if (!this.can('rangeban')) return false;
2132 const ipDesc = `IP ${(ip.endsWith('*') ? `range ` : ``)}${ip}`;
2133
2134 const curPunishment = Punishments.ipSearch(ip);
2135 if (curPunishment && curPunishment[0] === 'BAN') {
2136 return this.errorReply(`The ${ipDesc} is already temporarily banned.`);
2137 }
2138 Punishments.banRange(ip, reason);
2139 this.addModAction(`${user.name} hour-banned the ${ipDesc}: ${reason}`);
2140 this.modlog('RANGEBAN', null, reason);
2141 },
2142 baniphelp: [`/banip [ip] - Globally bans this IP or IP range for an hour. Accepts wildcards to ban ranges. Existing users on the IP will not be banned. Requires: & ~`],
2143
2144 unrangeban: 'unbanip',
2145 unbanip: function (target, room, user) {
2146 target = target.trim();
2147 if (!target) {
2148 return this.parse('/help unbanip');
2149 }
2150 if (!this.can('rangeban')) return false;
2151 if (!Punishments.ips.has(target)) {
2152 return this.errorReply(`${target} is not a locked/banned IP or IP range.`);
2153 }
2154 Punishments.ips.delete(target);
2155 this.addModAction(`${user.name} unbanned the ${(target.charAt(target.length - 1) === '*' ? "IP range" : "IP")}: ${target}`);
2156 this.modlog('UNRANGEBAN', null, target);
2157 },
2158 unbaniphelp: [`/unbanip [ip] - Unbans. Accepts wildcards to ban ranges. Requires: & ~`],
2159
2160 rangelock: 'lockip',
2161 lockip: function (target, room, user) {
2162 const [ip, reason] = this.splitOne(target);
2163 if (!ip || !/^[0-9.]+(?:\.\*)?$/.test(ip)) return this.parse('/help lockip');
2164 if (!reason) return this.errorReply("/lockip requires a lock reason");
2165
2166 if (!this.can('rangeban')) return false;
2167 const ipDesc = `IP ${(ip.endsWith('*') ? `range ` : ``)}${ip}`;
2168
2169 const curPunishment = Punishments.ipSearch(ip);
2170 if (curPunishment && (curPunishment[0] === 'BAN' || curPunishment[0] === 'LOCK')) {
2171 const punishDesc = curPunishment[0] === 'BAN' ? `temporarily banned` : `temporarily locked`;
2172 return this.errorReply(`The ${ipDesc} is already ${punishDesc}.`);
2173 }
2174
2175 Punishments.lockRange(ip, reason);
2176 this.addModAction(`${user.name} hour-locked the ${ipDesc}: ${reason}`);
2177 this.modlog('RANGELOCK', null, reason);
2178 },
2179 lockiphelp: [`/lockip [ip] - Globally locks this IP or IP range for an hour. Accepts wildcards to ban ranges. Existing users on the IP will not be banned. Requires: & ~`],
2180
2181 unrangelock: 'unlockip',
2182 rangeunlock: 'unlockip',
2183
2184 /*********************************************************
2185 * Moderating: Other
2186 *********************************************************/
2187
2188 mn: 'modnote',
2189 modnote: function (target, room, user, connection) {
2190 if (!target) return this.parse('/help modnote');
2191 if (!this.canTalk()) return;
2192
2193 if (target.length > MAX_REASON_LENGTH) {
2194 return this.errorReply(`The note is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
2195 }
2196 if (!this.can('receiveauthmessages', null, room)) return false;
2197 this.modlog('NOTE', null, target);
2198 return this.privateModAction(`(${user.name} notes: ${target})`);
2199 },
2200 modnotehelp: [`/modnote [note] - Adds a moderator note that can be read through modlog. Requires: % @ * # & ~`],
2201
2202 globalpromote: 'promote',
2203 promote: function (target, room, user, connection, cmd) {
2204 if (!target) return this.parse('/help promote');
2205
2206 target = this.splitTarget(target, true);
2207 let targetUser = this.targetUser;
2208 let userid = toId(this.targetUsername);
2209 let name = targetUser ? targetUser.name : this.targetUsername;
2210
2211 if (!userid) return this.parse('/help promote');
2212
2213 let currentGroup = ((targetUser && targetUser.group) || Users.usergroups[userid] || ' ')[0];
2214 let nextGroup = target;
2215 if (target === 'deauth') nextGroup = Config.groupsranking[0];
2216 if (!nextGroup) {
2217 return this.errorReply("Please specify a group such as /globalvoice or /globaldeauth");
2218 }
2219 if (!Config.groups[nextGroup]) {
2220 return this.errorReply(`Group '${nextGroup}' does not exist.`);
2221 }
2222 if (!cmd.startsWith('global')) {
2223 let groupid = Config.groups[nextGroup].id;
2224 if (!groupid && nextGroup === Config.groupsranking[0]) groupid = 'deauth';
2225 if (Config.groups[nextGroup].globalonly) return this.errorReply(`Did you mean "/global${groupid}"?`);
2226 if (Config.groups[nextGroup].roomonly) return this.errorReply(`Did you mean "/room${groupid}"?`);
2227 return this.errorReply(`Did you mean "/room${groupid}" or "/global${groupid}"?`);
2228 }
2229 if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) {
2230 return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
2231 }
2232
2233 let groupName = Config.groups[nextGroup].name || "regular user";
2234 if (currentGroup === nextGroup) {
2235 return this.errorReply(`User '${name}' is already a ${groupName}`);
2236 }
2237 if (!user.canPromote(currentGroup, nextGroup)) {
2238 return this.errorReply(`/${cmd} - Access denied.`);
2239 }
2240
2241 if (!Users.isUsernameKnown(userid)) {
2242 return this.errorReply(`/globalpromote - WARNING: '${name}' is offline and unrecognized. The username might be misspelled (either by you or the person who told you) or unregistered. Use /forcepromote if you're sure you want to risk it.`);
2243 }
2244 if (targetUser && !targetUser.registered) {
2245 return this.errorReply(`User '${name}' is unregistered, and so can't be promoted.`);
2246 }
2247 Users.setOfflineGroup(name, nextGroup);
2248 if (Config.groups[nextGroup].rank < Config.groups[currentGroup].rank) {
2249 this.privateModAction(`(${name} was demoted to ${groupName} by ${user.name}.)`);
2250 this.modlog(`GLOBAL ${groupName.toUpperCase()}`, userid, '(demote)');
2251 if (targetUser) targetUser.popup(`You were demoted to ${groupName} by ${user.name}.`);
2252 } else {
2253 this.addModAction(`${name} was promoted to ${groupName} by ${user.name}.`);
2254 this.modlog(`GLOBAL ${groupName.toUpperCase()}`, userid);
2255 if (targetUser) targetUser.popup(`You were promoted to ${groupName} by ${user.name}.`);
2256 }
2257
2258 if (targetUser) targetUser.updateIdentity();
2259 },
2260 promotehelp: [`/promote [username], [group] - Promotes the user to the specified group. Requires: & ~`],
2261
2262 confirmuser: 'trustuser',
2263 trustuser: function (target, room, user) {
2264 if (!target) return this.parse('/help trustuser');
2265 if (!this.can('promote')) return;
2266
2267 target = this.splitTarget(target, true);
2268 if (target) return this.errorReply(`This command does not support specifying a reason.`);
2269 let targetUser = this.targetUser;
2270 let userid = toId(this.targetUsername);
2271 let name = targetUser ? targetUser.name : this.targetUsername;
2272
2273 if (!userid) return this.parse('/help trustuser');
2274 if (!targetUser) return this.errorReply(`User '${name}' is not online.`);
2275
2276 if (targetUser.trusted) return this.errorReply(`User '${name}' is already trusted.`);
2277
2278 targetUser.setGroup(Config.groupsranking[0], true);
2279 this.sendReply(`User '${name}' is now trusted.`);
2280 this.privateModAction(`${name} was set as a trusted user by ${user.name}.`);
2281 this.modlog('TRUSTUSER', userid);
2282 },
2283 trustuserhelp: [`/trustuser [username] - Trusts the user (makes them immune to locks). Requires: & ~`],
2284
2285 globaldemote: 'demote',
2286 demote: function (target) {
2287 if (!target) return this.parse('/help demote');
2288 this.run('promote');
2289 },
2290 demotehelp: [`/demote [username], [group] - Demotes the user to the specified group. Requires: & ~`],
2291
2292 forcepromote: function (target, room, user) {
2293 // warning: never document this command in /help
2294 if (!this.can('forcepromote')) return false;
2295 target = this.splitTarget(target, true);
2296 let name = this.targetUsername;
2297 let nextGroup = target;
2298 if (!Config.groups[nextGroup]) return this.errorReply(`Group '${nextGroup}' does not exist.`);
2299 if (Config.groups[nextGroup].roomonly || Config.groups[nextGroup].battleonly) return this.errorReply(`Group '${nextGroup}' does not exist as a global rank.`);
2300
2301 if (Users.isUsernameKnown(name)) {
2302 return this.errorReply("/forcepromote - Don't forcepromote unless you have to.");
2303 }
2304 Users.setOfflineGroup(name, nextGroup);
2305
2306 this.addModAction(`${name} was promoted to ${(Config.groups[nextGroup].name || "regular user")} by ${user.name}.`);
2307 this.modlog(`GLOBAL${(Config.groups[nextGroup].name || "regular").toUpperCase()}`, toId(name));
2308 },
2309
2310 devoice: 'deauth',
2311 deauth: function (target, room, user) {
2312 return this.parse(`/demote ${target}, deauth`);
2313 },
2314
2315 deglobalvoice: 'globaldeauth',
2316 deglobalauth: 'globaldeauth',
2317 globaldevoice: 'globaldeauth',
2318 globaldeauth: function (target, room, user) {
2319 return this.parse(`/globaldemote ${target}, deauth`);
2320 },
2321
2322 deroomvoice: 'roomdeauth',
2323 roomdevoice: 'roomdeauth',
2324 deroomauth: 'roomdeauth',
2325 roomdeauth: function (target, room, user) {
2326 return this.parse(`/roomdemote ${target}, deauth`);
2327 },
2328
2329 declare: function (target, room, user) {
2330 target = target.trim();
2331 if (!target) return this.parse('/help declare');
2332 if (!this.can('declare', null, room)) return false;
2333 if (!this.canTalk()) return;
2334 if (target.length > 2000) return this.errorReply("Declares should not exceed 2000 characters.");
2335
2336 for (let u in room.users) {
2337 if (Users(u).connected) Users(u).sendTo(room, `|notify|${room.title} announcement!|${target}`);
2338 }
2339 this.add(Chat.html`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
2340 this.modlog('DECLARE', null, target);
2341 },
2342 declarehelp: [`/declare [message] - Anonymously announces a message. Requires: # * & ~`],
2343
2344 htmldeclare: function (target, room, user) {
2345 if (!target) return this.parse('/help htmldeclare');
2346 if (!this.can('gdeclare', null, room)) return false;
2347 if (!this.canTalk()) return;
2348 target = this.canHTML(target);
2349 if (!target) return;
2350
2351 for (let u in room.users) {
2352 if (Users(u).connected) Users(u).sendTo(room, `|notify|${room.title} announcement!|${Chat.stripHTML(target)}`);
2353 }
2354 this.add(`|raw|<div class="broadcast-blue"><b>${target}</b></div>`);
2355 this.modlog(`HTMLDECLARE`, null, target);
2356 },
2357 htmldeclarehelp: [`/htmldeclare [message] - Anonymously announces a message using safe HTML. Requires: ~`],
2358
2359 gdeclare: 'globaldeclare',
2360 globaldeclare: function (target, room, user) {
2361 if (!target) return this.parse('/help globaldeclare');
2362 if (!this.can('gdeclare')) return false;
2363 target = this.canHTML(target);
2364 if (!target) return;
2365
2366 Users.users.forEach(u => {
2367 if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw <div class="broadcast-blue"><b>${target}</b></div>`);
2368 });
2369 this.modlog(`GLOBALDECLARE`, null, target);
2370 },
2371 globaldeclarehelp: [`/globaldeclare [message] - Anonymously announces a message to every room on the server. Requires: ~`],
2372
2373 cdeclare: 'chatdeclare',
2374 chatdeclare: function (target, room, user) {
2375 if (!target) return this.parse('/help chatdeclare');
2376 if (!this.can('gdeclare')) return false;
2377 target = this.canHTML(target);
2378 if (!target) return;
2379
2380 Rooms.rooms.forEach((curRoom, id) => {
2381 if (id !== 'global' && curRoom.type !== 'battle') curRoom.addRaw(`<div class="broadcast-blue"><b>${target}</b></div>`).update();
2382 });
2383 this.modlog(`CHATDECLARE`, null, target);
2384 },
2385 chatdeclarehelp: [`/cdeclare [message] - Anonymously announces a message to all chatrooms on the server. Requires: ~`],
2386
2387 '!announce': true,
2388 wall: 'announce',
2389 announce: function (target, room, user) {
2390 if (!target) return this.parse('/help announce');
2391
2392 if (room && !this.can('announce', null, room)) return false;
2393
2394 target = this.canTalk(target);
2395 if (!target) return;
2396
2397 return `/announce ${target}`;
2398 },
2399 announcehelp: [`/announce OR /wall [message] - Makes an announcement. Requires: % @ * # & ~`],
2400
2401 notifyoffrank: 'notifyrank',
2402 notifyrank: function (target, room, user, connection, cmd) {
2403 if (!target) return this.parse(`/help notifyrank`);
2404 if (!this.can('addhtml', null, room)) return false;
2405 if (!this.canTalk()) return;
2406 let [rank, titleNotification] = this.splitOne(target);
2407 if (rank === 'all') rank = ` `;
2408 if (!(rank in Config.groups)) return this.errorReply(`Group '${rank}' does not exist.`);
2409 const id = `${room.id}-rank-${(Config.groups[rank].id || `all`)}`;
2410 if (cmd === 'notifyoffrank') {
2411 if (rank === ' ') {
2412 room.send(`|tempnotifyoff|${id}`);
2413 } else {
2414 room.sendRankedUsers(`|tempnotifyoff|${id}`, rank);
2415 }
2416 } else {
2417 let [title, notificationHighlight] = this.splitOne(titleNotification);
2418 if (!title) title = `${room.title} ${(Config.groups[rank].name ? `${Config.groups[rank].name}+ ` : ``)}message!`;
2419 if (!user.can('addhtml')) {
2420 title += ` (notification from ${user.name})`;
2421 }
2422 const [notification, highlight] = this.splitOne(notificationHighlight);
2423 if (notification.length > 300) return this.errorReply(`Notifications should not exceed 300 characters.`);
2424 const message = `|tempnotify|${id}|${title}|${notification}${(highlight ? `|${highlight}` : ``)}`;
2425 if (rank === ' ') {
2426 room.send(message);
2427 } else {
2428 room.sendRankedUsers(message, rank);
2429 }
2430 this.modlog(`NOTIFYRANK`, null, target);
2431 }
2432 },
2433 notifyrankhelp: [
2434 `/notifyrank [rank], [title], [message], [highlight] - Sends a notification to users who are [rank] or higher (and highlight on [highlight], if specified). Requires: # * & ~`,
2435 `/notifyoffrank [rank] - Closes the notification previously sent with /notifyrank [rank]. Requires: # * & ~`,
2436 ],
2437
2438 fr: 'forcerename',
2439 forcerename: function (target, room, user) {
2440 if (!target) return this.parse('/help forcerename');
2441
2442 let reason = this.splitTarget(target, true);
2443 let targetUser = this.targetUser;
2444 if (!targetUser) {
2445 this.splitTarget(target);
2446 if (this.targetUser) {
2447 return this.errorReply(`User has already changed their name to '${this.targetUser.name}'.`);
2448 }
2449 return this.errorReply(`User '${target}' not found.`);
2450 }
2451 if (!this.can('forcerename', targetUser)) return false;
2452
2453 let entry = `${targetUser.name} was forced to choose a new name by ${user.name}${(reason ? `: ${reason}` : ``)}`;
2454 this.privateModAction(`(${entry})`);
2455 this.modlog('FORCERENAME', targetUser, reason, {noip: 1, noalts: 1});
2456 Ladders.cancelSearches(targetUser);
2457 targetUser.resetName(true);
2458 targetUser.send(`|nametaken||${user.name} considers your name inappropriate${(reason ? `: ${reason}` : ".")}`);
2459 targetUser.trackRename = targetUser.name;
2460 return true;
2461 },
2462 forcerenamehelp: [`/forcerename OR /fr [username], [reason] - Forcibly change a user's name and shows them the [reason]. Requires: % @ * & ~`],
2463
2464 nl: 'namelock',
2465 namelock: function (target, room, user) {
2466 if (!target) return this.parse('/help namelock');
2467
2468 let reason = this.splitTarget(target, true);
2469 let targetUser = this.targetUser;
2470
2471 if (!targetUser) {
2472 return this.errorReply(`User '${this.targetUsername}' not found.`);
2473 }
2474 if (!this.can('forcerename', targetUser)) return false;
2475 if (targetUser.namelocked) return this.errorReply(`User '${targetUser.name}' is already namelocked.`);
2476
2477 const reasonText = reason ? ` (${reason})` : `.`;
2478 const lockMessage = `${targetUser.name} was namelocked by ${user.name}${reasonText}`;
2479 this.privateModAction(`(${lockMessage})`);
2480
2481 // Notify staff room when a user is locked outside of it.
2482 if (room.id !== 'staff' && Rooms('staff')) {
2483 Rooms('staff').addByUser(user, `<<${room.id}>> ${lockMessage}`);
2484 }
2485
2486 this.globalModlog("NAMELOCK", targetUser, ` by ${user.userid}${reasonText}`);
2487 Ladders.cancelSearches(targetUser);
2488 Punishments.namelock(targetUser, null, null, reason);
2489 targetUser.popup(`|modal|${user.name} has locked your name and you can't change names anymore${reasonText}`);
2490 return true;
2491 },
2492 namelockhelp: [`/namelock OR /nl [username], [reason] - Name locks a user and shows them the [reason]. Requires: % @ * & ~`],
2493
2494 unl: 'unnamelock',
2495 unnamelock: function (target, room, user) {
2496 if (!target) return this.parse('/help unnamelock');
2497 if (!this.can('forcerename')) return false;
2498
2499 let targetUser = Users.get(target);
2500 let reason = '';
2501 if (targetUser && targetUser.namelocked) {
2502 reason = ` (${targetUser.namelocked})`;
2503 }
2504
2505 let unlocked = Punishments.unnamelock(target);
2506
2507 if (unlocked) {
2508 this.addModAction(`${unlocked} was unnamelocked by ${user.name}.${reason}`);
2509 if (!reason) this.globalModlog("UNNAMELOCK", toId(target), ` by ${user.userid}`);
2510 if (targetUser) targetUser.popup(`${user.name} has unnamelocked you.`);
2511 } else {
2512 this.errorReply(`User '${target}' is not namelocked.`);
2513 }
2514 },
2515 unnamelockhelp: [`/unnamelock [username] - Unnamelocks the user. Requires: % @ * & ~`],
2516
2517 hidetextalts: 'hidetext',
2518 hidealttext: 'hidetext',
2519 hidealtstext: 'hidetext',
2520 hidetext: function (target, room, user, connection, cmd) {
2521 if (!target) return this.parse(`/help hidetext`);
2522
2523 this.splitTarget(target);
2524 let targetUser = this.targetUser;
2525 let name = this.targetUsername;
2526 if (!targetUser && !room.log.hasUsername(target)) return this.errorReply(`User ${target} not found or has no roomlogs.`);
2527 if (!targetUser && !user.can('lock')) return this.errorReply(`User ${name} not found.`);
2528 let userid = toId(this.inputUsername);
2529 if (!user.can('mute', targetUser, room) && !this.can('ban', targetUser, room)) return;
2530
2531 const localPunished = (targetUser && (targetUser.locked || Punishments.isRoomBanned(targetUser, room.id) || room.isMuted(targetUser)));
2532 if (!(user.can('lock') || localPunished)) return this.errorReply(`User ${name} is neither locked nor muted/banned from this room.`);
2533
2534 if (targetUser && (cmd === 'hidealtstext' || cmd === 'hidetextalts' || cmd === 'hidealttext')) {
2535 room.sendByUser(user, `${name}'s alts messages were cleared from ${room.title} by ${user.name}.`);
2536
2537 this.modlog('HIDEALTSTEXT', targetUser, null, {noip: 1});
2538 room.hideText([
2539 userid,
2540 ...Object.keys(targetUser.prevNames),
2541 ...targetUser.getAltUsers(true).map(user => user.getLastId()),
2542 ]);
2543 } else {
2544 room.sendByUser(user, `${name}'s messages were cleared from ${room.title} by ${user.name}.`);
2545 this.modlog('HIDETEXT', targetUser || userid, null, {noip: 1, noalts: 1});
2546 room.hideText([userid]);
2547 }
2548 },
2549 hidetexthelp: [
2550 `/hidetext [username] - Removes a locked or muted/banned user's messages from chat. Requires: % @ * # & ~`,
2551 `/hidealtstext [username] - Removes a locked or muted/banned user's messages, and their alternate account's messages from the chat. Requires: % @ * # & ~`,
2552 ],
2553
2554 ab: 'blacklist',
2555 blacklist: function (target, room, user) {
2556 if (!target) return this.parse('/help blacklist');
2557 if (!this.canTalk()) return;
2558 if (toId(target) === 'show') return this.errorReply(`You're looking for /showbl`);
2559
2560 target = this.splitTarget(target);
2561 const targetUser = this.targetUser;
2562 if (!targetUser) {
2563 this.errorReply(`User ${this.targetUsername} not found.`);
2564 return this.errorReply(`If you want to blacklist an offline account by name (not IP), consider /blacklistname`);
2565 }
2566 if (!this.can('editroom', targetUser, room)) return false;
2567 if (!room.chatRoomData) {
2568 return this.errorReply(`This room is not going to last long enough for a blacklist to matter - just ban the user`);
2569 }
2570 let punishment = Punishments.isRoomBanned(targetUser, room.id);
2571 if (punishment && punishment[0] === 'BLACKLIST') {
2572 return this.errorReply(`This user is already blacklisted from this room.`);
2573 }
2574
2575 if (!target && REQUIRE_REASONS) {
2576 return this.errorReply(`Blacklists require a reason.`);
2577 }
2578 if (target.length > MAX_REASON_LENGTH) {
2579 return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
2580 }
2581 const name = targetUser.getLastName();
2582 const userid = targetUser.getLastId();
2583
2584 if (targetUser.trusted && room.isPrivate !== true) {
2585 Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name}${(targetUser.trusted !== targetUser.userid ? ` (${targetUser.trusted})` : '')} was blacklisted from ${room.id} by ${user.name}, and should probably be demoted.`);
2586 }
2587
2588 if (targetUser in room.users || user.can('lock')) {
2589 targetUser.popup(
2590 `|modal||html|<p>${Chat.escapeHTML(user.name)} has blacklisted you from the room ${room.id}${(room.subRooms ? ` and its subrooms` : '')}. Reason: ${Chat.escapeHTML(target)}</p>` +
2591 `<p>To appeal the ban, PM the staff member that blacklisted you${(!room.battle && room.auth ? ` or a room owner. </p><p><button name="send" value="/roomauth ${room.id}">List Room Staff</button></p>` : `.</p>`)}`
2592 );
2593 }
2594
2595 this.privateModAction(`(${name} was blacklisted from ${room.title} by ${user.name}. ${(target ? ` (${target})` : '')})`);
2596
2597 let affected = Punishments.roomBlacklist(room, targetUser, null, null, target);
2598
2599 if (!room.isPrivate && room.chatRoomData) {
2600 let acAccount = (targetUser.autoconfirmed !== userid && targetUser.autoconfirmed);
2601 let displayMessage = '';
2602 if (affected.length > 1) {
2603 displayMessage = `(${name}'s ${(acAccount ? ` ac account: ${acAccount},` : '')} blacklisted alts: ${affected.slice(1).map(user => user.getLastName()).join(", ")})`;
2604 this.privateModAction(displayMessage);
2605 } else if (acAccount) {
2606 displayMessage = `(${name}'s ac account: ${acAccount})`;
2607 this.privateModAction(displayMessage);
2608 }
2609 }
2610
2611 if (!room.isPrivate && room.chatRoomData) {
2612 this.globalModlog("BLACKLIST", targetUser, ` by ${user.userid}${(target ? `: ${target}` : '')}`);
2613 } else {
2614 // Room modlog only
2615 this.modlog("BLACKLIST", targetUser, ` by ${user.userid}${(target ? `: ${target}` : '')}`);
2616 }
2617 return true;
2618 },
2619 blacklisthelp: [
2620 `/blacklist [username], [reason] - Blacklists the user from the room you are in for a year. Requires: # & ~`,
2621 `/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`,
2622 `/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`,
2623 `/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`,
2624 ],
2625
2626 forcebattleban: 'battleban',
2627 battleban: function (target, room, user, connection, cmd) {
2628 if (!target) return this.parse(`/help battleban`);
2629
2630 const reason = this.splitTarget(target);
2631 const targetUser = this.targetUser;
2632 if (!targetUser) return this.errorReply(`User ${this.targetUsername} not found.`);
2633 if (target.length > MAX_REASON_LENGTH) {
2634 return this.errorReply(`The reason is too long. It cannot exceed ${MAX_REASON_LENGTH} characters.`);
2635 }
2636 if (!reason) {
2637 return this.errorReply(`Battle bans require a reason.`);
2638 }
2639 if (!room.battle && (!reason.includes('.pokemonshowdown.com/') && cmd !== 'forcebattleban')) {
2640 return this.errorReply(`Battle bans require a battle replay if used outside of a battle; if the battle has expired, use /forcebattleban.`);
2641 }
2642 if (!this.can('lock', targetUser)) return;
2643 if (Punishments.isBattleBanned(targetUser)) return this.errorReply(`User '${targetUser.name}' is already banned from battling.`);
2644
2645 const reasonText = reason ? ` (${reason})` : `.`;
2646 const battlebanMessage = `${targetUser.name} was banned from starting new battles by ${user.name}${reasonText}`;
2647 this.privateModAction(`(${battlebanMessage})`);
2648
2649 // Notify staff room when a user is banned from battling outside of it.
2650 if (room.id !== 'staff' && Rooms('staff')) {
2651 Rooms('staff').addByUser(user, `<<${room.id}>> ${battlebanMessage}`);
2652 }
2653 if (targetUser.trusted) {
2654 Monitor.log(`[CrisisMonitor] Trusted user ${targetUser.name} was banned from battling by ${user.name}, and should probably be demoted.`);
2655 }
2656
2657 this.globalModlog("BATTLEBAN", targetUser, ` by ${user.userid}${reasonText}`);
2658 Ladders.cancelSearches(targetUser);
2659 Punishments.battleban(targetUser, null, null, reason);
2660 targetUser.popup(`|modal|${user.name} has prevented you from starting new battles for 2 days${reasonText}`);
2661
2662 // Automatically upload replays as evidence/reference to the punishment
2663 if (room.battle) this.parse('/savereplay');
2664 return true;
2665 },
2666 battlebanhelp: [`/battleban [username], [reason] - Prevents the user from starting new battles for 2 days and shows them the [reason]. Requires: % @ * & ~`],
2667
2668 unbattleban: function (target, room, user) {
2669 if (!target) return this.parse('/help unbattleban');
2670 if (!this.can('lock')) return;
2671
2672 const targetUser = Users.get(target);
2673 const unbanned = Punishments.unbattleban(target);
2674
2675 if (unbanned) {
2676 this.addModAction(`${unbanned} was allowed to battle again by ${user.name}.`);
2677 this.globalModlog("UNBATTLEBAN", toId(target), `by ${user.name}`);
2678 if (targetUser) targetUser.popup(`${user.name} has allowed you to battle again.`);
2679 } else {
2680 this.errorReply(`User ${target} is not banned from battling.`);
2681 }
2682 },
2683 unbattlebanhelp: [`/unbattleban [username] - Allows a user to battle again. Requires: % @ * & ~`],
2684
2685 nameblacklist: 'blacklistname',
2686 blacklistname: function (target, room, user) {
2687 if (!target) return this.parse('/help blacklistname');
2688 if (!this.canTalk()) return;
2689 if (!this.can('editroom', null, room)) return false;
2690 if (!room.chatRoomData) {
2691 return this.errorReply("This room is not going to last long enough for a blacklist to matter - just ban the user");
2692 }
2693
2694 let [targetStr, reason] = target.split('|').map(val => val.trim());
2695 if (!targetStr || (!reason && REQUIRE_REASONS)) {
2696 return this.errorReply("Usage: /blacklistname name1, name2, ... | reason");
2697 }
2698
2699 let targets = targetStr.split(',').map(s => toId(s));
2700
2701 let duplicates = targets.filter(userid => {
2702 let punishment = Punishments.roomUserids.nestedGet(room.id, userid);
2703 return punishment && punishment[0] === 'BLACKLIST';
2704 });
2705 if (duplicates.length) {
2706 return this.errorReply(`[${duplicates.join(', ')}] ${Chat.plural(duplicates, "are", "is")} already blacklisted.`);
2707 }
2708
2709 const userRank = Config.groupsranking.indexOf(room.getAuth(user));
2710 for (const userid of targets) {
2711 if (!userid) return this.errorReply(`User '${userid}' is not a valid userid.`);
2712 const targetRank = Config.groupsranking.indexOf(room.getAuth({userid}));
2713 if (targetRank >= userRank) return this.errorReply(`/blacklistname - Access denied: ${userid} is of equal or higher authority than you.`);
2714
2715 Punishments.roomBlacklist(room, null, null, userid, reason);
2716
2717 const trusted = Users.isTrusted(userid);
2718 if (trusted && room.isPrivate !== true) {
2719 Monitor.log(`[CrisisMonitor] Trusted user ${userid}${(trusted !== userid ? ` (${trusted})` : ``)} was nameblacklisted from ${room.id} by ${user.name}, and should probably be demoted.`);
2720 }
2721 if (!room.isPrivate && room.chatRoomData) {
2722 this.globalModlog("NAMEBLACKLIST", userid, ` by ${user.userid}${(reason ? `: ${reason}` : '')}`);
2723 }
2724 }
2725
2726 this.privateModAction(`(${targets.join(', ')}${Chat.plural(targets, " were", " was")} nameblacklisted from ${room.title} by ${user.name}.)`);
2727 return true;
2728 },
2729 blacklistnamehelp: [`/blacklistname OR /nameblacklist [username1, username2, etc.] | reason - Blacklists the given username(s) from the room you are in for a year. Requires: # & ~`],
2730
2731 unab: 'unblacklist',
2732 unblacklist: function (target, room, user) {
2733 if (!target) return this.parse('/help unblacklist');
2734 if (!this.can('editroom', null, room)) return false;
2735
2736 const name = Punishments.roomUnblacklist(room, target);
2737
2738 if (name) {
2739 this.privateModAction(`(${name} was unblacklisted by ${user.name}.)`);
2740 if (!room.isPrivate && room.chatRoomData) {
2741 this.globalModlog("UNBLACKLIST", name, ` by ${user.userid}`);
2742 }
2743 } else {
2744 this.errorReply(`User '${target}' is not blacklisted.`);
2745 }
2746 },
2747 unblacklisthelp: [`/unblacklist [username] - Unblacklists the user from the room you are in. Requires: # & ~`],
2748
2749 unblacklistall: function (target, room, user) {
2750 if (!this.can('editroom', null, room)) return false;
2751
2752 if (!target) {
2753 user.lastCommand = '/unblacklistall';
2754 this.errorReply("THIS WILL UNBLACKLIST ALL BLACKLISTED USERS IN THIS ROOM.");
2755 this.errorReply("To confirm, use: /unblacklistall confirm");
2756 return;
2757 }
2758 if (user.lastCommand !== '/unblacklistall' || target !== 'confirm') {
2759 return this.parse('/help unblacklistall');
2760 }
2761 user.lastCommand = '';
2762 let unblacklisted = Punishments.roomUnblacklistAll(room);
2763 if (!unblacklisted) return this.errorReply("No users are currently blacklisted in this room to unblacklist.");
2764 this.addModAction(`All blacklists in this room have been lifted by ${user.name}.`);
2765 this.modlog('UNBLACKLISTALL');
2766 this.roomlog(`Unblacklisted users: ${unblacklisted.join(', ')}`);
2767 },
2768 unblacklistallhelp: [`/unblacklistall - Unblacklists all blacklisted users in the current room. Requires #, &, ~`],
2769
2770 expiringbls: 'showblacklist',
2771 expiringblacklists: 'showblacklist',
2772 blacklists: 'showblacklist',
2773 showbl: 'showblacklist',
2774 showblacklist: function (target, room, user, connection, cmd) {
2775 if (target) room = Rooms.search(target);
2776 if (!room) return this.errorReply(`The room "${target}" was not found.`);
2777 if (!this.can('mute', null, room)) return false;
2778 const SOON_EXPIRING_TIME = 3 * 30 * 24 * 60 * 60 * 1000; // 3 months
2779
2780 if (!room.chatRoomData) return this.errorReply("This room does not support blacklists.");
2781
2782 const subMap = Punishments.roomUserids.get(room.id);
2783 if (!subMap || subMap.size === 0) {
2784 return this.sendReply("This room has no blacklisted users.");
2785 }
2786 let blMap = new Map();
2787 let ips = '';
2788
2789 subMap.forEach((punishment, userid) => {
2790 const [punishType, id, expireTime] = punishment;
2791 if (punishType === 'BLACKLIST') {
2792 if (!blMap.has(id)) blMap.set(id, [expireTime]);
2793 if (id !== userid) blMap.get(id).push(userid);
2794 }
2795 });
2796
2797 if (user.can('ban')) {
2798 const subMap = Punishments.roomIps.get(room.id);
2799
2800 if (subMap) {
2801 ips = '/ips';
2802 subMap.forEach((punishment, ip) => {
2803 const [punishType, id] = punishment;
2804 if (punishType === 'BLACKLIST') {
2805 if (!blMap.has(id)) blMap.set(id, []);
2806 blMap.get(id).push(ip);
2807 }
2808 });
2809 }
2810 }
2811
2812 let soonExpiring = (cmd === 'expiringblacklists' || cmd === 'expiringbls');
2813 let buf = Chat.html`Blacklist for ${room.title}${soonExpiring ? ` (expiring within 3 months)` : ''}:<br />`;
2814
2815 blMap.forEach((data, userid) => {
2816 const [expireTime, ...alts] = data;
2817 if (soonExpiring && expireTime > Date.now() + SOON_EXPIRING_TIME) return;
2818 const expiresIn = new Date(expireTime).getTime() - Date.now();
2819 const expiresDays = Math.round(expiresIn / 1000 / 60 / 60 / 24);
2820 buf += `- <strong>${userid}</strong>, for ${Chat.count(expiresDays, "days")}`;
2821 if (alts.length) buf += `, alts${ips}: ${alts.join(', ')}`;
2822 buf += `<br />`;
2823 });
2824
2825 this.sendReplyBox(buf);
2826 },
2827 showblacklisthelp: [
2828 `/showblacklist OR /showbl - show a list of blacklisted users in the room. Requires: % @ # & ~`,
2829 `/expiringblacklists OR /expiringbls - show a list of blacklisted users from the room whose blacklists are expiring in 3 months or less. Requires: % @ # & ~`,
2830 ],
2831
2832 markshared: function (target, room, user) {
2833 if (!target) return this.parse('/help markshared');
2834 if (!this.can('ban')) return false;
2835 let [ip, note] = this.splitOne(target);
2836 if (!/^[0-9.*]+$/.test(ip)) return this.errorReply("Please enter a valid IP address.");
2837
2838 if (Punishments.sharedIps.has(ip)) return this.errorReply("This IP is already marked as shared.");
2839 if (!note) {
2840 this.errorReply(`You must specify who owns this shared IP.`);
2841 this.parse(`/help markshared`);
2842 return;
2843 }
2844
2845 Punishments.addSharedIp(ip, note);
2846 note = ` (${note})`;
2847 this.globalModlog('SHAREDIP', ip, ` by ${user.name}${note}`);
2848
2849 const message = `The IP '${ip}' was marked as shared by ${user.name}.${note}`;
2850 const staffRoom = Rooms('staff');
2851 if (staffRoom) return staffRoom.addByUser(user, message);
2852
2853 return this.addModAction(message);
2854 },
2855 marksharedhelp: [`/markshared [IP], [owner/organization of IP] - Marks an IP address as shared. Note: the owner/organization (i.e., University of Minnesota) of the shared IP is required. Requires @, &, ~`],
2856
2857 unmarkshared: function (target, room, user) {
2858 if (!target) return this.parse('/help unmarkshared');
2859 if (!this.can('ban')) return false;
2860 if (!/^[0-9.*]+$/.test(target)) return this.errorReply("Please enter a valid IP address.");
2861
2862 if (!Punishments.sharedIps.has(target)) return this.errorReply("This IP isn't marked as shared.");
2863
2864 Punishments.removeSharedIp(target);
2865 this.globalModlog('UNSHAREIP', target, ` by ${user.name}`);
2866 return this.addModAction(`The IP '${target}' was unmarked as shared by ${user.name}.`);
2867 },
2868 unmarksharedhelp: [`/unmarkshared [ip] - Unmarks a shared IP address. Requires @, &, ~`],
2869
2870 /*********************************************************
2871 * Server management commands
2872 *********************************************************/
2873
2874 hotpatch: function (target, room, user) {
2875 if (!target) return this.parse('/help hotpatch');
2876 if (!this.can('hotpatch')) return;
2877
2878 const lock = Monitor.hotpatchLock;
2879 const hotpatches = ['chat', 'formats', 'loginserver', 'punishments', 'dnsbl'];
2880
2881 try {
2882 if (target === 'all') {
2883 if (lock['all']) return this.errorReply(`Hot-patching all has been disabled by ${lock['all'].by} (${lock['all'].reason})`);
2884 if (Config.disablehotpatchall) return this.errorReply("This server does not allow for the use of /hotpatch all");
2885
2886 for (const hotpatch of hotpatches) {
2887 this.parse(`/hotpatch ${hotpatch}`);
2888 }
2889 } else if (target === 'chat' || target === 'commands') {
2890 if (lock['chat']) return this.errorReply(`Hot-patching chat has been disabled by ${lock['chat'].by} (${lock['chat'].reason})`);
2891 if (lock['tournaments']) return this.errorReply(`Hot-patching tournaments has been disabled by ${lock['tournaments'].by} (${lock['tournaments'].reason})`);
2892
2893 const processManagers = require('./lib/process-manager').processManagers;
2894 for (let manager of processManagers.slice()) {
2895 if (manager.filename.startsWith(FS('chat-plugins').path)) {
2896 manager.destroy();
2897 }
2898 }
2899
2900 Chat.uncache('./chat');
2901 Chat.uncache('./chat-commands');
2902 Chat.uncacheDir('./chat-plugins');
2903 global.Chat = require('./chat');
2904
2905 let runningTournaments = Tournaments.tournaments;
2906 Chat.uncacheDir('./tournaments');
2907 global.Tournaments = require('./tournaments');
2908 Tournaments.tournaments = runningTournaments;
2909 this.sendReply("Chat commands have been hot-patched.");
2910 } else if (target === 'tournaments') {
2911 if (lock['tournaments']) return this.errorReply(`Hot-patching tournaments has been disabled by ${lock['tournaments'].by} (${lock['tournaments'].reason})`);
2912
2913 let runningTournaments = Tournaments.tournaments;
2914 Chat.uncacheDir('./tournaments');
2915 global.Tournaments = require('./tournaments');
2916 Tournaments.tournaments = runningTournaments;
2917 this.sendReply("Tournaments have been hot-patched.");
2918 } else if (target === 'battles') {
2919 if (lock['battles']) return this.errorReply(`Hot-patching battles has been disabled by ${lock['battles'].by} (${lock['battles'].reason})`);
2920 if (lock['formats']) return this.errorReply(`Hot-patching formats has been disabled by ${lock['formats'].by} (${lock['formats'].reason})`);
2921
2922 Rooms.PM.respawn();
2923 this.sendReply("Battles have been hot-patched. Any battles started after now will use the new code; however, in-progress battles will continue to use the old code.");
2924 } else if (target === 'formats') {
2925 if (lock['formats']) return this.errorReply(`Hot-patching formats has been disabled by ${lock['formats'].by} (${lock['formats'].reason})`);
2926 if (lock['battles']) return this.errorReply(`Hot-patching battles has been disabled by ${lock['battles'].by} (${lock['battles'].reason})`);
2927 if (lock['validator']) return this.errorReply(`Hot-patching the validator has been disabled by ${lock['validator'].by} (${lock['validator'].reason})`);
2928
2929 // uncache the sim/dex.js dependency tree
2930 Chat.uncacheDir('./sim');
2931 Chat.uncacheDir('./data');
2932 Chat.uncacheDir('./mods');
2933 Chat.uncache('./config/formats');
2934 // reload sim/dex.js
2935 global.Dex = require('./sim/dex');
2936 // rebuild the formats list
2937 delete Rooms.global.formatList;
2938 // respawn validator processes
2939 TeamValidatorAsync.PM.respawn();
2940 // respawn simulator processes
2941 Rooms.PM.respawn();
2942 // broadcast the new formats list to clients
2943 Rooms.global.send(Rooms.global.formatListText);
2944
2945 this.sendReply("Formats have been hot-patched.");
2946 } else if (target === 'loginserver') {
2947 FS('config/custom.css').unwatch();
2948 Chat.uncache('./loginserver');
2949 global.LoginServer = require('./loginserver');
2950 this.sendReply("The login server has been hot-patched. New login server requests will use the new code.");
2951 } else if (target === 'learnsets' || target === 'validator') {
2952 if (lock['validator']) return this.errorReply(`Hot-patching the validator has been disabled by ${lock['validator'].by} (${lock['validator'].reason})`);
2953 if (lock['formats']) return this.errorReply(`Hot-patching formats has been disabled by ${lock['formats'].by} (${lock['formats'].reason})`);
2954
2955 TeamValidatorAsync.PM.respawn();
2956 this.sendReply("The team validator has been hot-patched. Any battles started after now will have teams be validated according to the new code.");
2957 } else if (target === 'punishments') {
2958 if (lock['punishments']) return this.errorReply(`Hot-patching punishments has been disabled by ${lock['punishments'].by} (${lock['punishments'].reason})`);
2959
2960 Chat.uncache('./punishments');
2961 global.Punishments = require('./punishments');
2962 this.sendReply("Punishments have been hot-patched.");
2963 } else if (target === 'dnsbl' || target === 'datacenters') {
2964 Dnsbl.loadDatacenters();
2965 this.sendReply("Dnsbl has been hot-patched.");
2966 } else if (target.startsWith('disable')) {
2967 this.sendReply("Disabling hot-patch has been moved to its own command:");
2968 return this.parse('/help nohotpatch');
2969 } else {
2970 return this.errorReply("Your hot-patch command was unrecognized.");
2971 }
2972 } catch (e) {
2973 Rooms.global.notifyRooms(['development', 'staff', 'upperstaff'], `|c|${user.getIdentity()}|/log ${user.name} used /hotpatch ${target} - but something failed while trying to hot-patch.`);
2974 return this.errorReply(`Something failed while trying to hot-patch ${target}: \n${e.stack}`);
2975 }
2976 Rooms.global.notifyRooms(['development', 'staff', 'upperstaff'], `|c|${user.getIdentity()}|/log ${user.name} used /hotpatch ${target}`);
2977 },
2978 hotpatchhelp: [
2979 `Hot-patching the game engine allows you to update parts of Showdown without interrupting currently-running battles. Requires: ~`,
2980 `Hot-patching has greater memory requirements than restarting`,
2981 `You can disable various hot-patches with /nohotpatch. For more information on this, see /help nohotpatch`,
2982 `/hotpatch chat - reload chat-commands.js and the chat-plugins`,
2983 `/hotpatch battles - spawn new simulator processes`,
2984 `/hotpatch validator - spawn new team validator processes`,
2985 `/hotpatch formats - reload the sim/dex.js tree, rebuild and rebroad the formats list, and spawn new simulator and team validator processes`,
2986 `/hotpatch dnsbl - reloads Dnsbl datacenters`,
2987 `/hotpatch punishments - reloads new punishments code`,
2988 `/hotpatch tournaments - reloads new tournaments code`,
2989 `/hotpatch all - hot-patches chat, tournaments, formats, login server, punishments, and dnsbl`,
2990 ],
2991
2992 hotpatchlock: 'nohotpatch',
2993 nohotpatch: function (target, room, user) {
2994 if (!this.can('declare')) return;
2995 if (!target) return this.parse('/help nohotpatch');
2996
2997 const separator = ' ';
2998
2999 const hotpatch = toId(target.substr(0, target.indexOf(separator)));
3000 const reason = target.substr(target.indexOf(separator), target.length).trim();
3001 if (!reason || !target.includes(separator)) return this.parse('/help nohotpatch');
3002
3003 let lock = Monitor.hotpatchLock;
3004 const validDisable = ['chat', 'battles', 'formats', 'validator', 'tournaments', 'punishments', 'all'];
3005
3006 if (validDisable.includes(hotpatch)) {
3007 if (lock[hotpatch]) return this.errorReply(`Hot-patching ${hotpatch} has already been disabled by ${lock[hotpatch].by} (${lock[hotpatch].reason})`);
3008 lock[hotpatch] = {
3009 by: user.name,
3010 reason: reason,
3011 };
3012 this.sendReply(`You have disabled hot-patching ${hotpatch}.`);
3013 } else {
3014 return this.errorReply("This hot-patch is not an option to disable.");
3015 }
3016 Rooms.global.notifyRooms(['development', 'staff', 'upperstaff'], `|c|${user.getIdentity()}|/log ${user.name} has disabled hot-patching ${hotpatch}. Reason: ${reason}`);
3017 },
3018 nohotpatchhelp: [`/nohotpatch [chat|formats|battles|validator|tournaments|punishments|all] [reason] - Disables hotpatching the specified part of the simulator. Requires: & ~`],
3019
3020 savelearnsets: function (target, room, user) {
3021 if (!this.can('hotpatch')) return false;
3022 this.sendReply("saving...");
3023 FS('data/learnsets.js').write(`'use strict';\n\nexports.BattleLearnsets = {\n` +
3024 Object.entries(Dex.data.Learnsets).map(([k, v]) => (
3025 `\t${k}: {learnset: {\n` +
3026 Object.entries(v.learnset).sort(
3027 (a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
3028 ).map(([k, v]) => (
3029 `\t\t${k}: ["` + v.join(`", "`) + `"],\n`
3030 )).join('') +
3031 `\t}},\n`
3032 )).join('') +
3033 `};\n`).then(() => {
3034 this.sendReply("learnsets.js saved.");
3035 });
3036 },
3037
3038 widendatacenters: 'adddatacenters',
3039 adddatacenters: function (target, room, user, connection, cmd) {
3040 if (!this.can('hotpatch')) return false;
3041 // should be in the format: IP, IP, name, URL
3042 let widen = (cmd === 'widendatacenters');
3043
3044 FS('config/datacenters.csv').readIfExists().then(data => {
3045 let datacenters = [];
3046 for (const row of data.split("\n")) {
3047 if (!row) continue;
3048 const rowSplit = row.split(',');
3049 const rowData = [
3050 Dnsbl.ipToNumber(rowSplit[0]),
3051 Dnsbl.ipToNumber(rowSplit[1]),
3052 Dnsbl.urlToHost(rowSplit[3]),
3053 row,
3054 ];
3055 datacenters.push(rowData);
3056 }
3057
3058 data = String(target).split("\n");
3059 let successes = 0;
3060 let identicals = 0;
3061 let widenSuccesses = 0;
3062 for (const row of data) {
3063 if (!row) continue;
3064 let rowSplit = row.split(',');
3065 let rowData = [
3066 Dnsbl.ipToNumber(rowSplit[0]),
3067 Dnsbl.ipToNumber(rowSplit[1]),
3068 Dnsbl.urlToHost(rowSplit[3]),
3069 row,
3070 ];
3071 if (rowData[1] < rowData[0]) {
3072 this.errorReply(`Invalid range: ${row}`);
3073 continue;
3074 }
3075
3076 let iMin = 0;
3077 let iMax = datacenters.length;
3078 while (iMin < iMax) {
3079 let i = Math.floor((iMax + iMin) / 2);
3080 if (rowData[0] > datacenters[i][0]) {
3081 iMin = i + 1;
3082 } else {
3083 iMax = i;
3084 }
3085 }
3086 if (iMin < datacenters.length) {
3087 let next = datacenters[iMin];
3088 if (rowData[0] === next[0] && rowData[1] === next[1]) {
3089 identicals++;
3090 continue;
3091 }
3092 if (rowData[0] <= next[0] && rowData[1] >= next[1]) {
3093 if (widen === true) {
3094 widenSuccesses++;
3095 datacenters.splice(iMin, 1, rowData);
3096 continue;
3097 }
3098 this.errorReply(`Too wide: ${row}`);
3099 this.errorReply(`Intersects with: ${next[3]}`);
3100 continue;
3101 }
3102 if (rowData[1] >= next[0]) {
3103 this.errorReply(`Could not insert: ${row}`);
3104 this.errorReply(`Intersects with: ${next[3]}`);
3105 continue;
3106 }
3107 }
3108 if (iMin > 0) {
3109 let prev = datacenters[iMin - 1];
3110 if (rowData[0] >= prev[0] && rowData[1] <= prev[1]) {
3111 this.errorReply(`Too narrow: ${row}`);
3112 this.errorReply(`Intersects with: ${prev[3]}`);
3113 continue;
3114 }
3115 if (rowData[0] <= prev[1]) {
3116 this.errorReply(`Could not insert: ${row}`);
3117 this.errorReply(`Intersects with: ${prev[3]}`);
3118 continue;
3119 }
3120 }
3121 successes++;
3122 datacenters.splice(iMin, 0, rowData);
3123 }
3124
3125 let output = datacenters.map(r => r[3]).join('\n') + '\n';
3126 FS('config/datacenters.csv').write(output);
3127 this.sendReply(`Done: ${successes} successes, ${identicals} unchanged.`);
3128 if (widenSuccesses) this.sendReply(`${widenSuccesses} widens.`);
3129 });
3130 },
3131
3132 disableladder: function (target, room, user) {
3133 if (!this.can('disableladder')) return false;
3134 if (Ladders.disabled) {
3135 return this.errorReply(`/disableladder - Ladder is already disabled.`);
3136 }
3137
3138 Ladders.disabled = true;
3139
3140 this.modlog(`DISABLELADDER`);
3141 Monitor.log(`The ladder was disabled by ${user.name}.`);
3142
3143 const innerHTML = (
3144 `<b>Due to technical difficulties, the ladder has been temporarily disabled.</b><br />` +
3145 `Rated games will no longer update the ladder. It will be back momentarily.`
3146 );
3147
3148 Rooms.rooms.forEach((curRoom, id) => {
3149 if (curRoom.type === 'battle') curRoom.rated = false;
3150 if (id !== 'global') curRoom.addRaw(`<div class="broadcast-red">${innerHTML}</div>`).update();
3151 });
3152 Users.users.forEach(u => {
3153 if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw <div class="broadcast-red">${innerHTML}</div>`);
3154 });
3155 },
3156
3157 enableladder: function (target, room, user) {
3158 if (!this.can('disableladder')) return false;
3159 if (!Ladders.disabled) {
3160 return this.errorReply(`/enable - Ladder is already enabled.`);
3161 }
3162 Ladders.disabled = false;
3163
3164 this.modlog('ENABLELADDER');
3165 Monitor.log(`The ladder was enabled by ${user.name}.`);
3166
3167 const innerHTML = (
3168 `<b>The ladder is now back.</b><br />` +
3169 `Rated games will update the ladder now..`
3170 );
3171
3172 Rooms.rooms.forEach((curRoom, id) => {
3173 if (id !== 'global') curRoom.addRaw(`<div class="broadcast-green">${innerHTML}</div>`).update();
3174 });
3175 Users.users.forEach(u => {
3176 if (u.connected) u.send(`|pm|~|${u.group}${u.name}|/raw <div class="broadcast-green">${innerHTML}</div>`);
3177 });
3178 },
3179
3180 lockdown: function (target, room, user) {
3181 if (!this.can('lockdown')) return false;
3182
3183 Rooms.global.startLockdown();
3184
3185 const logRoom = Rooms('staff') || room;
3186 logRoom.roomlog(`${user.name} used /lockdown`);
3187 },
3188 lockdownhelp: [`/lockdown - locks down the server, which prevents new battles from starting so that the server can eventually be restarted. Requires: ~`],
3189
3190 autolockdown: 'autolockdownkill',
3191 autolockdownkill: function (target, room, user) {
3192 if (!this.can('lockdown')) return false;
3193 if (Config.autolockdown === undefined) Config.autolockdown = true;
3194
3195 if (this.meansYes(target)) {
3196 if (Config.autolockdown) return this.errorReply("The server is already set to automatically kill itself upon the final battle finishing.");
3197 Config.autolockdown = true;
3198 this.sendReply("The server is now set to automatically kill itself upon the final battle finishing.");
3199 const logRoom = Rooms('staff') || room;
3200 logRoom.roomlog(`${user.name} used /autolockdownkill on`);
3201 } else if (this.meansNo(target)) {
3202 if (!Config.autolockdown) return this.errorReply("The server is already set to not automatically kill itself upon the final battle finishing.");
3203 Config.autolockdown = false;
3204 this.sendReply("The server is now set to not automatically kill itself upon the final battle finishing.");
3205 const logRoom = Rooms('staff') || room;
3206 logRoom.roomlog(`${user.name} used /autolockdownkill off`);
3207 } else {
3208 return this.parse('/help autolockdownkill');
3209 }
3210 },
3211 autolockdownkillhelp: [
3212 `/autolockdownkill on - Turns on the setting to enable the server to automatically kill itself upon the final battle finishing. Requires ~`,
3213 `/autolockdownkill off - Turns off the setting to enable the server to automatically kill itself upon the final battle finishing. Requires ~`,
3214 ],
3215
3216 prelockdown: function (target, room, user) {
3217 if (!this.can('lockdown')) return false;
3218 Rooms.global.lockdown = 'pre';
3219 this.sendReply("Tournaments have been disabled in preparation for the server restart.");
3220 const logRoom = Rooms('staff') || room;
3221 logRoom.roomlog(`${user.name} used /prelockdown`);
3222 },
3223
3224 slowlockdown: function (target, room, user) {
3225 if (!this.can('lockdown')) return false;
3226
3227 Rooms.global.startLockdown(undefined, true);
3228
3229 const logRoom = Rooms('staff') || room;
3230 logRoom.roomlog(`${user.name} used /slowlockdown`);
3231 },
3232
3233 endlockdown: function (target, room, user) {
3234 if (!this.can('lockdown')) return false;
3235
3236 if (!Rooms.global.lockdown) {
3237 return this.errorReply("We're not under lockdown right now.");
3238 }
3239 if (Rooms.global.lockdown === true) {
3240 Rooms.rooms.forEach((curRoom, id) => {
3241 if (id !== 'global') curRoom.addRaw(`<div class="broadcast-green"><b>The server restart was canceled.</b></div>`).update();
3242 });
3243 } else {
3244 this.sendReply("Preparation for the server shutdown was canceled.");
3245 }
3246 Rooms.global.lockdown = false;
3247
3248 const logRoom = Rooms('staff') || room;
3249 logRoom.roomlog(`${user.name} used /endlockdown`);
3250 },
3251
3252 emergency: function (target, room, user) {
3253 if (!this.can('lockdown')) return false;
3254
3255 if (Config.emergency) {
3256 return this.errorReply("We're already in emergency mode.");
3257 }
3258 Config.emergency = true;
3259 Rooms.rooms.forEach((curRoom, id) => {
3260 if (id !== 'global') curRoom.addRaw(`<div class="broadcast-red">The server has entered emergency mode. Some features might be disabled or limited.</div>`).update();
3261 });
3262
3263 const logRoom = Rooms('staff') || room;
3264 logRoom.roomlog(`${user.name} used /emergency.`);
3265 },
3266
3267 endemergency: function (target, room, user) {
3268 if (!this.can('lockdown')) return false;
3269
3270 if (!Config.emergency) {
3271 return this.errorReply("We're not in emergency mode.");
3272 }
3273 Config.emergency = false;
3274 Rooms.rooms.forEach((curRoom, id) => {
3275 if (id !== 'global') curRoom.addRaw(`<div class="broadcast-green"><b>The server is no longer in emergency mode.</b></div>`).update();
3276 });
3277
3278 const logRoom = Rooms('staff') || room;
3279 logRoom.roomlog(`${user.name} used /endemergency.`);
3280 },
3281
3282 kill: function (target, room, user) {
3283 if (!this.can('lockdown')) return false;
3284
3285 if (Rooms.global.lockdown !== true) {
3286 return this.errorReply("For safety reasons, /kill can only be used during lockdown.");
3287 }
3288
3289 if (Chat.updateServerLock) {
3290 return this.errorReply("Wait for /updateserver to finish before using /kill.");
3291 }
3292
3293 Sockets.workers.forEach(worker => worker.kill());
3294
3295 if (!room.destroyLog) {
3296 process.exit();
3297 return;
3298 }
3299 const logRoom = Rooms('staff') || room;
3300 logRoom.roomlog(`${user.name} used /kill`);
3301 room.destroyLog(() => {
3302 process.exit();
3303 });
3304
3305 // Just in the case the above never terminates, kill the process
3306 // after 10 seconds.
3307 setTimeout(() => {
3308 process.exit();
3309 }, 10000);
3310 },
3311 killhelp: [`/kill - kills the server. Can't be done unless the server is in lockdown state. Requires: ~`],
3312
3313 loadbanlist: function (target, room, user, connection) {
3314 if (!this.can('hotpatch')) return false;
3315
3316 connection.sendTo(room, "Loading ipbans.txt...");
3317 Punishments.loadBanlist().then(
3318 () => connection.sendTo(room, "ipbans.txt has been reloaded."),
3319 error => connection.sendTo(room, `Something went wrong while loading ipbans.txt: ${error}`)
3320 );
3321 },
3322 loadbanlisthelp: [`/loadbanlist - Loads the bans located at ipbans.txt. The command is executed automatically at startup. Requires: ~`],
3323
3324 refreshpage: function (target, room, user) {
3325 if (!this.can('hotpatch')) return false;
3326 Rooms.global.send('|refresh|');
3327 const logRoom = Rooms('staff') || room;
3328 logRoom.roomlog(`${user.name} used /refreshpage`);
3329 },
3330
3331 updateserver: async function (target, room, user, connection) {
3332 if (!user.can('hotpatch')) {
3333 return this.errorReply(`/updateserver - Access denied.`);
3334 }
3335
3336 if (Chat.updateServerLock) {
3337 return this.errorReply(`/updateserver - Another update is already in progress (or a previous update crashed).`);
3338 }
3339
3340 Chat.updateServerLock = true;
3341
3342 const logRoom = Rooms('staff') || room;
3343
3344 /** @return {Promise<[number, string, string]>} */
3345 function exec(/** @type {string} */ command) {
3346 logRoom.roomlog(`$ ${command}`);
3347 return new Promise((resolve, reject) => {
3348 require('child_process').exec(command, (error, stdout, stderr) => {
3349 let log = `[o] ${stdout}[e] ${stderr}`;
3350 if (error) log = `[c] ${error.code}\n${log}`;
3351 logRoom.roomlog(log);
3352 resolve([error && error.code || 0, stdout, stderr]);
3353 });
3354 });
3355 }
3356
3357 this.sendReply(`Fetching newest version...`);
3358 logRoom.roomlog(`${user.name} used /updateserver`);
3359
3360 let [code, stdout, stderr] = await exec(`git fetch`);
3361 if (code) throw new Error(`updateserver: Crash while fetching - make sure this is a Git repository`);
3362 if (!stdout && !stderr) {
3363 Chat.updateServerLock = false;
3364 return this.sendReply(`There were no updates.`);
3365 }
3366
3367 [code, stdout, stderr] = await exec(`git rev-parse HEAD`);
3368 if (code || stderr) throw new Error(`updateserver: Crash while grabbing hash`);
3369 const oldHash = String(stdout).trim();
3370
3371 [code, stdout, stderr] = await exec(`git stash save --include-untracked "PS /updateserver autostash"`);
3372 let stashedChanges = true;
3373 if (code) throw new Error(`updateserver: Crash while stashing`);
3374 if ((stdout + stderr).includes("No local changes")) {
3375 stashedChanges = false;
3376 } else if (stderr) {
3377 throw new Error(`updateserver: Crash while stashing`);
3378 } else {
3379 this.sendReply(`Saving changes...`);
3380 }
3381
3382 // errors can occur while rebasing or popping the stash; make sure to recover
3383 try {
3384 this.sendReply(`Rebasing...`);
3385 [code] = await exec(`git rebase FETCH_HEAD`);
3386 if (code) {
3387 // conflict while rebasing
3388 await exec(`git rebase --abort`);
3389 throw new Error(`restore`);
3390 }
3391
3392 if (stashedChanges) {
3393 this.sendReply(`Restoring saved changes...`);
3394 [code] = await exec(`git stash pop`);
3395 if (code) {
3396 // conflict while popping stash
3397 await exec(`git reset HEAD .`);
3398 await exec(`git checkout .`);
3399 throw new Error(`restore`);
3400 }
3401 }
3402
3403 this.sendReply(`SUCCESSFUL, server updated.`);
3404 } catch (e) {
3405 // failed while rebasing or popping the stash
3406 await exec(`git reset --hard ${oldHash}`);
3407 await exec(`git stash pop`);
3408 this.sendReply(`FAILED, old changes restored.`);
3409 }
3410 Chat.updateServerLock = false;
3411 },
3412
3413 crashfixed: function (target, room, user) {
3414 if (Rooms.global.lockdown !== true) {
3415 return this.errorReply('/crashfixed - There is no active crash.');
3416 }
3417 if (!this.can('hotpatch')) return false;
3418
3419 Rooms.global.lockdown = false;
3420 if (Rooms.lobby) {
3421 Rooms.lobby.modchat = false;
3422 Rooms.lobby.addRaw(`<div class="broadcast-green"><b>We fixed the crash without restarting the server!</b><br />You may resume talking in the Lobby and starting new battles.</div>`).update();
3423 }
3424 const logRoom = Rooms('staff') || room;
3425 logRoom.roomlog(`${user.name} used /crashfixed`);
3426 },
3427 crashfixedhelp: [`/crashfixed - Ends the active lockdown caused by a crash without the need of a restart. Requires: ~`],
3428
3429 memusage: 'memoryusage',
3430 memoryusage: function (target) {
3431 if (!this.can('hotpatch')) return false;
3432 let memUsage = process.memoryUsage();
3433 let results = [memUsage.rss, memUsage.heapUsed, memUsage.heapTotal];
3434 let units = ["B", "KiB", "MiB", "GiB", "TiB"];
3435 for (let i = 0; i < results.length; i++) {
3436 let unitIndex = Math.floor(Math.log2(results[i]) / 10); // 2^10 base log
3437 results[i] = `${(results[i] / Math.pow(2, 10 * unitIndex)).toFixed(2)} ${units[unitIndex]}`;
3438 }
3439 this.sendReply(`||[Main process] RSS: ${results[0]}, Heap: ${results[1]} / ${results[2]}`);
3440 },
3441
3442 bash: function (target, room, user, connection) {
3443 if (!user.hasConsoleAccess(connection)) {
3444 return this.errorReply("/bash - Access denied.");
3445 }
3446 if (!target) return this.parse('/help bash');
3447
3448 connection.sendTo(room, `$ ${target}`);
3449 require('child_process').exec(target, (error, stdout, stderr) => {
3450 connection.sendTo(room, (`${stdout}${stderr}`));
3451 });
3452 },
3453 bashhelp: [`/bash [command] - Executes a bash command on the server. Requires: ~ console access`],
3454
3455 eval: async function (target, room, user, connection) {
3456 if (!user.hasConsoleAccess(connection)) {
3457 return this.errorReply("/eval - Access denied.");
3458 }
3459 if (!this.runBroadcast(true)) return;
3460
3461 if (!this.broadcasting) this.sendReply(`||>> ${target}`);
3462 try {
3463 /* eslint-disable no-unused-vars */
3464 let battle = room.battle;
3465 let me = user;
3466 let result = eval(target);
3467 if (result && result.then) {
3468 result = `Promise -> ${Chat.stringify(await result)}`;
3469 } else {
3470 result = Chat.stringify(result);
3471 }
3472 result = result.replace(/\n/g, '\n||');
3473 this.sendReply('||<< ' + result);
3474 /* eslint-enable no-unused-vars */
3475 } catch (e) {
3476 const message = ('' + e.stack).replace(/\n *at CommandContext\.eval [\s\S]*/m, '').replace(/\n/g, '\n||');
3477 this.sendReply(`|| << ${message}`);
3478 }
3479 },
3480
3481 evalbattle: function (target, room, user, connection) {
3482 if (!user.hasConsoleAccess(connection)) {
3483 return this.errorReply("/evalbattle - Access denied.");
3484 }
3485 if (!this.runBroadcast(true)) return;
3486 if (!room.battle) {
3487 return this.errorReply("/evalbattle - This isn't a battle room.");
3488 }
3489
3490 room.battle.stream.write(`>eval ${target.replace(/\n/g, '\f')}`);
3491 },
3492
3493 ebat: 'editbattle',
3494 editbattle: function (target, room, user) {
3495 if (!this.can('forcewin')) return false;
3496 if (!target) return this.parse('/help editbattle');
3497 if (!room.battle) {
3498 this.errorReply("/editbattle - This is not a battle room.");
3499 return false;
3500 }
3501 let cmd;
3502 let spaceIndex = target.indexOf(' ');
3503 if (spaceIndex > 0) {
3504 cmd = target.substr(0, spaceIndex).toLowerCase();
3505 target = target.substr(spaceIndex + 1);
3506 } else {
3507 cmd = target.toLowerCase();
3508 target = '';
3509 }
3510 if (cmd.charAt(cmd.length - 1) === ',') cmd = cmd.slice(0, -1);
3511 let targets = target.split(',');
3512 function getPlayer(input) {
3513 let player = room.battle.players[toId(input)];
3514 if (player) return player.slot;
3515 if (input.includes('1')) return 'p1';
3516 if (input.includes('2')) return 'p2';
3517 return 'p3';
3518 }
3519 function getPokemon(input) {
3520 if (/^[0-9]+$/.test(input)) {
3521 return `.pokemon[${(parseInt(input) - 1)}]`;
3522 }
3523 return `.pokemon.find(p => p.speciesid==='${toId(targets[1])}')`;
3524 }
3525 switch (cmd) {
3526 case 'hp':
3527 case 'h':
3528 room.battle.stream.write(`>eval let p=${getPlayer(targets[0]) + getPokemon(targets[1])};p.sethp(${parseInt(targets[2])});if (p.isActive)battle.add('-damage',p,p.getHealth);`);
3529 break;
3530 case 'status':
3531 case 's':
3532 room.battle.stream.write(`>eval let pl=${getPlayer(targets[0])};let p=pl${getPokemon(targets[1])};p.setStatus('${toId(targets[2])}');if (!p.isActive){battle.add('','please ignore the above');battle.add('-status',pl.active[0],pl.active[0].status,'[silent]');}`);
3533 break;
3534 case 'pp':
3535 room.battle.stream.write(`>eval let pl=${getPlayer(targets[0])};let p=pl${getPokemon(targets[1])};p.moveSlots[p.moves.indexOf('${toId(targets[2])}')].pp = ${parseInt(targets[3])};`);
3536 break;
3537 case 'boost':
3538 case 'b':
3539 room.battle.stream.write(`>eval let p=${getPlayer(targets[0]) + getPokemon(targets[1])};battle.boost({${toId(targets[2])}:${parseInt(targets[3])}},p)`);
3540 break;
3541 case 'volatile':
3542 case 'v':
3543 room.battle.stream.write(`>eval let p=${getPlayer(targets[0]) + getPokemon(targets[1])};p.addVolatile('${toId(targets[2])}')`);
3544 break;
3545 case 'sidecondition':
3546 case 'sc':
3547 room.battle.stream.write(`>eval let p=${getPlayer(targets[0])}.addSideCondition('${toId(targets[1])}')`);
3548 break;
3549 case 'fieldcondition': case 'pseudoweather':
3550 case 'fc':
3551 room.battle.stream.write(`>eval battle.addPseudoWeather('${toId(targets[0])}')`);
3552 break;
3553 case 'weather':
3554 case 'w':
3555 room.battle.stream.write(`>eval battle.setWeather('${toId(targets[0])}')`);
3556 break;
3557 case 'terrain':
3558 case 't':
3559 room.battle.stream.write(`>eval battle.setTerrain('${toId(targets[0])}')`);
3560 break;
3561 default:
3562 this.errorReply(`Unknown editbattle command: ${cmd}`);
3563 break;
3564 }
3565 },
3566 editbattlehelp: [
3567 `/editbattle hp [player], [pokemon], [hp]`,
3568 `/editbattle status [player], [pokemon], [status]`,
3569 `/editbattle pp [player], [pokemon], [move], [pp]`,
3570 `/editbattle boost [player], [pokemon], [stat], [amount]`,
3571 `/editbattle volatile [player], [pokemon], [volatile]`,
3572 `/editbattle sidecondition [player], [sidecondition]`,
3573 `/editbattle fieldcondition [fieldcondition]`,
3574 `/editbattle weather [weather]`,
3575 `/editbattle terrain [terrain]`,
3576 `Short forms: /ebat h OR s OR pp OR b OR v OR sc OR fc OR w OR t`,
3577 `[player] must be a username or number, [pokemon] must be species name or number (not nickname), [move] must be move name.`,
3578 ],
3579
3580 allowexportinputlog(/** @type {string} */ target, /** @type {Room?} */ room, /** @type {User} */ user) {
3581 const battle = room.battle;
3582 if (!battle) return this.errorReply(`Must be in a battle.`);
3583 if (!battle.allowExtraction) return this.errorReply(`Someone must have requested extraction.`);
3584 const targetUser = Users.getExact(target);
3585
3586 if (toId(battle.playerNames[0]) === user.userid) {
3587 battle.allowExtraction[0] = targetUser.userid;
3588 } else if (toId(battle.playerNames[1]) === user.userid) {
3589 battle.allowExtraction[1] = targetUser.userid;
3590 } else {
3591 return this.errorReply(`Must be a player in the battle.`);
3592 }
3593 this.addModAction(`${user.userid} consents to sharing battle team and choices with ${targetUser.userid}.`);
3594 if (battle.allowExtraction.join(',') !== `${targetUser.userid},${targetUser.userid}`) return;
3595
3596 this.addModAction(`${targetUser.name} has extracted the battle input log.`);
3597 const inputLog = battle.inputLog.map(Chat.escapeHTML).join(`<br />`);
3598 targetUser.sendTo(room, `|html|<div class="chat"><code style="white-space: pre-wrap; overflow-wrap: break-word; display: block">${inputLog}</code></div>`);
3599 },
3600
3601 requestinputlog: 'exportinputlog',
3602 exportinputlog(target, room, user) {
3603 const battle = room.battle;
3604 if (!battle) return this.errorReply(`This command only works in battle rooms.`);
3605 if (!battle.inputLog) {
3606 this.errorReply(`This command only works when the battle has ended - if the battle has stalled, ask players to forfeit.`);
3607 if (user.can('forcewin')) this.errorReply(`Alternatively, you can end the battle with /forcetie.`);
3608 return;
3609 }
3610 if (!this.can('exportinputlog', null, room)) return;
3611 if (!battle.allowExtraction) {
3612 battle.allowExtraction = ['', ''];
3613 }
3614 if (battle.allowExtraction[0] !== user.userid) {
3615 const p1 = Users(battle.playerNames[0]);
3616 if (p1) p1.sendTo(room, Chat.html`|html|${user.name} wants to extract the battle input log. <button name="send" value="/allowexportinputlog ${user.userid}">Share your team and choices with "${user.name}"</button>`);
3617 }
3618 if (battle.allowExtraction[1] !== user.userid) {
3619 const p2 = Users(battle.playerNames[1]);
3620 if (p2) p2.sendTo(room, Chat.html`|html|${user.name} wants to extract the battle input log. <button name="send" value="/allowexportinputlog ${user.userid}">Share your team and choices with "${user.name}"</button>`);
3621 }
3622
3623 if (battle.allowExtraction.join(',') !== `${user.userid},${user.userid}`) {
3624 this.addModAction(`${user.name} wants to extract the battle input log.`);
3625 return;
3626 }
3627
3628 this.addModAction(`${user.name} has extracted the battle input log.`);
3629 const inputLog = battle.inputLog.map(Chat.escapeHTML).join(`<br />`);
3630 user.sendTo(room, `|html|<div class="chat"><code style="white-space: pre-wrap; overflow-wrap: break-word; display: block">${inputLog}</code></div>`);
3631 },
3632 exportinputloghelp: [`/exportinputlog - Asks players in a battle for permission to export an inputlog. Requires: & ~`],
3633
3634 importinputlog(target, room, user, connection) {
3635 if (!this.can('broadcast')) return;
3636 const formatIndex = target.indexOf(`"formatid":"`);
3637 const nextQuoteIndex = target.indexOf(`"`, formatIndex + 12);
3638 if (formatIndex < 0 || nextQuoteIndex < 0) return this.errorReply(`Invalid input log.`);
3639 target = target.replace(/\r/g, '');
3640 if ((`\n` + target).includes(`\n>eval `) && !user.hasConsoleAccess(connection)) {
3641 return this.errorReply(`Your input log contains untrusted code - you must have console access to use it.`);
3642 }
3643
3644 const formatid = target.slice(formatIndex + 12, nextQuoteIndex);
3645 const battleRoom = Rooms.createBattle(formatid, {inputLog: target});
3646
3647 const nameIndex1 = target.indexOf(`"name":"`);
3648 const nameNextQuoteIndex1 = target.indexOf(`"`, nameIndex1 + 8);
3649 const nameIndex2 = target.indexOf(`"name":"`, nameNextQuoteIndex1 + 1);
3650 const nameNextQuoteIndex2 = target.indexOf(`"`, nameIndex2 + 8);
3651 if (nameIndex1 >= 0 && nameNextQuoteIndex1 >= 0 && nameIndex2 >= 0 && nameNextQuoteIndex2 >= 0) {
3652 const name1 = target.slice(nameIndex1 + 8, nameNextQuoteIndex1);
3653 const name2 = target.slice(nameIndex2 + 8, nameNextQuoteIndex2);
3654 battleRoom.battle.playerNames = [name1, name2];
3655 }
3656
3657 this.parse(`/join ${battleRoom.id}`);
3658 battleRoom.auth[user.userid] = Users.HOST_SYMBOL;
3659 setTimeout(() => {
3660 // timer to make sure this goes under the battle
3661 battleRoom.add(`|html|<div class="broadcast broadcast-blue"><strong>This is an imported replay</strong><br />Players will need to be manually added with <code>/addplayer</code> or <code>/restoreplayers</code></div>`);
3662 }, 500);
3663 },
3664 importinputloghelp: [`/importinputlog [inputlog] - Starts a battle with a given inputlog. Requires: + % @ * & ~`],
3665
3666 inputlog: function () {
3667 this.parse(`/help exportinputlog`);
3668 this.parse(`/help importinputlog`);
3669 },
3670
3671 /*********************************************************
3672 * Battle commands
3673 *********************************************************/
3674
3675 forfeit: function (target, room, user) {
3676 if (!room.game) return this.errorReply("This room doesn't have an active game.");
3677 if (!room.game.forfeit) {
3678 return this.errorReply("This kind of game can't be forfeited.");
3679 }
3680 if (!room.game.forfeit(user)) {
3681 return this.errorReply("Forfeit failed.");
3682 }
3683 },
3684
3685 choose: function (target, room, user) {
3686 if (!room.game) return this.errorReply("This room doesn't have an active game.");
3687 if (!room.game.choose) return this.errorReply("This game doesn't support /choose");
3688
3689 room.game.choose(user, target);
3690 },
3691
3692 mv: 'move',
3693 attack: 'move',
3694 move: function (target, room, user) {
3695 this.parse(`/choose move ${target}`);
3696 },
3697
3698 sw: 'switch',
3699 switch: function (target, room, user) {
3700 this.parse(`/choose switch ${target}`);
3701 },
3702
3703 team: function (target, room, user) {
3704 this.parse(`/choose team ${target}`);
3705 },
3706
3707 undo: function (target, room, user) {
3708 if (!room.game) return this.errorReply("This room doesn't have an active game.");
3709 if (!room.game.undo) return this.errorReply("This game doesn't support /undo");
3710
3711 room.game.undo(user, target);
3712 },
3713
3714 uploadreplay: 'savereplay',
3715 savereplay: async function (target, room, user, connection) {
3716 if (!room || !room.battle) return;
3717 // retrieve spectator log (0) if there are privacy concerns
3718 const format = Dex.getFormat(room.format, true);
3719 let hideDetails = !format.id.includes('customgame');
3720 if (format.team && room.battle.ended) hideDetails = false;
3721 const data = room.getLog(hideDetails ? 0 : 3);
3722 const datahash = crypto.createHash('md5').update(data.replace(/[^(\x20-\x7F)]+/g, '')).digest('hex');
3723 let players = room.battle.playerNames;
3724 let rating = 0;
3725 if (room.battle.ended && room.rated) rating = room.rated;
3726 const [success] = await LoginServer.request('prepreplay', {
3727 id: room.id.substr(7),
3728 loghash: datahash,
3729 p1: players[0],
3730 p2: players[1],
3731 format: format.id,
3732 rating: rating,
3733 hidden: room.isPrivate ? '1' : '',
3734 inputlog: room.battle.inputLog ? room.battle.inputLog.join('\n') : null,
3735 });
3736 if (success && success.errorip) {
3737 connection.popup(`This server's request IP ${success.errorip} is not a registered server.`);
3738 return;
3739 }
3740 connection.send('|queryresponse|savereplay|' + JSON.stringify({
3741 log: data,
3742 id: room.id.substr(7),
3743 }));
3744 },
3745
3746 addplayer: function (target, room, user) {
3747 if (!target) return this.parse('/help addplayer');
3748 if (!room.battle) return this.errorReply("You can only do this in battle rooms.");
3749 if (room.rated) return this.errorReply("You can only add a Player to unrated battles.");
3750
3751 target = this.splitTarget(target, true).trim();
3752 if (target !== 'p1' && target !== 'p2') {
3753 this.errorReply(`Player must be set to "p1" or "p2", not "${target}".`);
3754 return this.parse('/help addplayer');
3755 }
3756
3757 let targetUser = this.targetUser;
3758 let name = this.targetUsername;
3759
3760 if (!targetUser) return this.errorReply(`User ${name} not found.`);
3761 if (!targetUser.inRooms.has(room.id)) {
3762 return this.errorReply(`User ${name} must be in the battle room already.`);
3763 }
3764 if (!this.can('joinbattle', null, room)) return;
3765 if (room.battle[target]) {
3766 return this.errorReply(`This room already has a player in slot ${target}.`);
3767 }
3768
3769 room.battle.addPlayer(targetUser, target);
3770 this.addModAction(`${name} was added to the battle as Player ${target.slice(1)} by ${user.name}.`);
3771 this.modlog('ROOMPLAYER', targetUser.getLastId());
3772 },
3773 addplayerhelp: [
3774 `/addplayer [username], p1 - Allow the specified user to join the battle as Player 1.`,
3775 `/addplayer [username], p2 - Allow the specified user to join the battle as Player 2.`,
3776 ],
3777
3778 restoreplayers: function (target, room, user) {
3779 if (!room.battle) return this.errorReply("You can only do this in battle rooms.");
3780 if (room.rated) return this.errorReply("You can only add a Player to unrated battles.");
3781
3782 let didSomething = false;
3783 if (!room.battle.p1 && room.battle.playerNames[0] !== 'Player 1') {
3784 this.parse(`/addplayer ${room.battle.playerNames[0]}, p1`);
3785 didSomething = true;
3786 }
3787 if (!room.battle.p2 && room.battle.playerNames[1] !== 'Player 2') {
3788 this.parse(`/addplayer ${room.battle.playerNames[1]}, p2`);
3789 didSomething = true;
3790 }
3791
3792 if (!didSomething) return this.errorReply(`Players could not be restored (maybe this battle already has two players?).`);
3793 },
3794 restoreplayershelp: [
3795 `/restoreplayers - Restore previous players in an imported input log.`,
3796 ],
3797
3798 joinbattle: 'joingame',
3799 joingame: function (target, room, user) {
3800 if (!room.game) return this.errorReply("This room doesn't have an active game.");
3801 if (!room.game.joinGame) return this.errorReply("This game doesn't support /joingame");
3802
3803 room.game.joinGame(user);
3804 },
3805
3806 leavebattle: 'leavegame',
3807 partbattle: 'leavegame',
3808 leavegame: function (target, room, user) {
3809 if (!room.game) return this.errorReply("This room doesn't have an active game.");
3810 if (!room.game.leaveGame) return this.errorReply("This game doesn't support /leavegame");
3811
3812 room.game.leaveGame(user);
3813 },
3814
3815 kickbattle: 'kickgame',
3816 kickgame: function (target, room, user) {
3817 if (!room.battle) return this.errorReply("You can only do this in battle rooms.");
3818 if (room.battle.tour || room.battle.rated) return this.errorReply("You can only do this in unrated non-tour battles.");
3819
3820 target = this.splitTarget(target);
3821 let targetUser = this.targetUser;
3822 if (!targetUser || !targetUser.connected) {
3823 return this.errorReply(`User ${this.targetUsername} not found.`);
3824 }
3825 if (!this.can('kick', targetUser)) return false;
3826
3827 if (room.game.leaveGame(targetUser)) {
3828 this.addModAction(`${targetUser.name} was kicked from a battle by ${user.name} ${(target ? ` (${target})` : ``)}`);
3829 this.modlog('KICKBATTLE', targetUser, target, {noip: 1, noalts: 1});
3830 } else {
3831 this.errorReply("/kickbattle - User isn't in battle.");
3832 }
3833 },
3834 kickbattlehelp: [`/kickbattle [username], [reason] - Kicks a user from a battle with reason. Requires: % @ * & ~`],
3835
3836 kickinactive: function (target, room, user) {
3837 this.parse(`/timer on`);
3838 },
3839
3840 timer: function (target, room, user) {
3841 target = toId(target);
3842 if (!room.game || !room.game.timer) {
3843 return this.errorReply(`You can only set the timer from inside a battle room.`);
3844 }
3845 const timer = room.game.timer;
3846 if (!timer.timerRequesters) {
3847 return this.sendReply(`This game's timer is managed by a different command.`);
3848 }
3849 if (!target) {
3850 if (!timer.timerRequesters.size) {
3851 return this.sendReply(`The game timer is OFF.`);
3852 }
3853 return this.sendReply(`The game timer is ON (requested by ${[...timer.timerRequesters].join(', ')})`);
3854 }
3855 const force = user.can('timer', null, room);
3856 if (!force && !room.game.players[user]) {
3857 return this.errorReply(`Access denied.`);
3858 }
3859 if (this.meansNo(target) || target === 'stop') {
3860 if (timer.timerRequesters.size) {
3861 timer.stop(force ? undefined : user);
3862 if (force) room.send(`|inactiveoff|Timer was turned off by staff. Please do not turn it back on until our staff say it's okay.`);
3863 } else {
3864 this.errorReply(`The timer is already off.`);
3865 }
3866 } else if (this.meansYes(target) || target === 'start') {
3867 timer.start(user);
3868 } else {
3869 this.errorReply(`"${target}" is not a recognized timer state.`);
3870 }
3871 },
3872
3873 autotimer: 'forcetimer',
3874 forcetimer: function (target, room, user) {
3875 target = toId(target);
3876 if (!this.can('autotimer')) return;
3877 if (this.meansNo(target) || target === 'stop') {
3878 Config.forcetimer = false;
3879 this.addModAction(`Forcetimer is now OFF: The timer is now opt-in. (set by ${user.name})`);
3880 } else if (this.meansYes(target) || target === 'start' || !target) {
3881 Config.forcetimer = true;
3882 this.addModAction(`Forcetimer is now ON: All battles will be timed. (set by ${user.name})`);
3883 } else {
3884 this.errorReply(`'${target}' is not a recognized forcetimer setting.`);
3885 }
3886 },
3887
3888 forcetie: 'forcewin',
3889 forcewin: function (target, room, user) {
3890 if (!this.can('forcewin')) return false;
3891 if (!room.battle) {
3892 this.errorReply("/forcewin - This is not a battle room.");
3893 return false;
3894 }
3895
3896 room.battle.endType = 'forced';
3897 if (!target) {
3898 room.battle.tie();
3899 this.modlog('FORCETIE');
3900 return false;
3901 }
3902 let targetUser = Users.getExact(target);
3903 if (!targetUser) return this.errorReply(`User '${target}' not found.`);
3904
3905 room.battle.win(targetUser);
3906 this.modlog('FORCEWIN', targetUser.userid);
3907 },
3908 forcewinhelp: [
3909 `/forcetie - Forces the current match to end in a tie. Requires: & ~`,
3910 `/forcewin [user] - Forces the current match to end in a win for a user. Requires: & ~`,
3911 ],
3912
3913 /*********************************************************
3914 * Challenging and searching commands
3915 *********************************************************/
3916
3917 '!search': true,
3918 search: function (target, room, user, connection) {
3919 if (target) {
3920 if (Config.laddermodchat) {
3921 let userGroup = user.group;
3922 if (Config.groupsranking.indexOf(userGroup) < Config.groupsranking.indexOf(Config.laddermodchat)) {
3923 let groupName = Config.groups[Config.laddermodchat].name || Config.laddermodchat;
3924 this.popupReply(`On this server, you must be of rank ${groupName} or higher to search for a battle.`);
3925 return false;
3926 }
3927 }
3928 Ladders(target).searchBattle(user, connection);
3929 } else {
3930 Ladders.cancelSearches(user);
3931 }
3932 },
3933
3934 '!cancelsearch': true,
3935 cancelsearch: function (target, room, user) {
3936 if (target) {
3937 Ladders(toId(target)).cancelSearch(user);
3938 } else {
3939 Ladders.cancelSearches(user);
3940 }
3941 },
3942
3943 '!challenge': true,
3944 chall: 'challenge',
3945 challenge: function (target, room, user, connection) {
3946 target = this.splitTarget(target);
3947 let targetUser = this.targetUser;
3948 if (!targetUser || !targetUser.connected) {
3949 return this.popupReply(`The user '${this.targetUsername}' was not found.`);
3950 }
3951 if (user.locked && !targetUser.locked) {
3952 return this.popupReply(`You are locked and cannot challenge unlocked users.`);
3953 }
3954 if (Punishments.isBattleBanned(user)) {
3955 return this.popupReply(`You are banned from battling and cannot challenge users.`);
3956 }
3957 if (Config.pmmodchat) {
3958 let userGroup = user.group;
3959 if (Config.groupsranking.indexOf(userGroup) < Config.groupsranking.indexOf(Config.pmmodchat)) {
3960 let groupName = Config.groups[Config.pmmodchat].name || Config.pmmodchat;
3961 this.popupReply(`Because moderated chat is set, you must be of rank ${groupName} or higher to challenge users.`);
3962 return false;
3963 }
3964 }
3965 Ladders(target).makeChallenge(connection, targetUser);
3966 },
3967
3968 '!blockchallenges': true,
3969 bch: 'blockchallenges',
3970 blockchall: 'blockchallenges',
3971 blockchalls: 'blockchallenges',
3972 blockchallenges: function (target, room, user) {
3973 if (user.blockChallenges) return this.errorReply("You are already blocking challenges!");
3974 user.blockChallenges = true;
3975 this.sendReply("You are now blocking all incoming challenge requests.");
3976 },
3977 blockchallengeshelp: [`/blockchallenges - Blocks challenges so no one can challenge you. Unblock them with /unblockchallenges.`],
3978
3979 '!allowchallenges': true,
3980 unbch: 'allowchallenges',
3981 unblockchall: 'allowchallenges',
3982 unblockchalls: 'allowchallenges',
3983 unblockchallenges: 'allowchallenges',
3984 allowchallenges: function (target, room, user) {
3985 if (!user.blockChallenges) return this.errorReply("You are already available for challenges!");
3986 user.blockChallenges = false;
3987 this.sendReply("You are available for challenges from now on.");
3988 },
3989 allowchallengeshelp: [`/unblockchallenges - Unblocks challenges so you can be challenged again. Block them with /blockchallenges.`],
3990
3991 '!cancelchallenge': true,
3992 cchall: 'cancelChallenge',
3993 cancelchallenge: function (target, room, user) {
3994 Ladders.cancelChallenging(user);
3995 },
3996
3997 '!accept': true,
3998 accept: function (target, room, user, connection) {
3999 target = this.splitTarget(target);
4000 if (target) return this.popupReply(`This command does not support specifying multiple users`);
4001 const targetUser = this.targetUser || this.pmTarget;
4002 if (!targetUser) return this.popupReply(`User "${this.targetUsername}" not found.`);
4003 Ladders.acceptChallenge(connection, targetUser);
4004 },
4005
4006 '!reject': true,
4007 reject: function (target, room, user) {
4008 target = toId(target);
4009 if (!target && this.pmTarget) target = this.pmTarget.userid;
4010 Ladders.rejectChallenge(user, target);
4011 },
4012
4013 '!useteam': true,
4014 saveteam: 'useteam',
4015 utm: 'useteam',
4016 useteam: function (target, room, user) {
4017 user.team = target;
4018 },
4019
4020 '!vtm': true,
4021 vtm: function (target, room, user, connection) {
4022 if (Monitor.countPrepBattle(connection.ip, connection)) {
4023 return;
4024 }
4025 if (!target) return this.errorReply("Provide a valid format.");
4026 let originalFormat = Dex.getFormat(target);
4027 // Note: The default here of [Gen 7] Pokebank Anything Goes isn't normally hit; since the web client will send a default format
4028 let format = originalFormat.effectType === 'Format' ? originalFormat : Dex.getFormat('[Gen 7] Pokebank Anything Goes');
4029 if (format.effectType !== 'Format') return this.popupReply("Please provide a valid format.");
4030
4031 TeamValidatorAsync(format.id).validateTeam(user.team).then(result => {
4032 let matchMessage = (originalFormat === format ? "" : `The format '${originalFormat.name}' was not found.`);
4033 if (result.charAt(0) === '1') {
4034 connection.popup(`${(matchMessage ? matchMessage + "\n\n" : "")}Your team is valid for ${format.name}.`);
4035 } else {
4036 connection.popup(`${(matchMessage ? matchMessage + "\n\n" : "")}Your team was rejected for the following reasons:\n\n- ${result.slice(1).replace(/\n/g, '\n- ')}`);
4037 }
4038 });
4039 },
4040
4041 /*********************************************************
4042 * Low-level
4043 *********************************************************/
4044
4045 '!crq': true,
4046 cmd: 'crq',
4047 query: 'crq',
4048 crq: function (target, room, user, connection) {
4049 // In emergency mode, clamp down on data returned from crq's
4050 let trustable = (!Config.emergency || (user.named && user.registered));
4051 let spaceIndex = target.indexOf(' ');
4052 let cmd = target;
4053 if (spaceIndex > 0) {
4054 cmd = target.substr(0, spaceIndex);
4055 target = target.substr(spaceIndex + 1);
4056 } else {
4057 target = '';
4058 }
4059 if (cmd === 'userdetails') {
4060 let targetUser = Users.get(target);
4061 if (!trustable || !targetUser) {
4062 connection.send('|queryresponse|userdetails|' + JSON.stringify({
4063 userid: toId(target),
4064 rooms: false,
4065 }));
4066 return false;
4067 }
4068 let roomList = {};
4069 for (let roomid of targetUser.inRooms) {
4070 if (roomid === 'global') continue;
4071 let targetRoom = Rooms.get(roomid);
4072 if (!targetRoom) continue; // shouldn't happen
4073 let roomData = {};
4074 if (targetRoom.isPrivate) {
4075 if (!user.inRooms.has(roomid) && !user.games.has(roomid)) continue;
4076 roomData.isPrivate = true;
4077 }
4078 if (targetRoom.battle) {
4079 let battle = targetRoom.battle;
4080 roomData.p1 = battle.p1 ? ' ' + battle.p1.name : '';
4081 roomData.p2 = battle.p2 ? ' ' + battle.p2.name : '';
4082 }
4083 if (targetRoom.auth && targetUser.userid in targetRoom.auth) {
4084 roomid = targetRoom.auth[targetUser.userid] + roomid;
4085 }
4086 roomList[roomid] = roomData;
4087 }
4088 if (!targetUser.connected) roomList = false;
4089 let userdetails = {
4090 userid: targetUser.userid,
4091 avatar: targetUser.avatar,
4092 group: targetUser.group,
4093 rooms: roomList,
4094 };
4095 connection.send('|queryresponse|userdetails|' + JSON.stringify(userdetails));
4096 } else if (cmd === 'roomlist') {
4097 if (!trustable) return false;
4098 connection.send('|queryresponse|roomlist|' + JSON.stringify({
4099 rooms: Rooms.global.getBattles(target),
4100 }));
4101 } else if (cmd === 'rooms') {
4102 if (!trustable) return false;
4103 connection.send('|queryresponse|rooms|' + JSON.stringify(
4104 Rooms.global.getRooms(user)
4105 ));
4106 } else if (cmd === 'laddertop') {
4107 if (!trustable) return false;
4108 Ladders(toId(target)).getTop().then(result => {
4109 connection.send('|queryresponse|laddertop|' + JSON.stringify(result));
4110 });
4111 } else {
4112 // default to sending null
4113 connection.send(`|queryresponse|${cmd}|null`);
4114 }
4115 },
4116
4117 '!trn': true,
4118 trn: function (target, room, user, connection) {
4119 if (target === user.name) return false;
4120
4121 let commaIndex = target.indexOf(',');
4122 let targetName = target;
4123 let targetRegistered = false;
4124 let targetToken = '';
4125 if (commaIndex >= 0) {
4126 targetName = target.substr(0, commaIndex);
4127 target = target.substr(commaIndex + 1);
4128 commaIndex = target.indexOf(',');
4129 targetRegistered = target;
4130 if (commaIndex >= 0) {
4131 targetRegistered = !!parseInt(target.substr(0, commaIndex));
4132 targetToken = target.substr(commaIndex + 1);
4133 }
4134 }
4135 user.rename(targetName, targetToken, targetRegistered, connection);
4136 },
4137
4138 a: function (target, room, user) {
4139 if (!this.can('rawpacket')) return false;
4140 // secret sysop command
4141 room.add(target);
4142 },
4143
4144 /*********************************************************
4145 * Help commands
4146 *********************************************************/
4147
4148 '!help': true,
4149 commands: 'help',
4150 h: 'help',
4151 '?': 'help',
4152 man: 'help',
4153 help: function (target, room, user) {
4154 if (!this.runBroadcast()) return;
4155 target = target.toLowerCase();
4156
4157 // overall
4158 if (target === 'help' || target === 'h' || target === '?' || target === 'commands') {
4159 this.sendReply("/help OR /h OR /? - Gives you help.");
4160 } else if (!target) {
4161 this.sendReply("COMMANDS: /msg, /reply, /logout, /challenge, /search, /rating, /whois");
4162 this.sendReply("OPTION COMMANDS: /nick, /avatar, /ignore, /away, /back, /timestamps, /highlight");
4163 this.sendReply("INFORMATIONAL COMMANDS: /data, /dexsearch, /movesearch, /itemsearch, /groups, /faq, /rules, /intro, /formatshelp, /othermetas, /learn, /analysis, /calc (replace / with ! to broadcast. Broadcasting requires: + % @ * # & ~)");
4164 if (user.group !== Config.groupsranking[0]) {
4165 this.sendReply("DRIVER COMMANDS: /warn, /mute, /hourmute, /unmute, /alts, /forcerename, /modlog, /modnote, /lock, /unlock, /battleban, /unbattleban, /announce, /redirect");
4166 this.sendReply("MODERATOR COMMANDS: /ban, /unban, /ip, /modchat");
4167 this.sendReply("LEADER COMMANDS: /declare, /forcetie, /forcewin, /promote, /demote, /banip, /host, /unbanall");
4168 }
4169 this.sendReply("For an overview of room commands, use /roomhelp");
4170 this.sendReply("For details of a specific command, use something like: /help data");
4171 } else {
4172 let altCommandHelp;
4173 let helpCmd;
4174 let targets = target.split(' ');
4175 let allCommands = Chat.commands;
4176 if (typeof allCommands[target] === 'string') {
4177 // If a function changes with command name, help for that command name will be searched first.
4178 altCommandHelp = `${target}help`;
4179 if (altCommandHelp in allCommands) {
4180 helpCmd = altCommandHelp;
4181 } else {
4182 helpCmd = `${allCommands[target]}help`;
4183 }
4184 } else if (targets.length > 1 && typeof allCommands[targets[0]] === 'object') {
4185 // Handle internal namespace commands
4186 let helpCmd = `${targets.pop()}help`;
4187 let namespace = allCommands[targets.shift()];
4188 for (const t of targets) {
4189 if (!namespace[t]) return this.errorReply(`Help for the command '${target}' was not found. Try /help for general help.`);
4190 namespace = namespace[t];
4191 }
4192 if (typeof namespace[helpCmd] === 'object') return this.sendReply(namespace[helpCmd].join('\n'));
4193 if (typeof namespace[helpCmd] === 'function') return this.run(namespace[helpCmd]);
4194 return this.errorReply(`Help for the command '${target}' was not found. Try /help for general help.`);
4195 } else {
4196 helpCmd = `${target}help`;
4197 }
4198 if (helpCmd in allCommands) {
4199 if (typeof allCommands[helpCmd] === 'function') {
4200 // If the help command is a function, parse it instead
4201 this.run(allCommands[helpCmd]);
4202 } else if (Array.isArray(allCommands[helpCmd])) {
4203 this.sendReply(allCommands[helpCmd].join('\n'));
4204 }
4205 } else {
4206 this.errorReply(`Help for the command '${target}' was not found. Try /help for general help.`);
4207 }
4208 }
4209 },
4210
4211};
4212
4213exports.commands = commands;
4214
4215process.nextTick(() => {
4216 // We might want to migrate most of this to a JSON schema of command attributes.
4217 Chat.multiLinePattern.register(
4218 '>>>? ', '/(?:room|staff)intro ', '/(?:staff)?topic ', '/(?:add|widen)datacenters ', '/bash ', '!code ', '/code ',
4219 '/importinputlog '
4220 );
4221});
4222© 2018 GitHub, Inc.
4223Terms
4224Privacy
4225Security
4226Status
4227Help
4228Contact GitHub
4229Pricing
4230API
4231Training
4232Blog
4233About