Spaces:
Running
Running
File size: 74,373 Bytes
5c2ed06 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 |
/**
* Room settings commands
* Pokemon Showdown - http://pokemonshowdown.com/
*
* Commands for settings relating to room setting filtering.
*
* @license MIT
*/
import { Utils } from '../../lib';
import type { EffectiveGroupSymbol, RoomPermission } from '../user-groups';
const RANKS = Config.groupsranking;
const SLOWCHAT_MINIMUM = 2;
const SLOWCHAT_MAXIMUM = 60;
const SLOWCHAT_USER_REQUIREMENT = 10;
export const sections = [
'official', 'battleformats', 'languages', 'entertainment', 'gaming', 'lifehobbies', 'onsitegames',
] as const;
export type RoomSection = typeof sections[number];
export const RoomSections: { sectionNames: { [k in RoomSection]: string }, sections: readonly RoomSection[] } = {
sectionNames: {
official: 'Official',
battleformats: 'Battle formats',
languages: 'Languages',
entertainment: 'Entertainment',
gaming: 'Gaming',
lifehobbies: 'Life & hobbies',
onsitegames: 'On-site games',
},
sections,
};
export const commands: Chat.ChatCommands = {
roomsetting: 'roomsettings',
roomsettings(target, room, user, connection) {
room = this.requireRoom();
if (room.battle) return this.errorReply("This command cannot be used in battle rooms.");
let uhtml = 'uhtml';
if (!target) {
room.update();
} else {
void this.parse(`/${target}`);
uhtml = 'uhtmlchange';
}
let output = Utils.html`<div class="infobox">Room Settings for ${room.title}<br />`;
for (const handler of Chat.roomSettings) {
const setting = handler(room, user, connection);
if (typeof setting.permission === 'string') setting.permission = user.can(setting.permission, null, room);
output += `<strong>${setting.label}:</strong> <br />`;
for (const option of setting.options) {
// disabled button
if (option[1] === true) {
output += Utils.html`<button class="button disabled" style="font-weight:bold;color:#575757;` +
`background:#d3d3d3;">${option[0]}</button> `;
} else {
// only show proper buttons if we have the permissions to use them
if (!setting.permission) continue;
output += Utils.html`<button class="button" name="send" value="/roomsetting ${option[1]}">${option[0]}</button> `;
}
}
output += `<br />`;
}
output += '</div>';
user.sendTo(room, `|${uhtml}|roomsettings|${output}`);
},
roomsettingshelp: [`/roomsettings - Shows current room settings with buttons to change them (if you can).`],
modchat(target, room, user) {
room = this.requireRoom();
if (!target) {
const modchatSetting = (room.settings.modchat || "OFF");
return this.sendReply(`Moderated chat is currently set to: ${modchatSetting}`);
}
if (user.locked) { // would put this below but it behaves weird if there's no modchat set
return this.errorReply(`/modchat - Access denied.`);
} else {
this.checkCan('modchat', null, room);
}
if (
room.settings.modchat && room.settings.modchat.length <= 1 &&
!room.auth.atLeast(user, room.settings.modchat) &&
// Upper Staff should probably be able to set /modchat ~ in secret rooms
!user.can('bypassall')
) {
return this.errorReply(`/modchat - Access denied for changing a setting currently at ${room.settings.modchat}.`);
}
if ((room as any).requestModchat) {
const error = (room as GameRoom).requestModchat(user);
if (error) return this.errorReply(error);
}
// only admins can force modchat on a forced public battle
if (room.battle?.forcedSettings.modchat && !user.can('rangeban')) {
return this.errorReply(
`This battle is required to have modchat on due to one of the players having a username that starts with ` +
`${room.battle.forcedSettings.modchat}.`
);
}
target = target.toLowerCase().trim();
const currentModchat = room.settings.modchat;
switch (target) {
case 'off':
case 'false':
case 'no':
case 'disable':
room.settings.modchat = null;
break;
case 'ac':
case 'autoconfirmed':
room.settings.modchat = 'autoconfirmed';
break;
case 'trusted':
room.settings.modchat = 'trusted';
break;
case 'player':
target = Users.PLAYER_SYMBOL;
/* falls through */
default:
if (!Users.Auth.isAuthLevel(target) || ['‽', '!'].includes(target)) {
this.errorReply(`The rank '${target}' was unrecognized as a modchat level.`);
return this.parse('/help modchat');
}
// Users shouldn't be able to set modchat above their own rank (except for ROs who are also Upper Staff)
const modchatLevelHigherThanUserRank = !room.auth.atLeast(user, target) && !user.can('bypassall');
if (modchatLevelHigherThanUserRank || !Users.Auth.hasPermission(user, 'modchat', target as GroupSymbol, room)) {
return this.errorReply(`/modchat - Access denied for setting to ${target}.`);
}
room.settings.modchat = target;
break;
}
if (currentModchat === room.settings.modchat) {
return this.errorReply(`Modchat is already set to ${currentModchat || 'off'}.`);
}
if (!room.settings.modchat) {
this.add("|raw|<div class=\"broadcast-blue\"><strong>Moderated chat was disabled!</strong><br />Anyone may talk now.</div>");
} else {
const modchatSetting = Utils.escapeHTML(room.settings.modchat);
this.add(`|raw|<div class="broadcast-red"><strong>Moderated chat was set to ${modchatSetting}!</strong><br />Only users of rank ${modchatSetting} and higher can talk.</div>`);
}
if ((room as GameRoom).requestModchat && !room.settings.modchat) (room as GameRoom).requestModchat(null);
this.privateModAction(`${user.name} set modchat to ${room.settings.modchat || "off"}`);
this.modlog('MODCHAT', null, `to ${room.settings.modchat || "false"}`);
room.saveSettings();
},
modchathelp: [
`/modchat [off/autoconfirmed/trusted/+/%/@/*/player/#/~] - Set the level of moderated chat. Requires: % \u2606 for off/autoconfirmed/+/player options, * @ # ~ for all the options`,
],
automodchat(target, room, user) {
room = this.requireRoom();
if (!target) {
if (!room.settings.autoModchat) return this.sendReply(`This room has automodchat OFF.`);
const { rank: curRank, time: curTime } = room.settings.autoModchat;
return this.sendReply(`Automodchat is currently set to set modchat to ${curRank} after ${curTime} minutes.`);
}
this.checkCan('declare', null, room);
if (this.meansNo(toID(target))) {
if (!room.settings.autoModchat) return this.errorReply(`Auto modchat is not set.`);
delete room.settings.autoModchat;
room.saveSettings();
if (room.modchatTimer) clearTimeout(room.modchatTimer); // fallback just in case (should never happen)
this.privateModAction(`${user.name} turned off automatic modchat.`);
return this.modlog(`AUTOMODCHAT`, null, 'OFF');
}
let [rawTime, rank] = Utils.splitFirst(target, ',').map(i => i.trim()) as [string, GroupSymbol];
if (!rawTime) {
return this.parse(`/help automodchat`);
}
if (!rank) {
if (room.settings.autoModchat) {
rank = room.settings.autoModchat.rank;
} else {
return this.parse(`/help automodchat`);
}
}
const validGroups = [...Config.groupsranking as string[], 'trusted', 'autoconfirmed'];
if (!validGroups.includes(rank)) {
return this.errorReply(`Invalid rank.`);
}
const time = parseInt(rawTime);
if (isNaN(time) || time > 480 || time < 5) {
return this.errorReply("Invalid duration. Choose a number under 480 (in minutes) and over 5 minutes.");
}
room.settings.autoModchat = {
rank, time, active: false,
};
this.privateModAction(`${user.name} set automodchat to rank ${rank} and timeout ${time} minutes.`);
this.modlog(`AUTOMODCHAT`, null, `${rank}: ${time} minutes`);
room.saveSettings();
},
automodchathelp: [
`/automodchat [number], [rank] - Sets modchat [rank] to automatically turn on after [number] minutes with no staff.`,
`[number] must be between 5 and 480. Requires: # ~`,
`/automodchat off - Turns off automodchat.`,
],
ionext() {
this.errorReply(`"ionext" is an outdated feature. Hidden battles now have password-protected URLs, making them fully secure against eavesdroppers.`);
this.errorReply(`You probably want to switch from /ionext to /hidenext, and from /ioo to /hideroom`);
},
ioo() {
this.errorReply(`"ioo" is an outdated feature. Hidden battles now have password-protected URLs, making them fully secure against eavesdroppers.`);
this.errorReply(`You probably want to switch from /ioo to /hideroom`);
},
inviteonlynext(target, room, user) {
const groupConfig = Config.groups[Users.PLAYER_SYMBOL];
if (!groupConfig?.editprivacy) return this.errorReply(`/ionext - Access denied.`);
if (this.meansNo(target)) {
user.battleSettings.inviteOnly = false;
user.update();
this.sendReply("Your next battle will be publicly visible.");
} else {
user.battleSettings.inviteOnly = true;
user.update();
this.sendReply(`Your next battle will be invite-only${Rooms.RoomBattle.battleForcedSetting(user, 'privacy') ? `, unless it is rated` : ``}.`);
}
},
inviteonlynexthelp: [
`/inviteonlynext - Sets your next battle to be invite-only.`,
`/inviteonlynext off - Sets your next battle to be publicly visible.`,
],
inviteonly(target, room, user, connection, cmd) {
room = this.requireRoom();
if (!target) return this.parse('/help inviteonly');
if (this.meansYes(target)) {
return this.parse("/modjoin %");
} else {
return this.parse(`/modjoin ${target}`);
}
},
inviteonlyhelp: [
`/inviteonly [on|off] - Sets modjoin %. Users can't join unless invited with /invite. Requires: # ~`,
`/ioo - Shortcut for /inviteonly on`,
`/inviteonlynext OR /ionext - Sets your next battle to be invite-only.`,
`/ionext off - Sets your next battle to be publicly visible.`,
],
modjoin(target, room, user) {
room = this.requireRoom();
if (!target) {
const modjoinSetting = room.settings.modjoin === true ? "SYNC" : room.settings.modjoin || "OFF";
return this.sendReply(`Modjoin is currently set to: ${modjoinSetting}`);
}
if (room.battle) {
this.checkCan('editprivacy', null, room);
if (room.battle.forcedSettings.privacy) {
return this.errorReply(
`This battle is required to be public due to a player having a name prefixed by '${room.battle.forcedSettings.privacy}'.`
);
}
if (room.battle.inviteOnlySetter && !user.can('mute', null, room) && room.battle.inviteOnlySetter !== user.id) {
return this.errorReply(`Only the person who set this battle to be invite-only can turn it off.`);
}
room.battle.inviteOnlySetter = user.id;
} else if (room.settings.isPersonal) {
this.checkCan('editroom', null, room);
} else {
this.checkCan('makeroom');
}
if (room.tour && !room.tour.allowModjoin) {
return this.errorReply(`You can't do this in tournaments where modjoin is prohibited.`);
}
if (target === 'player') target = Users.PLAYER_SYMBOL;
if (this.meansNo(target)) {
if (!room.settings.modjoin) return this.errorReply(`Modjoin is already turned off in this room.`);
room.settings.modjoin = null;
this.add(`|raw|<div class="broadcast-blue"><strong>This room is no longer invite only!</strong><br />Anyone may now join.</div>`);
this.addModAction(`${user.name} turned off modjoin.`);
this.modlog('MODJOIN', null, 'OFF');
if (room.battle) room.battle.inviteOnlySetter = null;
room.saveSettings();
return;
} else if (target === 'sync') {
if (room.settings.modjoin === true) return this.errorReply(`Modjoin is already set to sync modchat in this room.`);
room.settings.modjoin = true;
this.add(`|raw|<div class="broadcast-red"><strong>Moderated join is set to sync with modchat!</strong><br />Only users who can speak in modchat can join.</div>`);
this.addModAction(`${user.name} set modjoin to sync with modchat.`);
this.modlog('MODJOIN SYNC');
} else if (target === 'ac' || target === 'autoconfirmed') {
if (room.settings.modjoin === 'autoconfirmed') return this.errorReply(`Modjoin is already set to autoconfirmed.`);
room.settings.modjoin = 'autoconfirmed';
this.add(`|raw|<div class="broadcast-red"><strong>Moderated join is set to autoconfirmed!</strong><br />Users must be rank autoconfirmed or invited with <code>/invite</code> to join</div>`);
this.addModAction(`${user.name} set modjoin to autoconfirmed.`);
this.modlog('MODJOIN', null, 'autoconfirmed');
} else if (Users.Auth.isAuthLevel(target) && !['‽', '!'].includes(target)) {
if (room.battle && !user.can('makeroom') && !'+%'.includes(target)) {
return this.errorReply(`/modjoin - Access denied from setting modjoin past % in battles.`);
}
if (room.settings.isPersonal && !user.can('makeroom') && !'+%'.includes(target)) {
return this.errorReply(`/modjoin - Access denied from setting modjoin past % in group chats.`);
}
if (room.settings.modjoin === target) return this.errorReply(`Modjoin is already set to ${target} in this room.`);
room.settings.modjoin = target;
this.add(`|raw|<div class="broadcast-red"><strong>This room is now invite only!</strong><br />Users must be rank ${target} or invited with <code>/invite</code> to join</div>`);
this.addModAction(`${user.name} set modjoin to ${target}.`);
this.modlog('MODJOIN', null, target);
} else {
this.errorReply(`Unrecognized modjoin setting.`);
void this.parse('/help modjoin');
return false;
}
room.saveSettings();
if (target === 'sync' && !room.settings.modchat) {
const lowestGroup = Config.groupsranking.find(group => {
const groupInfo = Users.Auth.getGroup(group);
return (
groupInfo.symbol !== Users.Auth.defaultSymbol() &&
room.auth.atLeast(user, group) &&
Users.Auth.isValidSymbol(groupInfo.symbol)
);
});
if (lowestGroup) void this.parse(`/modchat ${lowestGroup}`);
}
if (!room.settings.isPrivate) return this.parse('/hiddenroom');
},
modjoinhelp: [
`/modjoin [+|%|@|*|player|~|#|off] - Sets modjoin. Users lower than the specified rank can't join this room unless they have a room rank. Requires: \u2606 # ~`,
`/modjoin [sync|off] - Sets modjoin. Only users who can speak in modchat can join this room. Requires: \u2606 # ~`,
],
roomlanguage(target, room, user) {
room = this.requireRoom();
if (!target) {
return this.sendReply(`This room's primary language is ${Chat.languages.get(room.settings.language || '') || 'English'}`);
}
this.checkCan('editroom', null, room);
const targetLanguage = toID(target);
if (!Chat.languages.has(targetLanguage)) return this.errorReply(`"${target}" is not a supported language.`);
room.settings.language = targetLanguage === 'english' ? false : targetLanguage;
room.saveSettings();
this.modlog(`LANGUAGE`, null, Chat.languages.get(targetLanguage));
this.sendReply(`The room's language has been set to ${Chat.languages.get(targetLanguage)}`);
},
roomlanguagehelp: [
`/roomlanguage [language] - Sets the the language for the room, which changes language of a few commands. Requires # ~`,
`Supported Languages: English, Spanish, Italian, French, Simplified Chinese, Traditional Chinese, Japanese, Hindi, Turkish, Dutch, German.`,
],
slowchat(target, room, user) {
room = this.requireRoom();
if (!target) {
const slowchatSetting = (room.settings.slowchat || "OFF");
return this.sendReply(`Slow chat is currently set to: ${slowchatSetting}`);
}
this.checkChat();
this.checkCan('modchat', null, room);
let targetInt = parseInt(target);
if (this.meansNo(target)) {
if (!room.settings.slowchat) return this.errorReply(`Slow chat is already disabled in this room.`);
room.settings.slowchat = false;
} else if (targetInt) {
if (!user.can('bypassall') && room.userCount < SLOWCHAT_USER_REQUIREMENT) {
return this.errorReply(`This room must have at least ${SLOWCHAT_USER_REQUIREMENT} users to set slowchat; it only has ${room.userCount} right now.`);
}
if (room.settings.slowchat === targetInt) {
return this.errorReply(`Slow chat is already set to ${room.settings.slowchat} seconds in this room.`);
}
if (targetInt < SLOWCHAT_MINIMUM) targetInt = SLOWCHAT_MINIMUM;
if (targetInt > SLOWCHAT_MAXIMUM) targetInt = SLOWCHAT_MAXIMUM;
room.settings.slowchat = targetInt;
} else {
return this.parse("/help slowchat");
}
const slowchatSetting = (room.settings.slowchat || "OFF");
this.privateModAction(`${user.name} set slowchat to ${slowchatSetting}`);
this.modlog('SLOWCHAT', null, `${slowchatSetting}`);
room.saveSettings();
},
slowchathelp: [
`/slowchat [number] - Sets a limit on how often users in the room can send messages, between 2 and 60 seconds. Requires % @ # ~`,
`/slowchat off - Disables slowchat in the room. Requires % @ # ~`,
],
permission: 'permissions',
permissions: {
clear: 'set',
set(target, room, user) {
const [perm, displayRank] = this.splitOne(target);
room = this.requireRoom();
let rank = displayRank;
if (rank === 'default') rank = '';
if (rank === 'all users') rank = Users.Auth.defaultSymbol();
if (!room.persist) return this.errorReply(`This room does not allow customizing permissions.`);
if (!target || !perm) return this.parse(`/permissions help`);
if (rank && rank !== 'whitelist' && !Config.groupsranking.includes(rank as EffectiveGroupSymbol)) {
return this.errorReply(`${rank} is not a valid rank.`);
}
const validPerms = Users.Auth.supportedRoomPermissions(room);
const sanitizedPerm = perm.replace('!', '/'); // handles ! commands so we don't have to add commands to the array twice
if (!validPerms.some(p => (
// we need to check the raw permissions also because broadcast permissions are listed with the !
p === sanitizedPerm || p === perm ||
p.startsWith(`${sanitizedPerm} `) || p.startsWith(`${perm} `)
))) {
return this.errorReply(`${perm} is not a valid room permission.`);
}
if (!room.auth.atLeast(user, '#')) {
return this.errorReply(`/permissions set - You must be at least a Room Owner to set permissions.`);
}
if (
Users.Auth.ROOM_PERMISSIONS.includes(perm as RoomPermission) &&
!Users.Auth.hasPermission(user, perm, null, room)
) {
return this.errorReply(`/permissions set - You can't set the permission "${perm}" because you don't have it.`);
}
const currentPermissions = room.settings.permissions || {};
if (currentPermissions[perm] === (rank || undefined)) {
return this.errorReply(`${perm} is already set to ${displayRank || 'default'}.`);
}
if (rank) {
currentPermissions[perm] = rank as GroupSymbol;
room.settings.permissions = currentPermissions;
} else {
delete currentPermissions[perm];
if (!Object.keys(currentPermissions).length) delete room.settings.permissions;
}
room.saveSettings();
this.modlog(`SETPERMISSION`, null, `${perm}: ${displayRank}`);
this.refreshPage(`permissions-${room.roomid}`);
return this.privateModAction(`${user.name} set the required rank for ${perm} to ${displayRank}.`);
},
sethelp: [
`/permissions set [command], [rank symbol] - sets the required permission to use the command [command] to [rank]. Requires: # ~`,
`/permissions clear [command] - resets the required permission to use the command [command] to the default. Requires: # ~`,
],
view(target, room, user) {
room = this.requireRoom();
return this.parse(`/join view-permissions-${room.roomid}`);
},
help: '',
''(target, room, user) {
room = this.requireRoom();
const allPermissions = Users.Auth.supportedRoomPermissions(room);
const permissionGroups = allPermissions.filter(perm => !perm.startsWith('/') && !perm.startsWith('!'));
const permissions = allPermissions.filter(perm => {
const handler = Chat.parseCommand(perm)?.handler;
if (handler?.isPrivate && !user.can('lock')) return false;
return (perm.startsWith('/') || perm.startsWith('!')) && !perm.includes(' ');
});
const subPermissions = allPermissions
.filter(perm => (perm.startsWith('/') || perm.startsWith('!')) && perm.includes(' '))
.filter(perm => {
const handler = Chat.parseCommand(perm)?.handler;
if (handler?.isPrivate && !user.can('lock')) return false;
return (perm.startsWith('/') || perm.startsWith('!')) && perm.includes(' ');
});
const subPermissionsByNamespace: { [k: string]: string[] } = {};
for (const perm of subPermissions) {
const [namespace] = perm.split(' ', 1);
if (!subPermissionsByNamespace[namespace]) subPermissionsByNamespace[namespace] = [];
subPermissionsByNamespace[namespace].push(perm);
}
let buffer = `<strong>Room permissions help:</strong><hr />`;
buffer += `<p><strong>Usage: </strong><br />`;
buffer += `<code>/permissions set [permission], [rank symbol]</code><br />`;
buffer += `<code>/permissions clear [permission]</code><br />`;
buffer += `<code>/permissions view</code></p>`;
buffer += `<p><strong>Group permissions:</strong> (will affect multiple commands or part of one command)<br />`;
buffer += `<code>` + permissionGroups.join(`</code> <code>`) + `</code></p>`;
buffer += `<p><details class="readmore"><summary><strong>Single-command permissions:</strong> (will affect one command)</summary>`;
buffer += `Permissions starting with <code>!</code> are for broadcasting the command, not using it.<br />`;
buffer += `<code>` + permissions.join(`</code> <code>`) + `</code></details></p>`;
buffer += `<p><details class="readmore"><summary><strong>Sub-commands:</strong> (will affect one sub-command, like /roomevents view)</summary>`;
for (const subPerms of Object.values(subPermissionsByNamespace)) {
buffer += `<br /><code>` + subPerms.join(`</code> <code>`) + `</code><br />`;
}
buffer += `</details></p>`;
return this.sendReplyBox(buffer);
},
},
stretching: 'stretchfilter',
stretchingfilter: 'stretchfilter',
stretchfilter(target, room, user) {
room = this.requireRoom();
if (!target) {
const stretchSetting = (room.settings.filterStretching ? "ON" : "OFF");
return this.sendReply(`This room's stretch filter is currently: ${stretchSetting}`);
}
this.checkChat();
this.checkCan('editroom', null, room);
if (this.meansYes(target)) {
if (room.settings.filterStretching) return this.errorReply(`This room's stretch filter is already ON`);
room.settings.filterStretching = true;
} else if (this.meansNo(target)) {
if (!room.settings.filterStretching) return this.errorReply(`This room's stretch filter is already OFF`);
room.settings.filterStretching = false;
} else {
return this.parse("/help stretchfilter");
}
const stretchSetting = (room.settings.filterStretching ? "ON" : "OFF");
this.privateModAction(`${user.name} turned the stretch filter ${stretchSetting}`);
this.modlog('STRETCH FILTER', null, stretchSetting);
room.saveSettings();
},
stretchfilterhelp: [
`/stretchfilter [on/off] - Toggles filtering messages in the room for stretchingggggggg. Requires # ~`,
],
capitals: 'capsfilter',
capitalsfilter: 'capsfilter',
capsfilter(target, room, user) {
room = this.requireRoom();
if (!target) {
const capsSetting = (room.settings.filterCaps ? "ON" : "OFF");
return this.sendReply(`This room's caps filter is currently: ${capsSetting}`);
}
this.checkChat();
this.checkCan('editroom', null, room);
if (this.meansYes(target)) {
if (room.settings.filterCaps) return this.errorReply(`This room's caps filter is already ON`);
room.settings.filterCaps = true;
} else if (this.meansNo(target)) {
if (!room.settings.filterCaps) return this.errorReply(`This room's caps filter is already OFF`);
room.settings.filterCaps = false;
} else {
return this.parse("/help capsfilter");
}
const capsSetting = (room.settings.filterCaps ? "ON" : "OFF");
this.privateModAction(`${user.name} turned the caps filter ${capsSetting}`);
this.modlog('CAPS FILTER', null, capsSetting);
room.saveSettings();
},
capsfilterhelp: [`/capsfilter [on/off] - Toggles filtering messages in the room for EXCESSIVE CAPS. Requires # ~`],
emojis: 'emojifilter',
emoji: 'emojifilter',
emojifilter(target, room, user) {
room = this.requireRoom();
if (!target) {
const emojiSetting = (room.settings.filterEmojis ? "ON" : "OFF");
return this.sendReply(`This room's emoji filter is currently: ${emojiSetting}`);
}
this.checkChat();
this.checkCan('editroom', null, room);
if (this.meansYes(target)) {
if (room.settings.filterEmojis) return this.errorReply(`This room's emoji filter is already ON`);
room.settings.filterEmojis = true;
} else if (this.meansNo(target)) {
if (!room.settings.filterEmojis) return this.errorReply(`This room's emoji filter is already OFF`);
room.settings.filterEmojis = false;
} else {
return this.parse("/help emojifilter");
}
const emojiSetting = (room.settings.filterEmojis ? "ON" : "OFF");
this.privateModAction(`${user.name} turned the emoji filter ${emojiSetting}`);
this.modlog('EMOJI FILTER', null, emojiSetting);
room.saveSettings();
},
emojifilterhelp: [`/emojifilter [on/off] - Toggles filtering messages in the room for emojis. Requires # ~`],
linkfilter(target, room, user) {
room = this.requireRoom();
if (!target) {
return this.sendReply(
`This room's link filter is currently: ${room.settings.filterEmojis ? "ON" : "OFF"}`
);
}
this.checkChat();
this.checkCan('editroom', null, room);
if (this.meansYes(target)) {
if (room.settings.filterLinks) return this.errorReply(`This room's link filter is already ON`);
room.settings.filterLinks = true;
} else if (this.meansNo(target)) {
if (!room.settings.filterLinks) return this.errorReply(`This room's link filter is already OFF`);
room.settings.filterLinks = false;
} else {
return this.parse("/help linkfilter");
}
const setting = (room.settings.filterLinks ? "ON" : "OFF");
this.privateModAction(`${user.name} turned the link filter ${setting}`);
this.modlog('LINK FILTER', null, setting);
room.saveSettings();
},
linkfilterhelp: [`/linkfilter [on/off] - Toggles filtering messages in the room for links. Requires # ~`],
banwords: 'banword',
banword: {
regexadd: 'add',
addregex: 'add',
add(target, room, user, connection, cmd) {
room = this.requireRoom();
if (!target || target === ' ') return this.parse('/help banword');
this.checkCan('declare', null, room);
const regex = cmd.includes('regex');
if (regex && !user.can('makeroom')) return this.errorReply("Regex banwords are only allowed for administrators.");
if (!room.settings.banwords) room.settings.banwords = [];
// Most of the regex code is copied from the client. TODO: unify them?
// Regex banwords can have commas in the {1,5} pattern
let words = (regex ? target.match(/[^,]+(,\d*}[^,]*)?/g)! : target.split(','))
.map(word => word.replace(/\n/g, '').trim()).filter(word => word.length > 0);
if (!words || words.length === 0) return this.parse('/help banword');
// Escape any character with a special meaning in regex
if (!regex) {
words = words.map(word => {
if (/[\\^$*+?()|{}[\]]/.test(word) && user.can('rangeban')) {
this.errorReply(`"${word}" might be a regular expression, did you mean "/banword addregex"?`);
}
return Utils.escapeRegex(word);
});
}
// PS adds a preamble to the banword regex that's 32 chars long
let banwordRegexLen = (room.banwordRegex instanceof RegExp) ? room.banwordRegex.source.length : 32;
for (const word of words) {
Chat.validateRegex(word);
if (room.settings.banwords.includes(word)) return this.errorReply(`${word} is already a banned phrase.`);
// Banword strings are joined, so account for the first string not having the prefix
banwordRegexLen += (banwordRegexLen === 32) ? word.length : `|${word}`.length;
// RegExp instances whose source is greater than or equal to
// v8's RegExpMacroAssembler::kMaxRegister in length will crash
// the server on compile. In this case, that would happen each
// time a chat message gets tested for any banned phrases.
if (banwordRegexLen >= (1 << 16 - 1)) {
return this.errorReply("This room has too many banned phrases to add the ones given.");
}
}
for (const word of words) {
room.settings.banwords.push(word);
}
room.banwordRegex = null;
if (words.length > 1) {
this.privateModAction(`The banwords ${words.map(w => `'${w}'`).join(', ')} were added by ${user.name}.`);
this.modlog('BANWORD', null, words.map(w => `'${w}'`).join(', '));
this.sendReply(`Banned phrases successfully added.`);
} else {
this.privateModAction(`The banword '${words[0]}' was added by ${user.name}.`);
this.modlog('BANWORD', null, words[0]);
this.sendReply(`Banned phrase successfully added.`);
}
this.sendReply(`The list is currently: ${room.settings.banwords.join(', ')}`);
room.saveSettings();
},
delete(target, room, user) {
room = this.requireRoom();
if (!target) return this.parse('/help banword');
this.checkCan('declare', null, room);
if (!room.settings.banwords) return this.errorReply("This room has no banned phrases.");
const regexMatch = target.match(/[^,]+(,\d*}[^,]*)?/g);
if (!regexMatch) return this.parse('/help banword');
const words = regexMatch.map(word => word.replace(/\n/g, '').trim()).filter(word => word.length > 0);
for (const word of words) {
if (!room.settings.banwords.includes(word)) return this.errorReply(`${word} is not a banned phrase in this room.`);
}
room.settings.banwords = room.settings.banwords.filter(w => !words.includes(w));
room.banwordRegex = null;
if (words.length > 1) {
this.privateModAction(`The banwords ${words.map(w => `'${w}'`).join(', ')} were removed by ${user.name}.`);
this.modlog('UNBANWORD', null, words.map(w => `'${w}'`).join(', '));
this.sendReply(`Banned phrases successfully deleted.`);
} else {
this.privateModAction(`The banword '${words[0]}' was removed by ${user.name}.`);
this.modlog('UNBANWORD', null, words[0]);
this.sendReply(`Banned phrase successfully deleted.`);
}
if (!room.settings.banwords.length) room.settings.banwords = undefined;
this.sendReply(
room.settings.banwords ?
`The list is now: ${room.settings.banwords.join(', ')}` :
`The list is now empty.`
);
room.saveSettings();
},
list(target, room, user) {
room = this.requireRoom();
this.checkCan('mute', null, room);
if (!room.settings.banwords?.length) {
return this.sendReply("This room has no banned phrases.");
}
return this.sendReply(`Banned phrases in room ${room.roomid}: ${room.settings.banwords.join(', ')}`);
},
""(target, room, user) {
return this.parse("/help banword");
},
},
banwordhelp: [
`/banword add [words] - Adds the comma-separated list of phrases to the banword list of the current room. Requires: # ~`,
`/banword addregex [words] - Adds the comma-separated list of regular expressions to the banword list of the current room. Requires ~`,
`/banword delete [words] - Removes the comma-separated list of phrases from the banword list. Requires: # ~`,
`/banword list - Shows the list of banned words in the current room. Requires: % @ # ~`,
],
showapprovals(target, room, user) {
room = this.requireRoom();
this.checkCan('declare', null, room);
target = toID(target);
if (!target) {
return this.sendReply(`Approvals are currently ${room.settings.requestShowEnabled ? `ENABLED` : `DISABLED`} for ${room}.`);
}
if (this.meansNo(target)) {
if (!room.settings.requestShowEnabled) return this.errorReply(`Approvals are already disabled.`);
room.settings.requestShowEnabled = undefined;
this.privateModAction(`${user.name} disabled approvals in this room.`);
} else if (this.meansYes(target)) {
if (room.settings.requestShowEnabled) return this.errorReply(`Approvals are already enabled.`);
room.settings.requestShowEnabled = true;
this.privateModAction(`${user.name} enabled the use of media approvals in this room.`);
if (!room.settings.permissions || room.settings.permissions['/show'] === '@') {
this.privateModAction(
`Note: Due to this room's settings, Drivers aren't allowed to use /show directly, ` +
`but will be able to request and approve each other's /requestshow`
);
}
} else {
return this.errorReply(`Unrecognized setting for approvals. Use 'on' or 'off'.`);
}
room.saveSettings();
return this.modlog(`SHOWAPPROVALS`, null, `${this.meansYes(target) ? `ON` : `OFF`}`);
},
showapprovalshelp: [
`/showapprovals [setting] - Enable or disable the use of media approvals in the current room.`,
`Requires: # ~`,
],
showmedia(target, room, user) {
this.errorReply(`/showmedia has been deprecated. Use /permissions instead.`);
return this.parse(`/help permissions`);
},
hightraffic(target, room, user) {
room = this.requireRoom();
if (!target) {
return this.sendReply(`This room is: ${room.settings.highTraffic ? 'high' : 'low'} traffic`);
}
this.checkCan('makeroom');
if (this.meansYes(target)) {
room.settings.highTraffic = true;
} else if (this.meansNo(target)) {
room.settings.highTraffic = undefined;
} else {
return this.parse('/help hightraffic');
}
room.saveSettings();
this.modlog(`HIGHTRAFFIC`, null, `${!!room.settings.highTraffic}`);
this.addModAction(`This room was marked as ${room.settings.highTraffic ? 'high' : 'low'} traffic by ${user.name}.`);
},
hightraffichelp: [
`/hightraffic [on|off] - (Un)marks a room as a high traffic room. Requires ~`,
`When a room is marked as high-traffic, PS requires all messages sent to that room to contain at least 2 letters.`,
],
/*********************************************************
* Room management
*********************************************************/
makeprivatechatroom: 'makechatroom',
makepublicchatroom: 'makechatroom',
makechatroom(target, room, user, connection, cmd) {
room = this.requireRoom();
this.checkCan('makeroom');
const id = toID(target);
if (!id || this.cmd === 'makechatroom') return this.parse('/help makechatroom');
if (!Rooms.global.addChatRoom(target)) {
return this.errorReply(`The room '${target}' already exists or it is using an invalid title.`);
}
const targetRoom = Rooms.search(target);
if (!targetRoom) throw new Error(`Error in room creation.`);
if (cmd === 'makeprivatechatroom') {
if (!targetRoom.persist) throw new Error(`Private chat room created without settings.`);
targetRoom.setPrivate(true);
const upperStaffRoom = Rooms.get('upperstaff');
if (upperStaffRoom) {
upperStaffRoom.add(`|raw|<div class="broadcast-green">Private chat room created: <b>${Utils.escapeHTML(target)}</b></div>`).update();
}
this.sendReply(`The private chat room '${target}' was created.`);
} else {
const staffRoom = Rooms.get('staff');
if (staffRoom) {
staffRoom.add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Utils.escapeHTML(target)}</b></div>`).update();
}
const upperStaffRoom = Rooms.get('upperstaff');
if (upperStaffRoom) {
upperStaffRoom.add(`|raw|<div class="broadcast-green">Public chat room created: <b>${Utils.escapeHTML(target)}</b></div>`).update();
}
this.sendReply(`The chat room '${target}' was created.`);
}
},
makechatroomhelp: [
`/makeprivatechatroom [roomname] - Creates a new private room named [roomname]. Requires: ~`,
`/makepublicchatroom [roomname] - Creates a new public room named [roomname]. Requires: ~`,
],
subroomgroupchat: 'makegroupchat',
srgc: 'makegroupchat',
mgc: 'makegroupchat',
makegroupchat(target, room, user, connection, cmd) {
room = this.requireRoom();
this.checkChat();
if (!user.trusted) {
return this.errorReply("You must be trusted (public room driver or global voice) to make a groupchat.");
}
const groupchatbanned = Punishments.isGroupchatBanned(user);
if (groupchatbanned) {
const expireText = Punishments.checkPunishmentExpiration(groupchatbanned);
return this.errorReply(`You are banned from using groupchats ${expireText}.`);
}
if (cmd === 'subroomgroupchat' || cmd === 'srgc') {
if (!user.can('mute', null, room)) {
return this.errorReply("You can only create subroom groupchats for rooms you're staff in.");
}
if (room.battle) return this.errorReply("You cannot create a subroom of a battle.");
if (room.settings.isPersonal) return this.errorReply("You cannot create a subroom of a groupchat.");
}
const parent = cmd === 'subroomgroupchat' || cmd === 'srgc' ? room.roomid : null;
// this.checkCan('makegroupchat');
// Title defaults to a random 8-digit number.
let title = target.trim();
if (title.length >= 32) {
return this.errorReply("Title must be under 32 characters long.");
} else if (!title) {
title = (`${Math.floor(Math.random() * 100000000)}`);
} else if (this.filter(title) !== title) {
return this.errorReply("Invalid title.");
}
// `,` is a delimiter used by a lot of /commands
// `|` and `[` are delimiters used by the protocol
// `-` has special meaning in roomids
if (title.includes(',') || title.includes('|') || title.includes('[') || title.includes('-')) {
return this.errorReply("Room titles can't contain any of: ,|[-");
}
// Even though they're different namespaces, to cut down on confusion, you
// can't share names with registered chatrooms.
const existingRoom = Rooms.search(toID(title));
if (existingRoom && !existingRoom.settings.modjoin) {
return this.errorReply(`Your group chat name is too similar to existing chat room '${title}'.`);
}
// Room IDs for groupchats are groupchat-TITLEID
let titleid = toID(title);
if (!titleid) {
titleid = `${Math.floor(Math.random() * 100000000)}` as ID;
}
const roomid = `groupchat-${parent || user.id}-${titleid}` as RoomID;
// Titles must be unique.
if (Rooms.search(roomid)) return this.errorReply(`A group chat named '${title}' already exists.`);
// Tab title is prefixed with '[G]' to distinguish groupchats from
// registered chatrooms
if (Monitor.countGroupChat(connection.ip)) {
this.errorReply("Due to high load, you are limited to creating 4 group chats every hour.");
return;
}
const titleMsg = Utils.html`Welcome to ${parent ? room.title : user.name}'s` +
Utils.html`${!/^[0-9]+$/.test(title) ? ` ${title}` : ''}${parent ? ' subroom' : ''} groupchat!`;
const targetRoom = Rooms.createChatRoom(roomid, `[G] ${title}`, {
isPersonal: true,
isPrivate: 'hidden',
creationTime: Date.now(),
modjoin: parent ? null : '+',
parentid: parent,
auth: {},
introMessage: `` +
`<div style="text-align: center"><table style="margin:auto;"><tr><td><img src="//${Config.routes.client}/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>`,
staffMessage: `` +
`<p>Groupchats are temporary rooms, and will expire if there hasn't been any activity in 40 minutes.</p><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="/roomsettings">Room Settings</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>`,
});
if (!targetRoom) {
return this.errorReply(`An unknown error occurred while trying to create the room '${title}'.`);
}
// The creator is a Room Owner in subroom groupchats and a Host otherwise..
targetRoom.auth.set(user.id, parent ? '#' : Users.HOST_SYMBOL);
// Join after creating room. No other response is given.
user.joinRoom(targetRoom.roomid);
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.`);
if (parent) this.modlog('SUBROOMGROUPCHAT', null, title);
},
makegroupchathelp: [
`/makegroupchat [roomname] - Creates an invite-only group chat named [roomname].`,
`/subroomgroupchat [roomname] - Creates a subroom groupchat of the current room. Can only be used in a public room you have staff in.`,
`Only users who are staff in a public room or global auth can make groupchats.`,
],
groupchatuptime: 'roomuptime',
roomuptime(target, room, user, connection, cmd) {
if (!this.runBroadcast()) return;
if (!room) return this.errorReply(`Can only be used in a room.`);
// for hotpatching
if (!room.settings.creationTime) room.settings.creationTime = Date.now();
const uptime = Chat.toDurationString(Date.now() - room.settings.creationTime);
this.sendReplyBox(`Room uptime: <b>${uptime}</b>`);
},
roomuptimehelp: [`/roomuptime - Displays the uptime of the room.`],
deregisterchatroom(target, room, user) {
this.checkCan('makeroom');
this.errorReply("NOTE: You probably want to use `/deleteroom` now that it exists.");
const id = toID(target);
if (!id) return this.parse('/help deregisterchatroom');
const targetRoom = Rooms.search(id);
if (!targetRoom) return this.errorReply(`The room '${target}' doesn't exist.`);
target = targetRoom.title || targetRoom.roomid;
const isPrivate = targetRoom.settings.isPrivate;
const staffRoom = Rooms.get('staff');
const upperStaffRoom = Rooms.get('upperstaff');
if (Rooms.global.deregisterChatRoom(id)) {
this.sendReply(`The room '${target}' was deregistered.`);
this.sendReply("It will be deleted as of the next server restart.");
target = Utils.escapeHTML(target);
if (isPrivate) {
if (upperStaffRoom) {
upperStaffRoom.add(`|raw|<div class="broadcast-red">Private chat room deregistered by ${user.id}: <b>${target}</b></div>`).update();
}
} else {
if (staffRoom) {
staffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered: <b>${target}</b></div>`).update();
}
if (upperStaffRoom) {
upperStaffRoom.add(`|raw|<div class="broadcast-red">Public chat room deregistered by ${user.id}: <b>${target}</b></div>`).update();
}
}
return;
}
return this.errorReply(`The room "${target}" isn't registered.`);
},
deregisterchatroomhelp: [
`/deregisterchatroom [roomname] - Deletes room [roomname] after the next server restart. Requires: ~`,
],
deletechatroom: 'deleteroom',
deletegroupchat: 'deleteroom',
dgc: 'deleteroom',
deleteroom(target, room, user, connection, cmd) {
room = this.requireRoom();
const roomid = target.trim();
if (!roomid) {
// allow deleting personal rooms without typing out the room name
if (!room.settings.isPersonal || !['deletegroupchat', 'dgc'].includes(cmd)) {
return this.parse(`/help deleteroom`);
}
} else {
const targetRoom = Rooms.search(roomid);
if (targetRoom !== room) {
return this.parse(`/help deleteroom`);
}
}
if (room.roomid.startsWith('groupchat-')) {
this.checkCan('gamemanagement', null, room);
} else {
this.checkCan('makeroom');
}
const title = room.title || room.roomid;
if (room.persist) {
if (room.settings.isPrivate) {
const upperStaffRoom = Rooms.get('upperstaff');
if (upperStaffRoom) {
upperStaffRoom.add(
Utils.html`|raw|<div class="broadcast-red">Private chat room ` +
`deleted by ${user.id}: <b>${title}</b></div>`
).update();
}
} else {
const staffRoom = Rooms.get('staff');
if (staffRoom) {
staffRoom.add(Utils.html`|raw|<div class="broadcast-red">Public chat room deleted: <b>${title}</b></div>`).update();
}
const upperStaffRoom = Rooms.get('upperstaff');
if (upperStaffRoom) {
upperStaffRoom.add(
Utils.html`|raw|<div class="broadcast-red">Public chat ` +
`room deleted by ${user.id}: <b>${title}</b></div>`
).update();
}
}
}
room.add(`|raw|<div class="broadcast-red"><b>This room has been deleted.</b></div>`);
room.update();
room.send(`|expire|This room has been deleted.`);
room.destroy();
},
deleteroomhelp: [
`/deleteroom [roomname] - Deletes room [roomname]. Must be typed in the room to delete. Requires: ~`,
`/deletegroupchat - Deletes the current room, if it's a groupchat. Requires: ★ # ~`,
],
rename() {
this.errorReply("Did you mean /renameroom?");
},
renamegroupchat: 'renameroom',
renameroom(target, room, user, connection, cmd) {
room = this.requireRoom();
if (room.game || room.minorActivity || room.tour) {
return this.errorReply("Cannot rename room while a tour/poll/game is running.");
}
if (room.battle) {
return this.errorReply("Cannot rename battle rooms.");
}
const oldTitle = room.title;
const isGroupchat = cmd === 'renamegroupchat';
if (!toID(target)) return this.parse("/help renameroom");
if (room.persist && isGroupchat) return this.errorReply(`This isn't a groupchat.`);
if (!room.persist && !isGroupchat) return this.errorReply(`Use /renamegroupchat instead.`);
if (isGroupchat) {
if (!user.can('lock')) this.checkCan('declare', null, room);
const existingRoom = Rooms.search(toID(target));
if (existingRoom && !existingRoom.settings.modjoin) {
return this.errorReply(`Your groupchat name is too similar to existing chat room '${existingRoom.title}'.`);
}
if (this.filter(target) !== target) {
return this.errorReply("Invalid title.");
}
// `,` is a delimiter used by a lot of /commands
// `|` and `[` are delimiters used by the protocol
// `-` has special meaning in roomids
if (target.includes(',') || target.includes('|') || target.includes('[') || target.includes('-')) {
return this.errorReply("Room titles can't contain any of: ,|[-");
}
} else {
this.checkCan('makeroom');
}
const creatorID = room.roomid.split('-')[1];
const id = isGroupchat ? `groupchat-${creatorID}-${toID(target)}` as RoomID : undefined;
const title = isGroupchat ? `[G] ${target}` : target;
const oldID = room.roomid;
room.rename(title, id);
Chat.handleRoomRename(oldID, id || toID(target) as RoomID, room);
this.modlog(`RENAME${isGroupchat ? 'GROUPCHAT' : 'ROOM'}`, null, `from ${oldTitle}`);
const privacy = room.settings.isPrivate === true ? "Private" :
!room.settings.isPrivate ? "Public" :
`${room.settings.isPrivate.charAt(0).toUpperCase()}${room.settings.isPrivate.slice(1)}`;
if (!isGroupchat) {
Rooms.global.notifyRooms(
room.settings.isPrivate === true ? ['upperstaff'] : ['upperstaff', 'staff'],
Utils.html`|raw|<div class="broadcast-green">${privacy} chat room <b>${oldTitle}</b> renamed to <b>${target}</b></div>`
);
}
room.add(Utils.html`|raw|<div class="broadcast-green">The room has been renamed to <b>${target}</b></div>`).update();
},
renameroomhelp: [`/renameroom [new title] - Renames the current room to [new title]. Case-sensitive. Requires ~`],
hideroom: 'privateroom',
hiddenroom: 'privateroom',
secretroom: 'privateroom',
publicroom: 'privateroom',
unlistroom: 'privateroom',
privateroom(target, room, user, connection, cmd) {
room = this.requireRoom();
const battle = room.battle || room.bestOf;
if (battle) {
this.checkCan('editprivacy', null, room);
if (battle.forcedSettings.privacy) {
return this.errorReply(`This battle is required to be public because a player has a name prefixed by '${battle.forcedSettings.privacy}'.`);
}
if (room.tour?.forcePublic) {
return this.errorReply(`This battle can't be hidden, because the tournament is set to be forced public.`);
}
} else if (room.settings.isPersonal) {
this.checkCan('editroom', null, room);
} else {
// registered chatrooms show up on the room list and so require
// higher permissions to modify privacy settings
this.checkCan('makeroom');
}
let setting: boolean | 'hidden' | 'unlisted';
switch (cmd) {
case 'privateroom':
return this.parse('/help privateroom');
case 'publicroom':
setting = false;
break;
case 'secretroom':
this.checkCan('rangeban');
setting = true;
break;
case 'unlistroom':
this.checkCan('rangeban');
setting = 'unlisted';
break;
default:
if (room.settings.isPrivate === true && target !== 'force') {
return this.sendReply(`This room is a secret room. Use "/publicroom" to make it public, or "/hiddenroom force" to force it hidden.`);
}
setting = 'hidden';
break;
}
if (this.meansNo(target)) {
return this.errorReply(`Please specify what privacy setting you want for this room: /hiddenroom, /secretroom, or /publicroom`);
}
if (!setting) {
if (!room.settings.isPrivate) {
return this.errorReply(`This room is already public.`);
}
if (room.parent?.settings.isPrivate) {
return this.errorReply(`This room's parent ${room.parent.title} must be public for this room to be public.`);
}
if (room.settings.isPersonal && !battle) {
return this.errorReply(`This room can't be made public.`);
}
if (room.privacySetter && user.can('nooverride', null, room) && !user.can('makeroom')) {
if (!room.privacySetter.has(user.id)) {
const privacySetters = [...room.privacySetter].join(', ');
return this.errorReply(`You can't make the room public since you didn't make it private - only ${privacySetters} can.`);
}
room.privacySetter.delete(user.id);
if (room.privacySetter.size) {
const privacySetters = [...room.privacySetter].join(', ');
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.`);
}
}
room.privacySetter = null;
this.addModAction(`${user.name} made this room public.`);
this.modlog('PUBLICROOM');
room.setPrivate(false);
} else {
const settingName = (setting === true ? 'secret' : setting);
if (room.subRooms && !room.bestOf) {
if (settingName === 'secret') return this.errorReply("Secret rooms cannot have subrooms.");
for (const subRoom of room.subRooms.values()) {
if (!subRoom.settings.isPrivate) {
return this.errorReply(`Subroom ${subRoom.title} must be private to make this room private.`);
}
}
}
if (room.settings.isPrivate === setting) {
if (room.privacySetter && !room.privacySetter.has(user.id)) {
room.privacySetter.add(user.id);
return this.sendReply(`This room is already ${settingName}, but is now forced to stay that way until you use /publicroom.`);
}
return this.errorReply(`This room is already ${settingName}.`);
}
this.addModAction(`${user.name} made this room ${settingName}.`);
this.modlog(`${settingName.toUpperCase()}ROOM`);
if (!room.settings.isPersonal && !battle) room.setSection();
room.setPrivate(setting);
room.privacySetter = new Set([user.id]);
}
},
privateroomhelp: [
`/secretroom - Makes a room secret. Secret rooms are visible to ~ and up. Requires: ~`,
`/hiddenroom [on/off] - Makes a room hidden. Hidden rooms are visible to % and up, and inherit global ranks. Requires: \u2606 ~`,
`/publicroom - Makes a room public. Requires: \u2606 ~`,
],
hidenext(target, room, user) {
const groupConfig = Config.groups[Users.PLAYER_SYMBOL];
if (!groupConfig?.editprivacy) return this.errorReply(`/hidenext - Access denied.`);
if (this.meansNo(target)) {
user.battleSettings.hidden = false;
user.update();
this.sendReply("Your next battle will be publicly visible.");
} else {
user.battleSettings.hidden = true;
user.update();
this.sendReply(`Your next battle will be hidden${Rooms.RoomBattle.battleForcedSetting(user, 'privacy') ? `, unless it is rated` : ``}.`);
}
},
hidenexthelp: [
`/hidenext - Sets your next battle to be hidden.`,
`/hidenext off - Sets your next battle to be publicly visible.`,
],
officialchatroom: 'officialroom',
officialroom() {
this.parse(`/setroomsection official`);
},
roomspotlight(target, room, user) {
this.checkCan('makeroom');
room = this.requireRoom();
if (!target) return this.parse(`/help roomspotlight`);
if (!room.persist) {
return this.errorReply(`/roomspotlight - You can't spotlight this room.`);
}
if (this.meansNo(target)) {
if (!room.settings.spotlight) return this.errorReply(`This chatroom is not being spotlighted.`);
this.addModAction(`${user.name} removed this chatroom from the spotlight.`);
this.globalModlog('UNSPOTLIGHT');
delete room.settings.spotlight;
room.saveSettings();
} else {
if (room.settings.spotlight === target) return this.errorReply("This chat room is already spotlighted.");
this.addModAction(`${user.name} spotlighted this room with the message "${target}".`);
this.globalModlog('SPOTLIGHT');
room.settings.spotlight = target;
room.saveSettings();
}
},
roomspotlighthelp: [
`/roomspotlight [spotlight] - Makes the room this command is used in a spotlight room for the [spotlight] category on the roomlist. Requires: ~`,
`/roomspotlight off - Removes the room this command is used in from the list of spotlight rooms. Requires: ~`,
],
setsubroom: 'subroom',
subroom(target, room, user) {
room = this.requireRoom();
if (!user.can('makeroom')) return this.errorReply(`/subroom - Access denied. Did you mean /subrooms?`);
if (!target) return this.parse('/help subroom');
if (!room.persist) return this.errorReply(`Temporary rooms cannot be subrooms.`);
if (room.parent) {
return this.errorReply(`This room is already a subroom. To change which room this subroom belongs to, remove the subroom first.`);
}
if (room.subRooms) {
return this.errorReply(`This room is already a parent room, and a parent room cannot be made as a subroom.`);
}
const parent = Rooms.search(target);
if (!parent) return this.errorReply(`The room '${target}' does not exist.`);
if (parent.type !== 'chat') return this.errorReply(`Parent room '${target}' must be a chat room.`);
if (parent.parent) return this.errorReply(`Subrooms cannot have subrooms.`);
if (parent.settings.isPrivate === true) return this.errorReply(`Only public and hidden rooms can have subrooms.`);
if (parent.settings.isPrivate && !room.settings.isPrivate) {
return this.errorReply(`Private rooms cannot have public subrooms.`);
}
if (!parent.persist) return this.errorReply(`Temporary rooms cannot be parent rooms.`);
if (room === parent) return this.errorReply(`You cannot set a room to be a subroom of itself.`);
const settingsList = Rooms.global.settingsList;
const parentIndex = settingsList.findIndex(r => r.title === parent.title);
const index = settingsList.findIndex(r => r.title === room.title);
// Ensure that the parent room gets loaded before the subroom.
if (parentIndex > index) {
[settingsList[parentIndex], settingsList[index]] = [settingsList[index], settingsList[parentIndex]];
}
room.setParent(parent);
this.modlog('SUBROOM', null, `of ${parent.title}`);
return this.addModAction(`This room was set as a subroom of ${parent.title} by ${user.name}.`);
},
removesubroom: 'unsubroom',
desubroom: 'unsubroom',
unsubroom(target, room, user) {
room = this.requireRoom();
this.checkCan('makeroom');
if (!room.parent || !room.persist) {
return this.errorReply(`This room is not currently a subroom of a public room.`);
}
room.setParent(null);
this.modlog('UNSUBROOM');
return this.addModAction(`This room was unset as a subroom by ${user.name}.`);
},
unsubroomhelp: [`/unsubroom - Unmarks the current room as a subroom. Requires: ~`],
parentroom: 'subrooms',
subrooms(target, room, user, connection, cmd) {
room = this.requireRoom();
if (cmd === 'parentroom') {
if (!room.parent) return this.errorReply(`This room is not a subroom.`);
return this.sendReply(`This is a subroom of ${room.parent.title}.`);
}
if (!room.persist) return this.errorReply(`Temporary rooms cannot have subrooms.`);
if (!this.runBroadcast()) return;
const showSecret = !this.broadcasting && user.can('mute', null, room);
const subRooms = room.getSubRooms(showSecret);
if (!subRooms.length) return this.sendReply(`This room doesn't have any subrooms.`);
const subRoomText = subRooms.map(
subRoom =>
Utils.html`<a href="/${subRoom.roomid}">${subRoom.title}</a><br/><small>${subRoom.settings.desc}</small>`
);
return this.sendReplyBox(`<p style="font-weight:bold;">${Utils.escapeHTML(room.title)}'s subroom${Chat.plural(subRooms)}:</p><ul><li>${subRoomText.join('</li><br/><li>')}</li></ul></strong>`);
},
subroomhelp: [
`/subroom [room] - Marks the current room as a subroom of [room]. Requires: ~`,
`/unsubroom - Unmarks the current room as a subroom. Requires: ~`,
`/subrooms - Displays the current room's subrooms.`,
`/parentroom - Displays the current room's parent room.`,
],
roomdesc(target, room, user) {
room = this.requireRoom();
if (!target) {
if (!this.runBroadcast()) return;
if (!room.settings.desc) return this.sendReply(`This room does not have a description set.`);
this.sendReplyBox(Utils.html`The room description is: ${room.settings.desc}`);
return;
}
this.checkCan('makeroom');
if (target.length > 80) {
return this.errorReply(`Error: Room description is too long (must be at most 80 characters).`);
}
const normalizedTarget = ' ' + target.toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + ' ';
if (normalizedTarget.includes(' welcome ')) {
return this.errorReply(`Error: Room description must not contain the word "welcome".`);
}
if (normalizedTarget.startsWith(' discuss ')) {
return this.errorReply(`Error: Room description must not start with the word "discuss".`);
}
if (normalizedTarget.startsWith(' talk about ') || normalizedTarget.startsWith(' talk here about ')) {
return this.errorReply(`Error: Room description must not start with the phrase "talk about".`);
}
room.settings.desc = target;
this.sendReply(`(The room description is now: ${target})`);
this.privateModAction(`${user.name} changed the roomdesc to: "${target}".`);
this.modlog('ROOMDESC', null, `to "${target}"`);
room.saveSettings();
},
roomdeschelp: [`/roomdesc [description] - Sets the [description] of the current room. Requires: ~`],
topic: 'roomintro',
roomintro(target, room, user, connection, cmd) {
room = this.requireRoom();
if (!target) {
if (!this.runBroadcast()) return;
if (!room.settings.introMessage) return this.sendReply("This room does not have an introduction set.");
this.sendReply('|raw|<div class="infobox infobox-limited">' + room.settings.introMessage.replace(/\n/g, '') + '</div>');
if (!this.broadcasting && user.can('declare', null, room, 'roomintro') && cmd !== 'topic') {
const code = Utils.escapeHTML(room.settings.introMessage).replace(/\n/g, '<br />');
this.sendReplyBox(`<details open><summary>Source:</summary><code style="white-space: pre-wrap; display: table; tab-size: 3">/roomintro ${code}</code></details>`);
}
return;
}
this.checkCan('editroom', null, room);
if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deleteroomintro"?');
this.checkHTML(target);
if (!target.includes("<")) {
// not HTML, do some simple URL linking
const re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
target = target.replace(re, '<a href="$1">$1</a>');
}
if (target.substr(0, 11) === '/roomintro ') target = target.substr(11);
room.settings.introMessage = target.replace(/\r/g, '');
this.sendReply("(The room introduction has been changed to:)");
this.sendReply(`|raw|<div class="infobox infobox-limited">${room.settings.introMessage.replace(/\n/g, '')}</div>`);
this.privateModAction(`${user.name} changed the roomintro.`);
this.modlog('ROOMINTRO');
this.roomlog(room.settings.introMessage.replace(/\n/g, ''));
room.saveSettings();
},
roomintrohelp: [
`/roomintro - Display the room introduction of the current room.`,
`/roomintro [content] - Set an introduction for the room. Requires: # ~`,
],
deletetopic: 'deleteroomintro',
deleteroomintro(target, room, user) {
room = this.requireRoom();
this.checkCan('declare', null, room);
if (!room.settings.introMessage) return this.errorReply("This room does not have a introduction set.");
this.privateModAction(`${user.name} deleted the roomintro.`);
this.modlog('DELETEROOMINTRO');
this.roomlog(target);
delete room.settings.introMessage;
room.saveSettings();
},
deleteroomintrohelp: [`/deleteroomintro - Deletes the current room's introduction. Requires: # ~`],
stafftopic: 'staffintro',
staffintro(target, room, user, connection, cmd) {
room = this.requireRoom();
if (!target) {
this.checkCan('mute', null, room);
if (!room.settings.staffMessage) return this.sendReply("This room does not have a staff introduction set.");
this.sendReply(`|raw|<div class="infobox">${room.settings.staffMessage.replace(/\n/g, ``)}</div>`);
if (user.can('ban', null, room, 'staffintro') && cmd !== 'stafftopic') {
const code = Utils.escapeHTML(room.settings.staffMessage).replace(/\n/g, '<br />');
this.sendReplyBox(`<details open><summary>Source:</summary><code style="white-space: pre-wrap; display: table; tab-size: 3">/staffintro ${code}</code></details>`);
}
return;
}
this.checkCan('ban', null, room);
this.checkChat();
if (this.meansNo(target) || target === 'delete') return this.errorReply('Did you mean "/deletestaffintro"?');
this.checkHTML(target);
if (!target.includes("<")) {
// not HTML, do some simple URL linking
const re = /(https?:\/\/(([\w.-]+)+(:\d+)?(\/([\w/_.]*(\?\S+)?)?)?))/g;
target = target.replace(re, '<a href="$1">$1</a>');
}
if (target.substr(0, 12) === '/staffintro ') target = target.substr(12);
room.settings.staffMessage = target.replace(/\r/g, '');
this.sendReply("(The staff introduction has been changed to:)");
this.sendReply(`|raw|<div class="infobox">${target.replace(/\n/g, ``)}</div>`);
this.privateModAction(`${user.name} changed the staffintro.`);
this.modlog('STAFFINTRO');
this.roomlog(room.settings.staffMessage.replace(/\n/g, ``));
room.saveSettings();
},
staffintrohelp: [`/staffintro [content] - Set an introduction for staff members. Requires: @ # ~`],
deletestafftopic: 'deletestaffintro',
deletestaffintro(target, room, user) {
room = this.requireRoom();
this.checkCan('ban', null, room);
if (!room.settings.staffMessage) return this.errorReply("This room does not have a staff introduction set.");
this.privateModAction(`${user.name} deleted the staffintro.`);
this.modlog('DELETESTAFFINTRO');
this.roomlog(target);
delete room.settings.staffMessage;
room.saveSettings();
},
deletestaffintrohelp: [`/deletestaffintro - Deletes the current room's staff introduction. Requires: @ # ~`],
roomalias(target, room, user) {
room = this.requireRoom();
if (!target) {
if (!this.runBroadcast()) return;
if (!room.settings.aliases) return this.sendReplyBox("This room does not have any aliases.");
return this.sendReplyBox(`This room has the following aliases: ${room.settings.aliases.join(", ")}`);
}
this.checkCan('makeroom');
if (target.includes(',')) {
this.errorReply(`Invalid room alias: ${target.trim()}`);
return this.parse('/help roomalias');
}
const alias = toID(target);
if (!alias.length) return this.errorReply("Only alphanumeric characters are valid in an alias.");
if (Rooms.get(alias) || Rooms.aliases.has(alias)) {
return this.errorReply("You cannot set an alias to an existing room or alias.");
}
if (room.settings.isPersonal) return this.errorReply("Personal rooms can't have aliases.");
Rooms.aliases.set(alias, room.roomid);
this.privateModAction(`${user.name} added the room alias '${alias}'.`);
this.modlog('ROOMALIAS', null, alias);
if (!room.settings.aliases) room.settings.aliases = [];
room.settings.aliases.push(alias);
room.saveSettings();
},
roomaliashelp: [
`/roomalias - displays a list of all room aliases of the room the command was entered in.`,
`/roomalias [alias] - adds the given room alias to the room the command was entered in. Requires: ~`,
`/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: ~`,
],
deleteroomalias: 'removeroomalias',
deroomalias: 'removeroomalias',
unroomalias: 'removeroomalias',
removeroomalias(target, room, user) {
room = this.requireRoom();
if (!room.settings.aliases) return this.errorReply("This room does not have any aliases.");
this.checkCan('makeroom');
if (target.includes(',')) {
this.errorReply(`Invalid room alias: ${target.trim()}`);
return this.parse('/help removeroomalias');
}
const alias = toID(target);
if (!alias || !Rooms.aliases.has(alias)) return this.errorReply("Please specify an existing alias.");
if (Rooms.aliases.get(alias) !== room.roomid) {
return this.errorReply("You may only remove an alias from the current room.");
}
this.privateModAction(`${user.name} removed the room alias '${alias}'.`);
this.modlog('REMOVEALIAS', null, alias);
const aliasIndex = room.settings.aliases.indexOf(alias);
if (aliasIndex >= 0) {
room.settings.aliases.splice(aliasIndex, 1);
if (!room.settings.aliases.length) room.settings.aliases = undefined;
Rooms.aliases.delete(alias);
room.saveSettings();
}
},
removeroomaliashelp: [
`/removeroomalias [alias] - removes the given room alias of the room the command was entered in. Requires: ~`,
],
resettierdisplay: 'roomtierdisplay',
roomtierdisplay(target, room, user, connection, cmd) {
room = this.requireRoom();
const resetTier = cmd === 'resettierdisplay';
if (!target) {
if (!this.runBroadcast()) return;
return this.sendReplyBox(
`This room is currently displaying ${room.settings.dataCommandTierDisplay} as the tier when using /data.`
);
}
this.checkCan('declare', null, room);
const displayIDToName: { [k: string]: Room['settings']['dataCommandTierDisplay'] } = {
tiers: 'tiers',
doublestiers: 'doubles tiers',
nationaldextiers: 'National Dex tiers',
numbers: 'numbers',
};
if (!resetTier) {
if (!(toID(target) in displayIDToName)) {
this.errorReply(`Invalid tier display: ${target.trim()}`);
return this.parse(`/help roomtierdisplay`);
}
room.settings.dataCommandTierDisplay = displayIDToName[toID(target)];
this.sendReply(`(The room's tier display is now: ${displayIDToName[toID(target)]})`);
this.privateModAction(`${user.name} changed the room's tier display to: ${displayIDToName[toID(target)]}.`);
this.modlog('ROOMTIERDISPLAY', null, `to ${displayIDToName[toID(target)]}`);
} else {
room.settings.dataCommandTierDisplay = 'tiers';
this.sendReply(`(The room's tier display is now: tiers)`);
this.privateModAction(`${user.name} reset the room's tier display.`);
this.modlog('RESETTIERDISPLAY', null, `to tiers`);
}
room.saveSettings();
},
roomtierdisplayhelp: [
`/roomtierdisplay - displays the current room's display.`,
`/roomtierdisplay [option] - changes the current room's tier display. Valid options are: tiers, doubles tiers, numbers. Requires: # ~`,
`/resettierdisplay - resets the current room's tier display. Requires: # ~`,
],
setroomsection: 'roomsection',
roomsection(target, room, user) {
room = this.requireRoom();
const sectionNames = RoomSections.sectionNames;
if (!target) {
if (!this.runBroadcast()) return;
this.sendReplyBox(Utils.html`This room is ${room.settings.section ? `in the ${sectionNames[room.settings.section]} section` : `not in a section`}.`);
return;
}
this.checkCan('gdeclare');
const section = room.setSection(target);
this.sendReply(`The room section is now: ${section ? sectionNames[section] : 'none'}`);
this.privateGlobalModAction(`${user.name} changed the room section of ${room.title} to ${section ? sectionNames[section] : 'none'}.`);
this.globalModlog('ROOMSECTION', null, section || 'none');
},
roomsectionhelp: [
`/roomsection [section] - Sets the room this is used in to the specified [section]. Requires: ~`,
`Valid sections: ${sections.join(', ')}`,
],
roomdefaultformat(target, room, user) {
room = this.requireRoom();
this.checkCan('editroom', null, room);
if (!target) {
this.checkBroadcast();
if (room.settings.defaultFormat) {
this.sendReply(`This room's default format is ${room.settings.defaultFormat}.`);
} else {
this.sendReply(`This room has no default format.`);
}
return;
}
if (this.meansNo(target)) {
delete room.settings.defaultFormat;
room.saveSettings();
this.modlog(`DEFAULTFORMAT`, null, 'off');
this.privateModAction(`${user.name} removed this room's default format.`);
return;
}
target = toID(target);
const format = Dex.formats.get(target);
if (format.effectType === 'Format') {
target = format.name;
}
const { isMatch } = this.extractFormat(target);
if (!isMatch) throw new Chat.ErrorMessage(`Unrecognized format or mod "${target}"`);
room.settings.defaultFormat = target;
room.saveSettings();
this.modlog(`DEFAULTFORMAT`, null, target);
this.privateModAction(`${user.name} set this room's default format to ${target}.`);
},
roomdefaultformathelp: [
`/roomdefaultformat [format] or [mod] or gen[number] - Sets this room's default format/mod. Requires: # ~`,
`/roomdefaultformat off - Clears this room's default format/mod. Requires: # ~`,
`Affected commands: /details, /coverage, /effectiveness, /weakness, /learn`,
],
};
export const roomSettings: Chat.SettingsHandler[] = [
// modchat
(room, user) => ({
label: "Modchat",
permission: 'modchat',
options: [
'off',
'autoconfirmed',
'trusted',
...RANKS.slice(1).filter(symbol => Users.Auth.hasPermission(user, 'modchat', symbol, room)),
].map(rank => [rank, rank === (room.settings.modchat || 'off') || `modchat ${rank || 'off'}`]),
}),
(room, user) => ({
label: "Modjoin",
permission: room.settings.isPersonal ? user.can('editroom', null, room) : user.can('makeroom'),
options: [
'off',
'autoconfirmed',
// groupchat ROs can set modjoin, but only to +
// first rank is for modjoin off
...RANKS.slice(1, room.settings.isPersonal && !user.can('makeroom') ? 2 : undefined),
].map(rank => [rank, rank === (room.settings.modjoin || 'off') || `modjoin ${rank || 'off'}`]),
}),
room => ({
label: "Language",
permission: 'editroom',
options: [...Chat.languages].map(
([id, name]) => [name, id === (room.settings.language || 'english') || `roomlanguage ${id || 'off'}`]
),
}),
room => ({
label: "Stretch filter",
permission: 'editroom',
options: [
[`off`, !room.settings.filterStretching || 'stretchfilter off'],
[`on`, room.settings.filterStretching || 'stretchfilter on'],
],
}),
room => ({
label: "Caps filter",
permission: 'editroom',
options: [
[`off`, !room.settings.filterCaps || 'capsfilter off'],
[`on`, room.settings.filterCaps || 'capsfilter on'],
],
}),
room => ({
label: "Emoji filter",
permission: 'editroom',
options: [
[`off`, !room.settings.filterEmojis || 'emojifilter off'],
[`on`, room.settings.filterEmojis || 'emojifilter on'],
],
}),
room => ({
label: "Link filter",
permission: 'editroom',
options: [
[`off`, !room.settings.filterLinks || 'linkfilter off'],
[`on`, room.settings.filterLinks || 'linkfilter on'],
],
}),
room => ({
label: "Slowchat",
permission: room.userCount < SLOWCHAT_USER_REQUIREMENT ? 'bypassall' as any : 'editroom',
options: ['off', 5, 10, 20, 30, 60].map(
time => [`${time}`, time === (room.settings.slowchat || 'off') || `slowchat ${time || 'off'}`]
),
}),
room => ({
label: "/data Tier display",
permission: 'editroom',
options: [
[`tiers`, (room.settings.dataCommandTierDisplay ?? 'tiers') === 'tiers' || `roomtierdisplay tiers`],
[`doubles tiers`, room.settings.dataCommandTierDisplay === 'doubles tiers' || `roomtierdisplay doubles tiers`],
[
`National Dex tiers`,
room.settings.dataCommandTierDisplay === 'National Dex tiers' || `roomtierdisplay National Dex tiers`,
],
[`numbers`, room.settings.dataCommandTierDisplay === 'numbers' || `roomtierdisplay numbers`],
],
}),
room => ({
label: "/requestshow",
permission: 'declare',
options: [
[`off`, !room.settings.requestShowEnabled || `showapprovals off`],
[`on`, room.settings.requestShowEnabled || `showapprovals on`],
],
}),
];
export const pages: Chat.PageTable = {
permissions(args, user, connection) {
this.title = `[Permissions]`;
const room = this.requireRoom();
this.checkCan('mute', null, room);
const roomGroups = ['default', 'all users', ...Config.groupsranking.slice(1)];
const permissions = room.settings.permissions || {};
let buf = `<div class="pad"><h2>Command permissions for ${room.title}</h2>`;
buf += `<div class="ladder"><table>`;
buf += `<tr><th>Permission</th><th>Required rank</th></tr>`;
let atLeastOne = false;
for (const permission in permissions) {
const requiredRank = permissions[permission];
atLeastOne = true;
buf += `<tr><td><strong>${permission}</strong></td><td>`;
if (room.auth.atLeast(user, '#')) {
buf += roomGroups.map(group => (
requiredRank === group ?
Utils.html`<button class="button disabled" style="font-weight:bold;color:#575757;background:#d3d3d3">${group}</button>` :
Utils.html`<button class="button" name="send" value="/msgroom ${room.roomid},/permissions set ${permission}, ${group}">${group}</button>`
)).join(' ');
} else {
buf += Utils.html`<button class="button disabled" style="font-weight:bold;color:#575757;background:#d3d3d3">${requiredRank}</button>`;
}
buf += `</td>`;
}
if (!atLeastOne) {
buf += `<tr><td colspan="2">You don't have any permissions configured.</td></tr>`;
}
buf += `</table></div>`;
buf += `<p>Use <code>/permissions</code> to add new permissions</p>`;
return buf;
},
};
|