├── .env.example
├── .gitattributes
├── .gitignore
├── LICENSE
├── app
├── assets
│ ├── avatar.png
│ ├── categories
│ │ ├── advanced.svg
│ │ ├── data.svg
│ │ ├── leaderboard.svg
│ │ ├── levelup.svg
│ │ ├── multipliers.svg
│ │ ├── rankcard.svg
│ │ ├── rewardroles.svg
│ │ └── xp.svg
│ ├── fumo.png
│ ├── icons
│ │ ├── cog.svg
│ │ ├── download.svg
│ │ ├── pencil.svg
│ │ ├── plus.svg
│ │ └── podium.svg
│ ├── polaris.png
│ └── polaris.svg
├── css
│ └── polaris.css
├── html
│ ├── 404.html
│ ├── config.html
│ ├── home.html
│ ├── leaderboard.html
│ └── servers.html
└── js
│ └── extras.js
├── classes
├── DatabaseModel.js
├── LevelUpEmbed.js
├── LevelUpMessage.js
├── PageEmbed.js
└── Tools.js
├── commands
├── button
│ ├── export_xp.js
│ ├── list_multipliers.js
│ ├── list_reward_roles.js
│ ├── settings_edit.js
│ ├── settings_list.js
│ ├── settings_view.js
│ └── toggle_xp.js
├── events
│ └── message.js
├── misc
│ ├── json_import.js
│ └── polaris_transfer.js
├── slash
│ ├── addxp.js
│ ├── botstatus.js
│ ├── calculate.js
│ ├── clear.js
│ ├── config.js
│ ├── dev_db.js
│ ├── dev_deploy.js
│ ├── dev_run.js
│ ├── dev_setactivity.js
│ ├── dev_setversion.js
│ ├── multiplier.js
│ ├── rank.js
│ ├── rewardrole.js
│ ├── sync.js
│ └── top.js
└── user_context
│ ├── view_on_leaderboard.js
│ └── view_xp.js
├── config.json
├── database_schema.js
├── index.js
├── json
├── curve_presets.json
├── default_status.json
├── multiplier_modes.json
└── quick_settings.json
├── package.json
├── polaris.code-workspace
├── polaris.js
├── readme.md
└── web_app.js
/.env.example:
--------------------------------------------------------------------------------
1 | # Discord application
2 | # All these keys can be found in the developer portal
3 | DISCORD_ID=123456789
4 | DISCORD_TOKEN=ABCDEFGHIJKLM
5 | DISCORD_SECRET=ZYXWVUTSRQPON
6 |
7 | # MongoDB database
8 | # For more info on setting this up, see the readme
9 | MONGO_DB_NAME=polaris
10 | MONGO_DB_URI=
11 |
12 | # OPTIONAL: If you left MONGO_DB_URI blank, fill these out in instead
13 | MONGO_DB_IP=7.7.7.7
14 | MONGO_DB_USERNAME=root
15 | MONGO_DB_PASSWORD=ilovegdcologne
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | json/auto
3 | .env
4 | .vscode
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | TLDR: Do whatever you want as long as it's not for profit or commercial use.
2 |
3 | - You are free to use, fork, modify, distribute, and steal code snippets from this project
4 | - You may NOT use this project for commercial purposes. This includes selling the bot, or incorporating monetized features.
5 | - Please credit me (Colon) when you use this project, especially if your bot/fork is public
6 |
7 | Polaris is provided "as-is", without any kind of warranty or guarantees regarding its functionality. If you use this code, you're doing so at your own risk and are responsible for any functionality, data, legal, or other issues that arise. It's your problem, not mine.
--------------------------------------------------------------------------------
/app/assets/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GDColon/Polaris-Open/b5a93f0e6d6ed96e26db492c859f221dc22857e0/app/assets/avatar.png
--------------------------------------------------------------------------------
/app/assets/categories/advanced.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/categories/data.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/categories/leaderboard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/categories/levelup.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/categories/multipliers.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/categories/rankcard.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/categories/rewardroles.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/categories/xp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/fumo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GDColon/Polaris-Open/b5a93f0e6d6ed96e26db492c859f221dc22857e0/app/assets/fumo.png
--------------------------------------------------------------------------------
/app/assets/icons/cog.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icons/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icons/pencil.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/assets/icons/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
40 |
--------------------------------------------------------------------------------
/app/assets/icons/podium.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
73 |
--------------------------------------------------------------------------------
/app/assets/polaris.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GDColon/Polaris-Open/b5a93f0e6d6ed96e26db492c859f221dc22857e0/app/assets/polaris.png
--------------------------------------------------------------------------------
/app/assets/polaris.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
59 |
--------------------------------------------------------------------------------
/app/css/polaris.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Lato&display=swap');
2 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@500;700;800&display=swap');
3 |
4 | html {
5 | --bg: #202020;
6 | --fg: #303030;
7 | --lighterfg: #404040;
8 | --lightererfg: #505050;
9 | --evenlighterfg: #606060;
10 | --waylighterfg: #707070;
11 | --lightestfg: #808080;
12 |
13 | --lato: Lato, Arial, Helvetica, sans-serif;
14 | --opensans: 'Open Sans', --lato;
15 |
16 | --emojired: #DD2E44;
17 | --emojiyellow: #F4900C;
18 | --emojigreen: #77B255;
19 | --emojiblue: #3B88C3;
20 | --emojipurple: #9266CC;
21 |
22 | --defaultrolecol: #dddddd;
23 |
24 | --colon: #ff8000;
25 | --polarisgreen: #00ff80;
26 |
27 | --sliderwidth: 60px;
28 | --sliderheight: 32px;
29 | --sliderpadding: 4px;
30 |
31 | height: 100%;
32 | }
33 |
34 | body {
35 | background-color: var(--bg);
36 | margin: 0px 0px;
37 | }
38 |
39 | p, h1, h2, button {
40 | font-family: var(--lato);
41 | color: white;
42 | }
43 |
44 | p {
45 | font-size: 18px;
46 | margin: 12px 0px;
47 | line-height: 28px;
48 | }
49 |
50 | h1, h2 {
51 | font-family: var(--opensans);
52 | font-weight: 800;
53 | font-size: 36px;
54 | margin-top: 5px;
55 | margin-bottom: 5px;
56 | }
57 |
58 | h2 {
59 | margin-top: 26px;
60 | margin-bottom: 3px;
61 | font-size: 22px;
62 | }
63 |
64 | a {
65 | color: aqua !important;
66 | }
67 |
68 | input, select, textarea {
69 | font-family: var(--lato);
70 | background-color: var(--evenlighterfg);
71 | border: 1px solid black;
72 | border-radius: 4px;
73 | height: 40px;
74 | font-size: 18px;
75 | padding-left: 10px;
76 | color: white;
77 | width: 350px;
78 | }
79 |
80 | input[type="number"] {
81 | width: 125px;
82 | }
83 |
84 | input::placeholder, textarea::placeholder {
85 | color: white;
86 | opacity: 50%;
87 | }
88 |
89 | input[disabled] {
90 | cursor: not-allowed;
91 | background-color: var(--waylighterfg);
92 | }
93 |
94 | .lineinput {
95 | border: none;
96 | background: none;
97 | border-bottom: 1px solid white;
98 | border-bottom-left-radius: 0px;
99 | border-bottom-right-radius: 0px;
100 | padding: 0px 0px;
101 | text-align: center;
102 | background-color: rgba(0, 0, 0, 0.2);
103 | }
104 |
105 | select option:disabled {
106 | color: #999999;
107 | }
108 |
109 | select option {
110 | background-color: var(--bg);
111 | }
112 |
113 | button {
114 | font-family: var(--lato);
115 | min-width: 100px;
116 | height: 40px;
117 | padding: 0px 15px;
118 | font-size: 18px;
119 | font-weight: bold;
120 | border: none;
121 | border-radius: 4px;
122 | cursor: pointer;
123 | background-color: var(--emojiblue);
124 | }
125 |
126 | button:hover { filter: brightness(115%) }
127 | button:active { filter: brightness(125%) }
128 |
129 | button:disabled {
130 | cursor: not-allowed;
131 | opacity: 50%;
132 | filter: saturate(50%);
133 | }
134 |
135 | .fancybutton {
136 | font-family: var(--opensans);
137 | background-color: rgba(0, 0, 0, 0);
138 | border: 2px solid var(--polarisgreen);
139 | transition-duration: 0.25s;
140 | height: 50px;
141 | min-width: 140px;
142 | padding: 5px 20px;
143 | }
144 |
145 | .fancybutton:hover, .fancybutton:focus-visible {
146 | background-color: var(--polarisgreen);
147 | color: black;
148 | }
149 |
150 | .boringbutton {
151 | font-family: var(--opensans);
152 | background-color: rgba(0, 0, 0, 0);
153 | border: none;
154 | font-weight: 400;
155 | opacity: 66%;
156 | }
157 |
158 | .boringbutton:hover, .boringbutton:focus-visible {
159 | opacity: 100%;
160 | }
161 |
162 | textarea {
163 | width: 600px;
164 | height: 200px;
165 | padding-top: 8px;
166 | max-width: 100%;
167 | max-height: 700px;
168 | }
169 |
170 | .codeArea {
171 | font-family: monospace;
172 | font-size: 16px;
173 | }
174 |
175 | .colInputPreview {
176 | height: 40px;
177 | width: 40px;
178 | border: 1px solid black;
179 | border-radius: 4px;
180 | background-color: var(--polarisgreen);
181 | cursor: pointer;
182 | }
183 |
184 | .hiddenColInput {
185 | width: 0px;
186 | margin: 0px 0px;
187 | padding: 0px 0px;
188 | opacity: 0;
189 | position: absolute;
190 | pointer-events: none;
191 | }
192 |
193 | .canfocus:focus-visible, input:focus, button:focus-visible, textarea:focus, select:focus, input[type=range]:focus-visible,
194 | [contenteditable]:focus, #sidebar .category:focus-visible, .flexTable div p:focus-visible, .colInputPreview:focus-visible,
195 | .leaderboardSlot.canManage:focus-visible {
196 | outline: 2px solid var(--colon);
197 | }
198 |
199 | .slider {
200 | position: relative;
201 | display: inline-block;
202 | width: var(--sliderwidth);
203 | height: var(--sliderheight);
204 | }
205 |
206 | .slider:has(> input:disabled) {
207 | opacity: 33%;
208 | }
209 |
210 | .slider input {
211 | opacity: 0;
212 | width: 0;
213 | height: 0;
214 | }
215 |
216 | .slider:focus-within {
217 | outline: 2.5px solid white;
218 | border-radius: 420px;
219 | }
220 |
221 | .popup {
222 | position: fixed;
223 | display: none;
224 | width: 100%;
225 | height: 100%;
226 | top: 0; left: 0; right: 0; bottom: 0;
227 | background-color: rgba(0, 0, 0, 0.66);
228 | z-index: 2;
229 | text-align: center;
230 | }
231 |
232 | .popupbox {
233 | width: 100%;
234 | height: 100%;
235 | display: flex;
236 | align-items: center;
237 | justify-content: center;
238 | }
239 |
240 | .popupbox .box {
241 | width: 650px;
242 | }
243 |
244 | .popupConfirm {
245 | font-size: 22px;
246 | height: 50px;
247 | width: 150px;
248 | margin: 20px 7px 0px 7px;
249 | }
250 |
251 | .sliderspan {
252 | position: absolute;
253 | cursor: pointer;
254 | margin: 0px 0px !important;
255 | top: 0; left: 0; right: 0; bottom: 0;
256 | background-color: #aaaaaa;
257 | transition-duration: 0.25s;
258 | transition-timing-function: ease-in-out;
259 | border-radius: 420px;
260 | }
261 |
262 | .sliderspan:before {
263 | position: absolute;
264 | content: "";
265 | height: calc(var(--sliderheight) - var(--sliderpadding) );
266 | width: calc(var(--sliderheight) - var(--sliderpadding) );
267 | left: var(--sliderpadding);
268 | bottom: calc(var(--sliderpadding) * 0.5);
269 | background-color: white;
270 | transition-duration: 0.25s;
271 | transition-timing-function: ease-in-out;
272 | border-radius: 420px;
273 | }
274 |
275 | input:checked + .sliderspan {
276 | background-color: var(--colon);
277 | }
278 |
279 | input:checked + .sliderspan:before {
280 | transform: translateX(calc(var(--sliderwidth) - var(--sliderheight) - (var(--sliderpadding))));
281 | }
282 |
283 | .box {
284 | background-color: var(--lighterfg);
285 | border: 1px solid black;
286 | border-radius: 5px;
287 | padding: 10px 20px 30px 20px;
288 | margin: 20px 10px;
289 | }
290 |
291 | .details {
292 | font-size: 16px;
293 | opacity: 70%;
294 | margin-bottom: 5px;
295 | margin-top: 0px;
296 | }
297 |
298 | .settingBox {
299 | width: 600px;
300 | height: fit-content;
301 | }
302 |
303 | .settingBox h2:first-child {
304 | margin-top: 12px;
305 | }
306 |
307 | .categoryBox {
308 | cursor: pointer;
309 | width: 379px;
310 | height: 150px;
311 | padding: 10px 20px;
312 | transition-duration: 0.1s;
313 | }
314 |
315 | .categoryBox:hover, .categoryBox:focus-visible {
316 | background-color: var(--lightererfg);
317 | }
318 |
319 | .categoryBox h2 {
320 | margin-top: 6px !important;
321 | }
322 |
323 | .categoryBox img {
324 | height: 50px;
325 | margin-right: 18px;
326 | }
327 |
328 | .fulllength {
329 | width: 1260px;
330 | }
331 |
332 | .settingBreak {
333 | width: 100%;
334 | }
335 |
336 | .centerflex {
337 | display: flex;
338 | flex-direction: row;
339 | align-items: center;
340 | justify-content: flex-start;
341 | }
342 |
343 | .simpleflex {
344 | display: flex;
345 | flex-direction: row;
346 | align-items: flex-start;
347 | justify-content: flex-start;
348 | }
349 |
350 | .middleflex {
351 | display: flex;
352 | justify-content: center;
353 | align-items: center
354 | }
355 |
356 | .spacedflex * {
357 | margin-right: 10px;
358 | }
359 |
360 | .field {
361 | display: flex;
362 | flex-direction: column;
363 | align-items: flex-start;
364 | justify-content: flex-start;
365 | }
366 |
367 | .bottomBar {
368 | position: absolute;
369 | bottom: 3px;
370 | left: 15px;
371 | }
372 |
373 | .multiplierDescription {
374 | opacity: 100%;
375 | margin-top: 10px;
376 | height: 42px;
377 | width: 420px;
378 | }
379 |
380 | #header {
381 | display: flex;
382 | flex-direction: row;
383 | align-items: center;
384 | width: 100%;
385 | background-color: var(--fg);
386 | height: 50px;
387 | border-bottom: 1px solid black;
388 | z-index: 1;
389 | overflow: hidden;
390 | }
391 |
392 | #header div {
393 | display: flex;
394 | align-items: center;
395 | height: 100%;
396 | margin: 0px 25px;
397 | cursor: pointer;
398 | }
399 |
400 | #header div:focus-visible {
401 | outline: none;
402 | }
403 |
404 | #header h2 {
405 | margin: 0px 0px;
406 | font-size: 18px;
407 | font-weight: 700;
408 | transition-duration: 0.1s;
409 | }
410 |
411 | #header div:hover h2, #header div:focus-visible h2 {
412 | color: var(--polarisgreen);
413 | }
414 |
415 | #header div:focus-visible h2 {
416 | text-decoration: underline;
417 | }
418 |
419 | #header img {
420 | height: 70%;
421 | width: 36px;
422 | margin-right: 12px;
423 | }
424 |
425 | #header a {
426 | display: flex;
427 | align-items: center;
428 | text-decoration: none !important;
429 | width: 100%;
430 | height: 100%;
431 | }
432 |
433 | #sidebar {
434 | position: fixed;
435 | display: flex;
436 | flex-direction: column;
437 | width: 225px;
438 | height: 100%;
439 | background-color: var(--fg);
440 | margin-right: 32px;
441 | border-right: 1px solid black;
442 | overflow: hidden;
443 | white-space: nowrap;
444 | z-index: 1;
445 | }
446 |
447 | #sidebar div {
448 | height: 70px;
449 | display: flex;
450 | flex-direction: row;
451 | align-items: center;
452 | justify-content: flex-start;
453 | padding-left: 15px;
454 | cursor: pointer;
455 | }
456 |
457 | #sidebar div:focus-visible {
458 | background: rgba(255, 255, 255, 0.05);
459 | }
460 |
461 | #sidebar div:hover {
462 | background-color: var(--evenlighterfg) !important;
463 | }
464 |
465 | #sidebar div.current {
466 | color: var(--colon);
467 | font-weight: bold;
468 | background-color: var(--lighterfg);
469 | }
470 |
471 | #sidebar div img {
472 | width: 32px;
473 | height: 32px;
474 | margin-right: 16px;
475 | }
476 |
477 | #unsavedWarning {
478 | position: fixed;
479 | display: flex;
480 | pointer-events: none;
481 | justify-content: center;
482 | z-index: 2;
483 | width: 100%;
484 | height: 50px;
485 | bottom: 10px;
486 | transition-duration: 0.25s;
487 | transition-timing-function: ease-in-out;
488 | transition-property: transform;
489 | transform: translateY(60px);
490 | }
491 |
492 | #unsavedWarning.activeWarning {
493 | transform: translateY(0px);
494 | pointer-events: all;
495 | }
496 |
497 | .unsavedBox {
498 | display: flex;
499 | justify-content: space-between;
500 | align-items: center;
501 | width: 300px;
502 | border: 1px solid black;
503 | border-radius: 8px;
504 | background-color: var(--fg);
505 | padding: 0px 20px;
506 | }
507 |
508 | .unsavedBox button {
509 | height: 34px;
510 | width: 69px;
511 | }
512 |
513 | .configboxes {
514 | display: flex;
515 | flex-wrap: wrap;
516 | justify-content: flex-start;
517 | align-items: flex-start;
518 | padding-bottom: 20px;
519 | margin-left: 240px;
520 | }
521 |
522 | .sideoption {
523 | width: 600px;
524 | }
525 |
526 | .optionRow {
527 | margin-bottom: 16px;
528 | }
529 |
530 | .curvefield p {
531 | margin-left: 4px;
532 | margin-right: 15px;
533 | }
534 |
535 | .desmos {
536 | width: 512px;
537 | height: 512px;
538 | }
539 |
540 | .flexTable {
541 | overflow-y: auto;
542 | width: min-content;
543 | max-height: 750px;
544 | }
545 |
546 | .flexTable div p {
547 | padding: 7px 50px 7px 10px;
548 | margin: 0px 0px;
549 | border: 1px solid black;
550 | background-color: var(--lighterfg);
551 | white-space: nowrap;
552 | overflow: hidden;
553 | }
554 |
555 | .flexTable div p:nth-child(2n) {
556 | background-color: var(--lightererfg);
557 | }
558 |
559 | .longname {
560 | text-overflow: ellipsis;
561 | overflow: hidden;
562 | }
563 |
564 | .deleteRow {
565 | text-align: center;
566 | padding-left: 0px !important;
567 | padding-right: 0px !important;
568 | cursor: pointer;
569 | }
570 |
571 | #rewards div p:hover:not(:first-child) { background-color: var(--waylighterfg); }
572 |
573 | .deleteRow:hover { background-color: var(--emojired) !important }
574 |
575 | .toggleRow:hover {
576 | cursor: pointer;
577 | background-color: var(--emojiblue) !important;
578 | }
579 |
580 | .varList select {
581 | width: 120px;
582 | margin-right: 10px;
583 | }
584 |
585 | .serverOption {
586 | height: 90px;
587 | display: flex;
588 | flex-direction: row !important;
589 | align-items: center;
590 | justify-content: space-between;
591 | width: 80%;
592 | max-width: 850px;
593 | min-width: 500px;
594 | background-color: var(--lighterfg);
595 | border: 1px solid black;
596 | border-radius: 8px;
597 | margin: 0px auto 3px auto;
598 | padding: 0px 10px;
599 | overflow: hidden;
600 | cursor: pointer;
601 | transition-duration: 0.1s;
602 | }
603 |
604 | .serverOption:hover, .serverOption:focus-visible {
605 | background-color: var(--lightererfg);
606 | }
607 |
608 | .serverOption img[sv=icon] {
609 | height: 65px;
610 | border-radius: 420px;
611 | margin-left: 10px;
612 | margin-right: 20px;
613 | }
614 |
615 | .serverOption p {
616 | margin: 4px 0px;
617 | white-space: nowrap;
618 | overflow: hidden;
619 | text-overflow: ellipsis;
620 | max-width: 600px;
621 | }
622 |
623 | .serverOption .serverIcons img {
624 | cursor: pointer;
625 | height: 48px;
626 | }
627 |
628 | .serverOption .serverIcons a {
629 | transition-duration: 0.1s;
630 | border-radius: 5px;
631 | padding: 5px 5px;
632 | margin: 5px 5px;
633 | display: none;
634 | }
635 |
636 | .serverOption .serverIcons a:hover, .serverOption .serverIcons a:focus-visible {
637 | transform: scale(1.1);
638 | }
639 |
640 | .serverBreak {
641 | height: 30px;
642 | }
643 |
644 | .leaderboardBox {
645 | width: 90%;
646 | min-width: 500px;
647 | max-width: 1500px;
648 | background-color: var(--lighterfg);
649 | border: 1px solid black;
650 | border-radius: 8px;
651 | margin: auto;
652 | display: flex;
653 | flex-direction: column;
654 | padding: 0px 10px;
655 | }
656 |
657 | .leaderboardSlot {
658 | height: 100px;
659 | width: 100%;
660 | border-radius: 2px;
661 | display: flex;
662 | justify-content: space-between;
663 | align-items: center;
664 | }
665 |
666 | .leaderboardSlot .mainInfo {
667 | display: flex;
668 | align-items: center;
669 | overflow: hidden;
670 | }
671 |
672 | .leaderboardSlot:not(:last-child) {
673 | border-bottom: 1px solid rgba(255, 255, 255, 0.25);
674 | }
675 |
676 | .leaderboardSlot.canManage {
677 | cursor: pointer;
678 | transition-duration: 0.1s;
679 | transition-property: background-color;
680 | }
681 |
682 | .leaderboardSlot.isSelf {
683 | background-color: rgba(255, 255, 255, 0.2);
684 | }
685 |
686 | .leaderboardSlot.canManage:hover, .leaderboardSlot.canManage:focus-visible {
687 | background-color: rgba(255, 255, 255, 0.15);
688 | }
689 |
690 | .hideFromLeaderboard:hover {
691 | color: yellow !important;
692 | opacity: 100% !important;
693 | font-weight: bold !important;
694 | }
695 |
696 | .highlightedSlot {
697 | background-color: rgba(255, 255, 255, 0.25);
698 | border-bottom: none !important;
699 | }
700 |
701 | .plsLogIn {
702 | display: none;
703 | text-align: center;
704 | margin-bottom: 20px;
705 | color: aqua;
706 | }
707 |
708 | .leaderboardSlot p {
709 | font-size: 20px;
710 | margin: 4px 0px;
711 | white-space: nowrap;
712 | }
713 |
714 | .leaderboardSlot h2[lb=rank] {
715 | margin: 0px 0px;
716 | width: 200px;
717 | text-align: center;
718 | width: 60px;
719 | }
720 |
721 | .leaderboardSlot.notInServer .generalInfo p {
722 | text-decoration: line-through;
723 | font-weight: normal !important;
724 | opacity: 66%;
725 | }
726 |
727 | .progressBar {
728 | width: 100%;
729 | background-color: rgba(0, 0, 0, 0.33);
730 | border-radius: 16px;
731 | height: 60px;
732 | overflow: hidden;
733 | position: relative;
734 | }
735 |
736 | .progressBar .progress {
737 | height: 100%;
738 | }
739 |
740 | .progressBar .xpOverlay {
741 | position: absolute;
742 | display: flex;
743 | justify-content: center;
744 | flex-direction: column;
745 | margin: 0px 0px;
746 | padding-left: 12px;
747 | height: 100%;
748 | top: 0px;
749 | width: 100%;
750 | }
751 |
752 | .progressBar .xpOverlay p {
753 | line-height: 25px;
754 | margin: 0px 0px;
755 | font-size: 18px;
756 | font-weight: bold;
757 | text-shadow: 0.5px 0.5px 3px black, 0.5px 0.5px 3px black;
758 | white-space: nowrap;
759 | }
760 |
761 | .progressBar .xpOverlay .editIcon {
762 | position: absolute;
763 | right: 30px;
764 | height: 30px;
765 | filter: drop-shadow(0.5px 0.5px 3px black);
766 | display: none;
767 | }
768 |
769 | .leaderboardSlot p[lb=multiplier] {
770 | font-size: 15px;
771 | }
772 |
773 | .accountBox p {
774 | font-size: 22px;
775 | }
776 |
777 | .accountBox img[lb=pfp] {
778 | border-radius: 420px;
779 | height: 100px;
780 | margin-right: 25px;
781 | }
782 |
783 | .statBox {
784 | display: flex;
785 | flex-direction: column;
786 | align-items: center;
787 | margin-top: 20px;
788 | max-width: 400px;
789 | }
790 |
791 | .statBox p {
792 | font-weight: bold;
793 | margin: 0px 0px;
794 | }
795 |
796 | .statBox p[lb] {
797 | font-weight: normal;
798 | margin: 7px 0px 40px 0px;
799 | }
800 |
801 | .emptyLbBox {
802 | display: flex;
803 | flex-wrap: wrap;
804 | justify-content: center;
805 | width: 90%;
806 | min-width: 500px;
807 | max-width: 1500px;
808 | margin: auto;
809 | }
810 |
811 | .hiddenMemberSlot {
812 | display: flex;
813 | flex-direction: column;
814 | justify-content: center;
815 | background-color: var(--lighterfg);
816 | border: 1px solid black;
817 | text-align: center;
818 | border-radius: 5px;
819 | width: 275px;
820 | height: 140px;
821 | overflow: hidden;
822 | margin: 10px 10px;
823 | }
824 |
825 | .hiddenMemberSlot p[lb="unhide"] {
826 | width: fit-content;
827 | margin: auto;
828 | padding: 5px 20px;
829 | font-size: 20px;
830 | font-weight: bold;
831 | color: var(--polarisgreen);
832 | cursor: pointer;
833 | text-decoration: underline;
834 | }
835 |
836 | #lbButtons button {
837 | margin: 0px 10px;
838 | width: 170px;
839 | background-color: var(--emojiblue);
840 | }
841 |
842 | #lbButtons button:not(.selectedlb) {
843 | background-color: #20384b;
844 | }
845 |
846 | #uhoh #errorhelp {
847 | display: none;
848 | opacity: 50%;
849 | font-size: 16px;
850 | margin-top: 0px
851 | }
852 |
853 | #uhoh #loginbutton {
854 | display: none;
855 | background-color: var(--emojipurple);
856 | margin-top: 5p
857 | }
858 |
859 | .coolthing {
860 | width: 100%;
861 | display: flex;
862 | align-items: center;
863 | justify-content: center;
864 | min-height: 300px;
865 | margin-top: 30px;
866 | margin-bottom: 50px;
867 | }
868 |
869 | .coolthing div {
870 | width: 450px;
871 | padding: 0px 50px;
872 | margin: 0px 20px;
873 | }
874 |
875 | .coolthing .coolimage img {
876 | background-color: rgba(255, 255, 255, 0.25);
877 | border-radius: 8px;
878 | width: 450px;
879 | transition-duration: 0.2s;
880 | transition-timing-function: ease-in-out;
881 | }
882 |
883 | .coolthing .coolimage img:hover {
884 | transform: scale(1.2);
885 | }
886 |
887 | .coolthing .cooltext {
888 | min-width: 350px;
889 | }
890 |
891 | .coolthing .cooltext p {
892 | opacity: 75%;
893 | }
894 |
895 | .red { color: red }
896 |
897 | /* @media screen and (max-width: 800px) {
898 | #sidebar { width: 62px; min-width: 62px; }
899 | #sidebar p { display: none; }
900 | } */
--------------------------------------------------------------------------------
/app/html/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Nowhere
8 |
9 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
this page does not exist
85 |
86 |

87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
103 |
104 |
--------------------------------------------------------------------------------
/app/html/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Polaris
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

18 |
19 |
Polaris Open
20 |
A fully customizable, bullshit-free levelling bot.
21 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |

32 |
Originally created by Colon :
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
65 |
66 |
--------------------------------------------------------------------------------
/app/html/servers.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Polaris
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Loading...
16 |
17 |
18 |
19 |
Welcome back,
20 |
Select a server to manage
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
![]()
33 |
37 |
38 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
172 |
173 |
--------------------------------------------------------------------------------
/app/js/extras.js:
--------------------------------------------------------------------------------
1 | function Fetch(url, settings={}) {
2 | return new Promise(function (res, rej) {
3 | fetch(url).then(r => {
4 | if (r.ok) return settings.text ? r.text() : r.json()
5 | else return r.json()
6 | })
7 | .then(r => {
8 | r.apiError ? rej(r) : res(r)
9 | })
10 | .catch(rej)
11 | });
12 | }
13 |
14 |
15 | function timeStr(ms, decimals=0, noS, shortTime) {
16 | if (ms > 3e16) return "Forever"
17 | function timeFormat(amount, str) {
18 | amount = +amount
19 | return `${commafy(amount)} ${str}${noS || amount == 1 ? "" : "s"}`
20 | }
21 | ms = Math.abs(ms)
22 | let seconds = (ms / 1000).toFixed(0)
23 | let minutes = (ms / (1000 * 60)).toFixed(decimals)
24 | let hours = (ms / (1000 * 60 * 60)).toFixed(decimals)
25 | let days = (ms / (1000 * 60 * 60 * 24)).toFixed(decimals)
26 | let years = (ms / (1000 * 60 * 60 * 24 * 365)).toFixed(decimals)
27 | if (seconds < 1) return timeFormat((ms / 1000).toFixed(2), shortTime ? "sec" : "second")
28 | if (seconds < 60) return timeFormat(seconds, shortTime ? "sec" : "second")
29 | else if (minutes < 60) return timeFormat(minutes, shortTime ? "min" : "minute")
30 | else if (hours <= 24) return timeFormat(hours, "hour")
31 | else if (days <= 365) return timeFormat(days, "day")
32 | else return timeFormat(years, "year")
33 | }
34 |
35 | function addUhOh() {
36 | $('body').append(`
37 |
38 |
39 |
40 |
`)
41 | }
42 |
43 | function loginButton() {
44 | localStorage.polaris_url = window.location.pathname
45 | window.location.href = "/discord"
46 | }
47 |
48 | let mobile = ( /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent) )
--------------------------------------------------------------------------------
/classes/DatabaseModel.js:
--------------------------------------------------------------------------------
1 | // for the sake of the open source project, i've moved all the database methods here. maybe it'll help if you wanna switch to a different db
2 |
3 | const mongoose = require("mongoose")
4 |
5 | // connect to database
6 | const uri = process.env.MONGO_DB_URI
7 | const dbName = process.env.MONGO_DB_NAME || "polaris"
8 | const dbSettings = uri ? { dbName } : { dbName, user: process.env.MONGO_DB_USERNAME, pass: process.env.MONGO_DB_PASSWORD }
9 |
10 | mongoose.connect(uri || `mongodb://${process.env.MONGO_DB_IP}`, dbSettings)
11 | .then(() => console.log(`Database connected! (${+process.uptime().toFixed(2)} secs)`))
12 | .catch(e => { console.error('\x1b[40m\x1b[31m%s\x1b[0m', "!!! Error connecting to the database !!!"); console.error(e) })
13 |
14 | class Model {
15 | constructor(collectionName, schema) {
16 | this.schema = schema;
17 | this.model = mongoose.model(collectionName, this.schema);
18 |
19 | this.fetch = (id, filter, options) => this.model.findById(id, filter, options);
20 | this.update = (id, data, options) => this.model.findByIdAndUpdate(id, data, options);
21 | this.create = (data, options) => this.model.create(data, options);
22 | this.find = (query, filter, options) => this.model.find(query, filter, options);
23 | this.delete = (query, options) => this.model.deleteMany(query, options);
24 | }
25 | }
26 |
27 | module.exports = Model;
--------------------------------------------------------------------------------
/classes/LevelUpEmbed.js:
--------------------------------------------------------------------------------
1 | const Discord = require("discord.js")
2 | const Tools = require("./Tools.js")
3 |
4 | class LevelUpEmbed {
5 | constructor(data) {
6 |
7 | this.extraContent = null
8 | this.messageEmbed = null
9 |
10 | try {
11 | data = JSON.parse(data)
12 | let embed = data?.embeds[0]
13 | if (!embed || Array.isArray(embed) || typeof embed != "object") this.invalid = true
14 |
15 | if (data.content) this.extraContent = data.content
16 | this.messageEmbed = new Discord.EmbedBuilder(embed); // embed builder helps validate things
17 |
18 | }
19 | catch(e) {
20 | console.log(e)
21 | this.invalid = true
22 | }
23 |
24 | }
25 |
26 | json(returnFull=true) {
27 | if (this.invalid || !this.messageEmbed) return null
28 | let jsonData = this.messageEmbed.toJSON()
29 | delete jsonData.type
30 |
31 | // delete null values
32 | for (const [key, val] of Object.entries(jsonData)) {
33 | if (val === null || val === undefined) delete jsonData[key]
34 | }
35 |
36 | let fullData = returnFull ? { content: this.extraContent || undefined, embeds: [ jsonData ] } : jsonData
37 | return fullData
38 | }
39 | }
40 |
41 | module.exports = LevelUpEmbed;
--------------------------------------------------------------------------------
/classes/LevelUpMessage.js:
--------------------------------------------------------------------------------
1 | const ordinal = require('ordinal/indicator');
2 | const LevelUpEmbed = require("./LevelUpEmbed.js")
3 | const Tools = require("./Tools.js")
4 | const tools = Tools.global
5 |
6 | const ifLevelRegex = /\[\[\s*IFLEVEL\s*([=>!%]+)\s*(\d+)\s*\|(.+?)\]\]/
7 | const ordinalRegex = /(\d+)(\s*)\[\[\s*NTH\s*\]\]/
8 |
9 | class LevelUpMessage {
10 | constructor(settings, message, data={}) {
11 |
12 | this.channel = settings.levelUp.channel
13 | this.msg = settings.levelUp.message
14 | this.userMessage = message
15 | this.level = data.level
16 |
17 | let roleList = data.roleList || message.guild.roles.cache
18 | this.rewardRoles = settings.rewards.filter(x => x.level == data.level).map(x => roleList.find(r => r.id == x.id)).filter(x => x)
19 |
20 | if (settings.levelUp.rewardRolesOnly && !this.rewardRoles.length && !data.example) {
21 | this.invalid = true;
22 | return
23 | }
24 |
25 | this.variables = {
26 | "LEVEL": tools.commafy(data.level),
27 | "OLD_LEVEL": tools.commafy(data.oldLevel ?? data.level - 1),
28 | "XP": tools.commafy(data.userData.xp),
29 | "NEXT_LEVEL": Math.min(data.level + 1, settings.maxLevel),
30 | "NEXT_XP": tools.commafy(tools.xpForLevel(data.level + 1, settings) - data.userData.xp),
31 | "@": `<@${message.author.id}>`,
32 | "USERNAME": message.author.username,
33 | "DISPLAYNAME": message.author.displayName,
34 | "DISCRIM": message.author.discriminator,
35 | "ID": message.author.id,
36 | "NICKNAME": message.member.displayName,
37 | "AVATAR": message.member.avatarLink || message.member.displayAvatarURL({format: "png", dynamic: true}),
38 | "SERVER": message.guild.name,
39 | "SERVER_ID": message.guild.id,
40 | "SERVER_ICON": message.guild.iconLink || message.guild.iconURL({format: "png", dynamic: true}) || "",
41 | "CHANNEL": `<#${message.channel.id}>`,
42 | "CHANNEL_NAME": message.channel.name,
43 | "CHANNEL_ID": message.channel.id,
44 | "ROLE": this.rewardRoles.map(x => `<@&${x.id}>`).join(" "),
45 | "ROLE_NAME": this.rewardRoles.map(x => x.name).join(", "),
46 | "TIMESTAMP": Math.round(Date.now() / 1000),
47 | "EMBEDTIMESTAMP": new Date().toISOString()
48 | }
49 |
50 | if (settings.levelUp.embed) {
51 | let mbed = new LevelUpEmbed(this.msg)
52 | if (mbed.invalid) {
53 | this.msg = ""
54 | this.invalid = true
55 | }
56 | else {
57 | let mbedJSON = mbed.json(false)
58 |
59 | // add vars to all strings
60 | for (const [key, val] of Object.entries(mbedJSON)) {
61 | if (typeof val == "string") mbedJSON[key] = this.subVariables(val)
62 |
63 | // go one extra layer deep lmao
64 | else if (val && typeof val == "object" && !Array.isArray(val)) {
65 | for (const [key2, val2] of Object.entries(val)) {
66 | if (typeof val2 == "string") mbedJSON[key][key2] = this.subVariables(val2)
67 | }
68 | }
69 | }
70 |
71 | // add vars to fields
72 | if (mbedJSON.fields && mbedJSON.fields.length) {
73 | mbedJSON.fields = mbedJSON.fields.map(f => ({ name: this.subVariables(f.name), value: this.subVariables(f.value), inline: f.inline }))
74 | }
75 |
76 | this.msg = { embeds: [ mbedJSON ] }
77 | if (mbed.extraContent) this.msg.content = this.subVariables(mbed.extraContent)
78 | }
79 | }
80 |
81 | else this.msg = { content: this.subVariables(this.msg) }
82 |
83 | if (this.msg) this.msg.reply = { messageReference: message.id }
84 | }
85 |
86 | subVariables(msg) {
87 |
88 | if (!msg) return msg
89 | let newMsg = msg.replace(/\n/g, " ")
90 | let newLevel = this.level
91 |
92 | // simple variables
93 | let vars = this.variables
94 | newMsg = newMsg.replace(/\[\[[A-Z@_ ]+\]\]/g, function(str) {
95 | let v = str.slice(2, -2).trim()
96 | return vars[v] ?? str
97 | })
98 |
99 | // random choose
100 | newMsg = newMsg.replace(/\[\[\s*CHOOSE.+?\]\]/g, function(str) {
101 | let pool = []
102 | let totalWeight = 0
103 | let choose = str.slice(2, -2).split(/(? x.trim()).filter(x => x) // split at one | but not more
104 | choose[0] = choose[0].replace(/^\s*CHOOSE\s*/, "")
105 | if (!choose[0]) choose.shift()
106 |
107 | let chooseRegex = /^<([\d.]+)>\s+/
108 | if (choose.some(x => x.match(chooseRegex))) { // if list has weighting...
109 | choose.forEach(c => {
110 | let weightMatch = c.match(chooseRegex)
111 | let weight = weightMatch ? (Number(weightMatch[1])) || 1 : 1
112 | if (weight > 0) {
113 | weight = tools.clamp(Math.round(weight * 500), 1, 1e6)
114 | pool.push({ msg: c.replace(chooseRegex, ""), weight, index: totalWeight })
115 | totalWeight += weight
116 | }
117 | })
118 |
119 | let roll = tools.rng(0, totalWeight)
120 | let finalChoice = pool.reverse().find(x => roll >= x.index)
121 | return finalChoice.msg
122 | }
123 |
124 | else return tools.choose(choose)
125 | })
126 |
127 | // if level
128 | newMsg = newMsg.replace(new RegExp(ifLevelRegex, "g"), function(str) {
129 | let match = str.match(ifLevelRegex)
130 | let [all, operation, lvl, data] = match
131 | if (!data) return
132 | data = (data).trim()
133 | lvl = Number(lvl)
134 | if (isNaN(lvl)) return ""
135 |
136 | switch (operation.trim()) {
137 | case ">": return (newLevel > lvl ? data : "")
138 | case "<": return (newLevel < lvl ? data : "")
139 | case ">=": case "=>": return (newLevel >= lvl ? data : "")
140 | case "<=": case "=<": return (newLevel <= lvl ? data : "")
141 | case "!=": case "=!": case "=/": case "=/=": return (newLevel != lvl ? data : "")
142 | case "/": case "%": return (newLevel % lvl == 0 ? data : "")
143 | default: return (newLevel == lvl ? data : "")
144 | }
145 | })
146 |
147 | let rewardRoles = this.rewardRoles
148 |
149 | // if role
150 | newMsg = newMsg.replace(/\[\[\s*IFROLE\s*\|.+?\]\]/g, function(str) {
151 | if (!rewardRoles.length) return ""
152 | else return str.split("|").slice(1).join("|").slice(0, -2)
153 | })
154 |
155 | // if no role
156 | newMsg = newMsg.replace(/\[\[\s*IFNOROLE\s*\|.+?\]\]/g, function(str) {
157 | if (rewardRoles.length) return ""
158 | else return str.split("|").slice(1).join("|").slice(0, -2)
159 | })
160 |
161 | // nth
162 | newMsg = newMsg.replace(new RegExp(ordinalRegex, "g"), function(str) {
163 | let match = str.match(ordinalRegex)
164 | if (match) {
165 | let num = (Number(match[1]) || 0)
166 | let spacing = match[2] || ""
167 | return `${num}${spacing}${ordinal(num)}`
168 | }
169 | }).replace(/\[\[\s*NTH\s*\]\]/g, "")
170 |
171 | return newMsg.replace(/ /g, "\n").trim()
172 |
173 | }
174 |
175 | async send() {
176 | if (!this.msg || this.invalid) return
177 | let sendChannel = this.channel
178 | let ch =
179 | (sendChannel == "current") ? this.userMessage.channel
180 | : (sendChannel == "dm") ? this.userMessage.author
181 | : await this.userMessage.guild.channels.fetch(sendChannel).catch(() => {})
182 |
183 | if (ch && ch.id) ch.send(this.msg).catch((e) => {
184 | ch.send(`**Error sending level up message!**\n\`\`\`${e.message}\`\`\`\n(anyways, congrats on level ${this.variables.LEVEL}!)`).catch(() => {})
185 | })
186 | }
187 | }
188 |
189 | module.exports = LevelUpMessage;
--------------------------------------------------------------------------------
/classes/PageEmbed.js:
--------------------------------------------------------------------------------
1 | const Tools = require("./Tools.js")
2 | const tools = Tools.global
3 |
4 | const activeCollectors = {}
5 |
6 | class PageEmbed {
7 | constructor(embed, data, config={}) {
8 |
9 | this.fullData = data
10 |
11 | this.embed = embed
12 | this.page = config.page || 1
13 | this.size = config.size || 10
14 | this.extraButtons = config.extraButtons || []
15 | this.mapFunction = config.mapFunction
16 | this.timeoutSecs = config.timeoutSecs || 30
17 | this.ownerID = config.owner
18 | this.ephemeral = config.ephemeral
19 |
20 | this.suffix = embed.data.description
21 | this.footer = embed.data.footer?.text
22 |
23 | this.pages = Math.floor((this.fullData.length-1) / this.size) + 1
24 | if (this.page < 0) this.page = this.pages + this.page + 1 // if page is negative, start from last page
25 |
26 | this.data = this.paginate()
27 | this.setDesc()
28 |
29 | this.int = null
30 |
31 | if (this.ownerID) {
32 | let foundCollector = activeCollectors[this.ownerID]
33 | if (foundCollector) {
34 | foundCollector.stop()
35 | delete activeCollectors[this.ownerID]
36 | }
37 | }
38 |
39 | return this
40 | }
41 |
42 | paginate(pg=this.page) {
43 | return this.fullData.slice((pg - 1) * this.size, (pg - 1) * this.size + this.size)
44 | }
45 |
46 | setDesc() {
47 | let currentData = this.data
48 | if (typeof this.mapFunction == "function") currentData = currentData.map((x, y) => {
49 | let truePos = y + ((this.page - 1) * this.size) + 1
50 | return this.mapFunction(x, y, truePos)
51 | })
52 | return this.embed.setDescription(currentData.join("\n") + (this.suffix ? `\n${this.suffix}` : ""))
53 | }
54 |
55 | post(int, msgSettings={}) {
56 |
57 | let firstPage = (this.page == 1)
58 | let lastPage = (this.page >= this.pages)
59 |
60 | let pageOptions = [
61 | {style: firstPage ? "Secondary" : "Success", label: `<< Page ${firstPage ? this.pages : Math.max((this.page - 1) || 1, 1)}`, customId: 'prev'},
62 | {style: lastPage ? "Secondary" : "Success", label: `Page ${lastPage ? 1 : Math.min((this.page + 1), this.pages)} >>`, customId: 'next'}
63 | ]
64 |
65 | if (this.pages == 2) {
66 | if (this.page == 1) pageOptions.shift()
67 | else pageOptions.splice(1, 1)
68 | }
69 |
70 | let pageButtons = this.pages <= 1 ? this.extraButtons : tools.button(pageOptions).concat(this.extraButtons)
71 |
72 | let footerText = this.footer || ""
73 | if (this.pages > 1) footerText += `\nPage ${this.page} of ${this.pages}`
74 | if (footerText) this.embed.setFooter({text: footerText})
75 |
76 | let pgButtonRow = pageButtons[0] ? tools.row(pageButtons) : null
77 |
78 | if (!this.int) return int.reply(Object.assign({ embeds: [this.embed], components: pgButtonRow, fetchReply: true, ephemeral: this.ephemeral }, msgSettings)).then(msg => {
79 | this.int = int
80 | if (this.pages > 1) this.handleButtons(msg, pageButtons)
81 | }).catch(() => {})
82 |
83 | else return this.int.editReply({embeds: [this.embed], components: pgButtonRow }).then(msg => {
84 | this.handleButtons(msg, pageButtons)
85 | }).catch(() => {})
86 | }
87 |
88 | handleButtons(msg, buttons) {
89 | let buttonPressed = false
90 | let collector = msg.createMessageComponentCollector({ time: this.timeoutSecs * 1000 })
91 | if (this.ownerID) activeCollectors[this.ownerID] = collector
92 | collector.on('collect', b => {
93 | if (buttonPressed || !tools.canPressButton(b, [this.ownerID])) return tools.buttonReply(b)
94 | else buttonPressed = true
95 | collector.stop()
96 |
97 | switch (b.customId) {
98 | case "prev": { this.setPage(-1, b); return this.post() }
99 | case "next": { this.setPage(1, b); return this.post() }
100 | }
101 | })
102 | collector.on('end', b => {
103 | if (!buttonPressed) {
104 | this.int.editReply({ components: tools.disableButtons(buttons) })
105 | this.destroy()
106 | }
107 | })
108 | return msg.id
109 | }
110 |
111 | setPage(change, button, exact) {
112 | if (button) button.deferUpdate()
113 | let oldPage = this.page
114 |
115 | this.page = exact ? change : oldPage + change
116 | if (this.page < 1) this.page = this.pages
117 | if (this.page > this.pages) this.page = 1
118 |
119 | if (oldPage == this.page) return
120 | this.data = this.paginate()
121 | this.embed = this.setDesc()
122 | }
123 |
124 | destroy() {
125 | delete activeCollectors[this.ownerID]
126 | this.fullData = null
127 | this.data = null
128 | this.embed = null
129 | }
130 | }
131 |
132 | module.exports = PageEmbed;
--------------------------------------------------------------------------------
/classes/Tools.js:
--------------------------------------------------------------------------------
1 | const config = require('../config.json')
2 | const Discord = require('discord.js')
3 |
4 | // this class contains all sorts of misc functions used around the bot
5 |
6 | class Tools {
7 | constructor(client, int) {
8 |
9 | this.WEBSITE = config.siteURL
10 | if (!this.WEBSITE.startsWith("http")) this.WEBSITE = "https://gdcolon.com/polaris" // backup URL or some buttons will break
11 |
12 | this.COLOR = 0x00ff80 // polaris green
13 |
14 | // has manage guild perm
15 | this.canManageServer = function(member=int?.member, nahnvm) {
16 | return nahnvm || (member && member.permissions.has(Discord.PermissionFlagsBits.ManageGuild))
17 | }
18 |
19 | // has manage roles perm
20 | this.canManageRoles = function(member=int?.member, nahnvm) {
21 | return nahnvm || (member && member.permissions.has(Discord.PermissionFlagsBits.ManageRoles))
22 | }
23 |
24 | // is in developer list
25 | this.isDev = function(user=int.user) {
26 | return config.developer_ids.includes(user?.id)
27 | }
28 |
29 | // converts a string (e.g. "rank") into a clickable slash command
30 | this.commandTag = function(cmd) {
31 | let foundCmd = client.application.commands.cache.find(x => x.name == cmd && x.type == Discord.ApplicationCommandType.ChatInput)
32 | return foundCmd?.id ? `${cmd}:${foundCmd.id}>`: `\`/${cmd}\``
33 | }
34 |
35 | // some common error messages
36 | this.errors = {
37 | xpDisabled: `XP is not enabled in this server!${this.canManageServer() ? ` (enable with ${this.commandTag("config")})` : ""}`,
38 | noData: "This server doesn't have any data yet!",
39 | noBotXP: "Bots can't earn XP, silly!",
40 | cantManageRoles: "I don't have permission to manage roles!",
41 | notMod: "You don't have permission to use this command!"
42 | }
43 |
44 | // fetch settings from db/cache (+ some xp)
45 | this.fetchSettings = async function(userID, serverID=int.guild.id) {
46 | let data = await client.db.fetch(serverID, ["settings", userID ? `users.${userID}` : null])
47 | if (!data) {
48 | await client.db.create({ _id: serverID })
49 | return await this.fetchSettings(userID, serverID)
50 | }
51 | if (!data.users) data.users = {}
52 | return data
53 | }
54 |
55 | // fetch all xp in the server
56 | this.fetchAll = async function(serverID=int.guild.id) {
57 | return await client.db.fetch(serverID).then(data => {
58 | if (!data) return
59 | return data
60 | })
61 | }
62 |
63 | // calculates current level from xp
64 | this.getLevel = function(xp, settings, returnRequirement) {
65 | let lvl = 0
66 | let previousLevel = 0
67 | let xpRequired = 0
68 | while (xp >= xpRequired && lvl <= settings.maxLevel) { // cubic formula my ass, here's a while loop. could probably binary search this?
69 | lvl++
70 | previousLevel = xpRequired
71 | xpRequired = this.xpForLevel(lvl, settings)
72 | }
73 | lvl--
74 | return returnRequirement ? { level: lvl, xpRequired, previousLevel } : lvl
75 | }
76 |
77 | // calculate xp to reach a level
78 | this.xpForLevel = function(lvl, settings) {
79 | if (lvl > settings.maxLevel) lvl = settings.maxLevel
80 | let xpRequired = Object.entries(settings.curve).reduce((total, n) => total + (n[1] * (lvl ** n[0])), 0)
81 | return settings.rounding > 1 ? settings.rounding * Math.round(xpRequired / settings.rounding) : Math.round(xpRequired)
82 | }
83 |
84 | // get expected reward roles for a certain level
85 | this.getRolesForLevel = function(lvl, rewards) {
86 | if (!lvl || !rewards) return []
87 |
88 | let levelRoles = rewards.filter(x => x.level <= lvl) // get all reward roles less than or equal to level
89 | .sort((a, b) => b.level - a.level) // sort from highest to lowest level
90 |
91 | let topRole = levelRoles[0] // get highest level role
92 | if (topRole) levelRoles = levelRoles.filter(x => x.keep || (x.level == topRole.level)) // remove the rest of the non-keep roles
93 |
94 | return levelRoles
95 | }
96 |
97 | // check which level roles member should and shouldn't have
98 | this.checkLevelRoles = function(allRoles, roles, lvl, rewards, shouldHave, oldLevel) {
99 | rewards = rewards.filter(x => allRoles.some(r => r.id == x.id))
100 | if (!oldLevel) oldLevel = lvl
101 | if (!shouldHave) shouldHave = this.getRolesForLevel(lvl, rewards)
102 | let currentLevelRoles = rewards.filter(x => roles.some(r => r.id == x.id))
103 |
104 | let correct = []
105 | let missing = []
106 | shouldHave.forEach(x => {
107 | if (currentLevelRoles.some(r => r.id == x.id)) correct.push(x)
108 | else if (!x.noSync || (x.noSync && oldLevel < x.level)) missing.push(x)
109 | })
110 | let incorrect = currentLevelRoles.filter(x => !x.noSync && !shouldHave.some(r => r.id == x.id))
111 |
112 | return { current: currentLevelRoles, shouldHave, correct, incorrect, missing }
113 | }
114 |
115 | // adds missing level roles and removes incorrect ones
116 | this.syncLevelRoles = async function(member, list) {
117 | if (!member.guild.members.me.permissions.has(Discord.PermissionFlagsBits.ManageRoles)) return
118 | if (!list.incorrect.length && !list.missing.length) return
119 | let currentRoles = member.roles.cache
120 | let newRoles = currentRoles.map(x => x.id)
121 | .filter(x => !list.incorrect.some(r => r.id == x)) // remove incorrect roles
122 | .concat(list.missing.map(x => x.id)) // add missing roles
123 | return member.roles.set(newRoles)
124 | }
125 |
126 | // get and calculate xp multiplier (for both channels and roles)
127 | this.getMultiplier = function(member, settings, channel=int.channel) {
128 | let obj = { multiplier: 1, role: 1, channel: 1, roleList: [], channelList: [] }
129 | let memberRoles = member.roles.cache
130 |
131 | obj.rolePriority = settings.multipliers.rolePriority
132 | obj.channelStacking = settings.multipliers.channelStacking
133 |
134 | let thread = {}
135 | if (channel && channel.isThread()) {
136 | thread = channel
137 | channel = channel.parent
138 | }
139 |
140 | let channelIDs = [ thread?.id, channel?.id, channel?.parent?.id ] // channel order of priority (thread > channel > category)
141 | let foundChannelBoost = channelIDs.map(x => settings.multipliers.channels.find(c => c.id == x)).find(x => x)
142 |
143 | if (foundChannelBoost) {
144 | obj.channel = foundChannelBoost.boost
145 | obj.channelList = [foundChannelBoost]
146 | }
147 |
148 | let roleBoosts = settings.multipliers.roles.filter(x => memberRoles.has(x.id))
149 | let foundRoleBoost;
150 | if (roleBoosts.length) {
151 |
152 | let foundXPBan = roleBoosts.find(x => x.boost <= 0)
153 | if (foundXPBan) foundRoleBoost = foundXPBan
154 |
155 | else switch (obj.rolePriority) {
156 | case "smallest": // lowest boost
157 | foundRoleBoost = roleBoosts.sort((a, b) => a.boost - b.boost)[0]; break;
158 | case "highest": // highest role
159 | let foundTopBoost = memberRoles.sort((a, b) => b.position - a.position).find(x => roleBoosts.find(y => y.id == x.id))
160 | foundRoleBoost = roleBoosts.find(x => x.id == foundTopBoost.id); break;
161 | case "combine": // multiply all, holy shit
162 | let combined = roleBoosts.map(x => x.boost).reduce((a, b) => a * b, 1).toFixed(4)
163 | combined = Math.min(+combined, 1000000) // 1 million max
164 | obj.role = combined; obj.roleList = roleBoosts; break;
165 | case "add": // add (n-1) from each
166 | let filteredBoosts = roleBoosts.filter(x => x.boost != 1)
167 | let summed = filteredBoosts.length == 1 ? filteredBoosts[0].boost : filteredBoosts.map(x => x.boost).reduce((a, b) => a + (b-1), 1)
168 | obj.role = Number(summed.toFixed(4)); obj.roleList = filteredBoosts; break;
169 | default: // largest boost
170 | obj.rolePriority = "largest"
171 | foundRoleBoost = roleBoosts.sort((a, b) => b.boost - a.boost)[0]; break;
172 | }
173 |
174 | if (foundRoleBoost) {
175 | obj.role = foundRoleBoost.boost
176 | obj.roleList = [foundRoleBoost]
177 | }
178 | }
179 |
180 | if (obj.role <= 0 || obj.channel <= 0) obj.multiplier = 0 // 0 always takes priority
181 | else switch (settings.multipliers.channelStacking) {
182 | case "largest": obj.multiplier = Math.max(obj.role, obj.channel); break; // pick largest between channel and role
183 | case "channel": obj.multiplier = foundChannelBoost ? obj.channel : obj.role; break; // channel always takes priority if it exists
184 | case "role": obj.multiplier = foundRoleBoost ? obj.role : obj.channel; break; // role takes priority if it exists
185 | case "add": obj.multiplier = Math.max(0, 1 + (obj.role - 1) + (obj.channel - 1)); break; // add (n-1) from each
186 | default: obj.channelStacking = "multiply"; obj.multiplier = obj.role * obj.channel; break; // just multiply them together
187 | }
188 |
189 | obj.multiplier = Math.round(obj.multiplier * 10000) / 10000
190 |
191 | return obj
192 | }
193 |
194 | // error message if user has no xp
195 | this.noXPYet = function(user) {
196 | return this.warn(user.bot ? "*noBotXP" : user.id != int.user.id ? `${user.displayName} doesn't have any XP yet!` : `You don't have any XP yet!`)
197 | }
198 |
199 | // creates an embed from an object, because i despise how discord.js does it
200 | this.createEmbed = function(options={}) {
201 | let embed = new Discord.EmbedBuilder()
202 | if (options.title) embed.setTitle(options.title)
203 | if (options.description) embed.setDescription(options.description)
204 | if (options.color) embed.setColor(options.color)
205 | if (options.author) embed.setAuthor(typeof options.author == "string" ? {name: options.author, iconURL: int.member.displayAvatarURL()} : options.author)
206 | if (options.footer) embed.setFooter(typeof options.footer == "string" ? {text: options.footer} : options.footer)
207 | if (options.fields) embed.addFields(options.fields)
208 | if (options.timestamp) embed.setTimestamp()
209 | return embed
210 | }
211 |
212 | // creates a button (or multiple)
213 | this.button = function(buttonOptions) {
214 | let isArr = Array.isArray(buttonOptions)
215 | if (!isArr) buttonOptions = [buttonOptions]
216 | buttonOptions = buttonOptions.map(b => {
217 | if (typeof b.style == "string") b.style = Discord.ButtonStyle[b.style]
218 | return b
219 | })
220 |
221 | if (isArr) return buttonOptions.map(x => new Discord.ButtonBuilder(x))
222 | else return new Discord.ButtonBuilder(buttonOptions[0])
223 | }
224 |
225 | // creates two confirmation buttons
226 | this.confirmButtons = function(titleText, titleColor, cancelText, cancelColor) {
227 | return this.button([
228 | {style: titleColor || "Success", label: titleText || 'Confirm', customId: 'confirm'},
229 | {style: cancelColor || "Danger", label: cancelText || 'Cancel', customId: 'cancel'}
230 | ])
231 | }
232 |
233 | // check if user is allowed to interact with a button
234 | this.canPressButton = function(b, allowedUsers) {
235 | return (b.user.id.includes(allowedUsers || [int?.user.id]))
236 | }
237 |
238 | // ignore the press if the user can't press that button
239 | this.buttonReply = function(int, message) {
240 | return message ? int.reply(message) : int.deferUpdate()
241 | }
242 |
243 | // creates a component row without all the bullshit
244 | this.row = function(components) {
245 | if (!components || (Array.isArray(components) && !components[0])) return null
246 | if (!components.length) components = [components]
247 | return [new Discord.ActionRowBuilder({components})]
248 | }
249 |
250 | // disables all clickable buttons, optionally hide all except clicked
251 | this.disableButtons = function(btns, selected) {
252 | let disabledBtns = btns.map(x => x.data.style == Discord.ButtonStyle.Link ? x : x.setDisabled())
253 | if (selected) {
254 | selected.deferUpdate()
255 | disabledBtns = disabledBtns.filter(x => x.customId == selected.customId)
256 | }
257 | return this.row(disabledBtns)
258 | }
259 |
260 | // creates a timed yes/no confirmation (options: secs, buttons, message, timeoutMessage, onClick, onTimeout)
261 | this.createConfirmationButtons = function(options={}) {
262 |
263 | let secs = options.secs || 20
264 | let buttonData = options.buttons || []
265 | if (typeof buttonData == "string") buttonData = [buttonData]
266 | let confirmBtns = this.confirmButtons(...buttonData)
267 |
268 | let messageData = options.message || {}
269 | messageData.components = this.row(confirmBtns)
270 | messageData.fetchReply = true
271 |
272 | let activeConfirmation = true
273 | return int.reply(messageData).then(msg => {
274 |
275 | let collector = msg.createMessageComponentCollector({ time: secs * 1000 })
276 |
277 | collector.on('collect', (b) => {
278 | if (!activeConfirmation || !this.canPressButton(b)) return this.buttonReply()
279 |
280 | else {
281 | activeConfirmation = false
282 | msg.edit({components: this.disableButtons(confirmBtns, b)})
283 | if (options.onClick) return options.onClick(b.customId == "confirm", msg, b)
284 | }
285 |
286 | })
287 | collector.on('end', () => {
288 | if (activeConfirmation) {
289 | msg.edit({content: `~~${messageData.content}~~\n${options.timeoutMessage}`, components: this.disableButtons(confirmBtns)}).catch(() => {})
290 | if (options.onTimeout) return options.onTimeout(msg)
291 | }
292 | })
293 | })
294 | }
295 |
296 | // edit the message if possible, otherwise post as reply
297 | this.editOrReply = function(data, forceReply) {
298 | if (forceReply) int.reply(data).catch(() => null)
299 |
300 | else int.message.edit(data).catch(() => {
301 | int.reply(data).catch(() => null)
302 | }).then(() => int.deferUpdate())
303 | }
304 |
305 | // xp is stored as an object, convert to array
306 | this.xpObjToArray = function(users) {
307 | return Object.entries(users).map(x => Object.assign({id: x[0]}, x[1]))
308 | }
309 |
310 | // sends an ephemeral reply, usually when the user did something wrong
311 | this.warn = function(msg) {
312 | if (msg.startsWith("*")) msg = this.errors[msg.slice(1)] || msg
313 | return int.reply({content: this.errors[msg] || msg, ephemeral: true})
314 | }
315 |
316 | // get detailed position info on a channel, for sorting
317 | this.getTrueChannelPos = function(c, ChannelType=Discord.ChannelType) {
318 | let isThread = c.isThread()
319 | let channel = isThread ? c.parent : c
320 | let isCategory = channel?.type == ChannelType.GuildCategory
321 | return {
322 | group: isCategory ? channel.position : channel?.parent?.position ?? -1,
323 | section: channel && channel.isVoiceBased() ? 1 : 0,
324 | position: isCategory ? -1 : channel?.position + (isThread ? 0.5 : 0)
325 | }
326 | }
327 |
328 | // get setting from an id, e.g. "levelUp.multiple"
329 | this.getSettingFromID = function(id, settings) {
330 | let val = settings
331 | id.split(".").forEach(x => { val = val[x] })
332 | return val;
333 | }
334 |
335 | // random number between min and max, inclusive
336 | this.rng = function(min, max) {
337 | if (max == undefined && +min) { max = min; min = 1 } // rng(5) is the same as rng(1, 5)
338 | return Math.floor(Math.random() * (max - min + 1)) + min
339 | }
340 |
341 | // randomly pick from array
342 | this.choose = function(arr) {
343 | return arr[Math.floor(Math.random() * arr.length)];
344 | }
345 |
346 | // remove duplicates from array
347 | this.undupe = function(array) {
348 | if (!Array.isArray(array)) return array
349 | else return array.filter((x, y) => array.indexOf(x) == y)
350 | }
351 |
352 | // shuffle array
353 | this.shuffle = function(arr) {
354 | for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]] }
355 | return arr
356 | }
357 |
358 | // limit number between two values
359 | this.clamp = function(num, min, max) {
360 | return Math.min(Math.max(num, min), max)
361 | };
362 |
363 | // cut off text once it passes a certain length and add "..."
364 | this.limitLength = function(string, max, after="...") {
365 | if (string.length <= max) return string
366 | else return string.slice(0, max) + after
367 | }
368 |
369 | // capitalize first letter of word(s)
370 | this.capitalize = function(str, all) {
371 | let text = all ? str.split(" ") : [str]
372 | text = text.map(x => x.charAt(0).toUpperCase() + x.slice(1).toLowerCase())
373 | return text.join(" ")
374 | }
375 |
376 | // adds commas to long numbers
377 | this.commafy = function(num, locale="en-US") {
378 | return num.toLocaleString(locale, { maximumFractionDigits: 10 })
379 | }
380 |
381 | // convert timestamp to neat string (e.g. "3 minutes")
382 | this.time = function(ms, decimals=0, noS, shortTime) {
383 | let commafy = this.commafy
384 | if (ms > 3e16) return "Forever"
385 | function timeFormat(amount, str) {
386 | amount = +amount
387 | return `${commafy(amount)} ${str}${noS || amount == 1 ? "" : "s"}`
388 | }
389 | ms = Math.abs(ms)
390 | let seconds = (ms / 1000).toFixed(0)
391 | let minutes = (ms / (1000 * 60)).toFixed(decimals)
392 | let hours = (ms / (1000 * 60 * 60)).toFixed(decimals)
393 | let days = (ms / (1000 * 60 * 60 * 24)).toFixed(decimals)
394 | let years = (ms / (1000 * 60 * 60 * 24 * 365)).toFixed(decimals)
395 | if (seconds < 1) return timeFormat((ms / 1000).toFixed(2), shortTime ? "sec" : "second")
396 | if (seconds < 60) return timeFormat(seconds, shortTime ? "sec" : "second")
397 | else if (minutes < 60) return timeFormat(minutes, shortTime ? "min" : "minute")
398 | else if (hours <= 24) return timeFormat(hours, "hour")
399 | else if (days <= 365) return timeFormat(days, "day")
400 | else return timeFormat(years, "year")
401 | }
402 |
403 | // convert timestamp to h:m:s (e.g. 4:20)
404 | this.timestamp = function(ms, useTimeIfLong) {
405 | if (useTimeIfLong && ms >= 86399000) return this.time(ms, 1) // > 1 day
406 | let secs = Math.ceil(Math.abs(ms) / 1000)
407 | if (secs < 0) secs = 0
408 | let days = Math.floor(secs / 86400)
409 | if (days) secs -= days * 86400
410 | let timestamp = `${ms < 0 ? "-" : ""}${days ? `${days}d + ` : ""}${[Math.floor(+secs / 3600), Math.floor(+secs / 60) % 60, +secs % 60].map(v => v < 10 ? "0" + v : v).filter((v,i) => v !== "00" || i > 0).join(":")}`
411 | if (timestamp.length > 5) timestamp = timestamp.replace(/^0+/, "")
412 | return timestamp
413 | }
414 |
415 | // adds either 's or ' for plural nouns
416 | this.pluralS = function(msg="", full=true) {
417 | let extraS = msg.toLowerCase().endsWith("s") ? "" : "s"
418 | return full ? msg + "'" + extraS : extraS
419 | }
420 |
421 | // adds an extra s for plurals (e.g. 1 level, 2 levels)
422 | this.extraS = function(msg, count, onlyExtra, extra={}) {
423 | let extraStr = (count == 1) ? (extra.s || "") : (extra.p || "s")
424 | return onlyExtra ? extraStr : msg + extraStr
425 | }
426 |
427 | // debug: import xp from json
428 | this.jsonImport = function(serverID, url, xpKey="xp", idKey="id",) {
429 | fetch(url).then(res => res.json()).then(list => {
430 | let xpStuff = list.map(x => ({xp: Math.round(x[xpKey]), id: x[idKey]}));
431 | let users = {};
432 | xpStuff.forEach(x => users[x.id] = { xp: x.xp });
433 | client.db.update(serverID, { $set: { users } }).exec().then(() => { int.channel.send({ content: "Success!" }) })
434 | }).catch(e => { int.channel.send({ content: "Failed! " + e.message }) })
435 | }
436 |
437 | }
438 | }
439 |
440 | Tools.global = new Tools(); // use for files that never run functions involving the client
441 | module.exports = Tools;
--------------------------------------------------------------------------------
/commands/button/export_xp.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js')
2 | module.exports = {
3 | metadata: {
4 | name: "button:export_xp",
5 | },
6 |
7 | async run(client, int, tools) {
8 | let db = await tools.fetchSettings() // only fetch settings before checking perms
9 | if (!db) return tools.warn("*noData")
10 |
11 | if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
12 |
13 | await int.deferReply({ephemeral: true})
14 |
15 | let allData = await tools.fetchAll(); // fetch all data
16 |
17 | let jsonData = JSON.stringify({ settings: allData.settings, users: allData.users }, null, 2)
18 | let attached = new Discord.AttachmentBuilder(Buffer.from(jsonData, 'utf-8'), { name: `${int.guild.name}.json` })
19 | return int.followUp({content: `Here's all the data for **${int.guild.name}**, as of `, files: [attached], ephemeral: true})
20 | .catch(e => int.followUp({content: `**Something went wrong!** ${e.message}`, ephemeral: true}))
21 |
22 | }}
--------------------------------------------------------------------------------
/commands/button/list_multipliers.js:
--------------------------------------------------------------------------------
1 | const PageEmbed = require("../../classes/PageEmbed.js")
2 | const Discord = require("discord.js")
3 |
4 | module.exports = {
5 | metadata: {
6 | name: "button:list_multipliers",
7 | },
8 |
9 | async run(client, int, tools) {
10 | let db = await tools.fetchSettings()
11 | if (!db) return tools.warn("*noData")
12 |
13 | if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
14 |
15 | let isChannel = int.customId.split("~")[1] == "channels"
16 | let mType = isChannel ? "channel" : "role"
17 | let mList = db.settings.multipliers[isChannel ? "channels" : "roles"]
18 |
19 | if (!mList.length) return tools.warn(`This server doesn't have any ${mType} multipliers!`)
20 |
21 | let embed = tools.createEmbed({
22 | title: `${tools.capitalize(mType)} Multipliers (${mList.length})`,
23 | color: tools.COLOR,
24 | footer: "Add or remove multipliers with /multiplier"
25 | })
26 |
27 | let multipliers = mList.sort((a, b) => a.boost - b.boost);
28 |
29 | let categories;
30 | if (isChannel) {
31 | categories = await int.guild.channels.fetch().then(x => x.filter(c => c.type == Discord.ChannelType.GuildCategory).map(x => x.id))
32 | }
33 |
34 | let multiplierEmbed = new PageEmbed(embed, multipliers, {
35 | size: 20, owner: int.user.id,
36 | mapFunction: (x) => `**${x.boost}x:** ${isChannel ? (categories.includes(x.id) ? `**<#${x.id}>** (category)` : `<#${x.id}>`) : `<@&${x.id}>`}`
37 | })
38 |
39 | multiplierEmbed.post(int)
40 |
41 | }}
--------------------------------------------------------------------------------
/commands/button/list_reward_roles.js:
--------------------------------------------------------------------------------
1 | const PageEmbed = require("../../classes/PageEmbed.js")
2 |
3 | module.exports = {
4 | metadata: {
5 | name: "button:list_reward_roles",
6 | },
7 |
8 | async run(client, int, tools) {
9 | let db = await tools.fetchSettings()
10 | if (!db) return tools.warn("*noData")
11 |
12 | if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
13 |
14 | if (!db.settings.rewards.length) return tools.warn("This server doesn't have any reward roles!")
15 |
16 | let embed = tools.createEmbed({
17 | title: `Reward Roles (${db.settings.rewards.length})`,
18 | color: tools.COLOR,
19 | footer: "Add or remove reward roles with /rewardrole"
20 | })
21 |
22 | let rewards = db.settings.rewards.sort((a, b) => a.level - b.level);
23 |
24 | let rewardEmbed = new PageEmbed(embed, rewards, {
25 | size: 20, owner: int.user.id,
26 | mapFunction: (x) => `**Level ${x.level}** - <@&${x.id}>${x.keep ? " (keep)" : ""}${x.noSync ? " (no sync)" : ""}`
27 | })
28 |
29 | rewardEmbed.post(int)
30 |
31 | }}
--------------------------------------------------------------------------------
/commands/button/settings_edit.js:
--------------------------------------------------------------------------------
1 | const Discord = require("discord.js")
2 | const schema = require("../../database_schema.js").settingsIDs
3 |
4 | module.exports = {
5 | metadata: {
6 | name: "button:settings_edit",
7 | },
8 |
9 | async run(client, int, tools, modal) {
10 |
11 | let buttonData = int.customId.split("~")
12 | if (!modal && buttonData[2] != int.user.id) return int.deferUpdate()
13 |
14 | let settingID = modal || buttonData[1]
15 | let setting = schema[settingID]
16 | if (!setting) return tools.warn("Invalid setting!")
17 |
18 | let isBool = setting.type == "bool"
19 | let isNumber = (setting.type == "int" || setting.type == "float")
20 |
21 | if (!modal) {
22 | if (isNumber) {
23 | let numModal = new Discord.ModalBuilder()
24 | .setCustomId(`configmodal~${settingID}~${int.user.id}`)
25 | .setTitle("Edit setting")
26 |
27 | let numOption = new Discord.TextInputBuilder()
28 | .setLabel("New value")
29 | .setStyle(Discord.TextInputStyle.Short)
30 | .setCustomId("configmodal_value")
31 | .setMaxLength(20)
32 | .setRequired(true)
33 | if (!isNaN(setting.min) && !isNaN(setting.max)) numOption.setPlaceholder(`${tools.commafy(setting.min)} - ${tools.commafy(setting.max)}`)
34 |
35 | let numRow = new Discord.ActionRowBuilder().addComponents(numOption)
36 | numModal.addComponents(numRow)
37 | return int.showModal(numModal);
38 | }
39 | }
40 |
41 |
42 | let db = await tools.fetchSettings()
43 | if (!db) return tools.warn("*noData")
44 |
45 | let settings = db.settings
46 | if (!tools.canManageServer(int.member, settings.manualPerms)) return tools.warn("*notMod")
47 |
48 | let newValue;
49 | let oldValue = tools.getSettingFromID(settingID, settings);
50 |
51 | if (isBool) newValue = !oldValue
52 |
53 | else if (isNumber) {
54 | let modalVal = int.fields.getTextInputValue("configmodal_value")
55 |
56 | if (modalVal) {
57 | let num = Number(modalVal)
58 | if (isNaN(num)) return int.deferUpdate()
59 |
60 | if (setting.type == "int") num = Math.round(num)
61 |
62 | if (!isNaN(setting.min) && num < setting.min) num = setting.min
63 | else if (!isNaN(setting.max) && num > setting.max) num = setting.max
64 |
65 | newValue = num
66 | }
67 | }
68 |
69 | if (newValue === undefined || newValue == oldValue) return int.deferUpdate()
70 |
71 | client.db.update(int.guild.id, { $set: { [`settings.${settingID}`]: newValue, 'info.lastUpdate': Date.now() }}).then(() => {
72 | client.commands.get("button:settings_view").run(client, int, tools, ["val", null, settingID])
73 | }).catch(() => tools.warn("Something went wrong while trying to change this setting!"))
74 |
75 | }}
--------------------------------------------------------------------------------
/commands/button/settings_list.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js')
2 | const config = require("../../json/quick_settings.json")
3 | const schema = require("../../database_schema.js").settingsIDs
4 |
5 | const rootFolder = "home"
6 |
7 | module.exports = {
8 | metadata: {
9 | name: "button:settings_list",
10 | },
11 |
12 | async run(client, int, tools, selected) {
13 |
14 | let buttonData = [];
15 | if (int.isButton) {
16 | buttonData = int.customId.split("~")
17 | if (buttonData[2] && buttonData[2] != int.user.id) return int.deferUpdate()
18 | }
19 |
20 | let db = await tools.fetchSettings()
21 | if (!db) return tools.warn("*noData")
22 |
23 | let settings = db.settings
24 | if (!tools.canManageServer(int.member, settings.manualPerms)) return tools.warn("*notMod")
25 |
26 | // displays the preview value for a setting
27 | function previewSetting(val, data, schema) {
28 | if (data.zeroText && val === 0) return data.zeroText
29 | switch(schema.type) {
30 | case "bool": return (data.invert ? !val : val) ? "__True__" : "False";
31 | case "int": return tools.commafy(val);
32 | case "float": return tools.commafy(Number(val.toFixed(schema.precision || 4)));
33 | }
34 | return val.toString()
35 | }
36 |
37 | function getDataEmoji(type, val) {
38 | if (type == "bool") return val ? "✅" : "❎"
39 | else if (type == "int" || type == "float") return "#️⃣"
40 | else return "📝"
41 | }
42 |
43 | let dirName = (selected ? selected[1] : int.isButton ? buttonData[1] : rootFolder) || rootFolder
44 | let entries = config[dirName]
45 |
46 | if (!entries) return tools.warn("Invalid category!")
47 |
48 | let rows = []
49 | let options = []
50 | let groupName = "Settings"
51 | let isHome = (dirName == rootFolder)
52 |
53 | entries.forEach(x => {
54 | if (x.groupName) groupName = x.groupName
55 |
56 | if (x.folder) {
57 | let emoji = x.emoji || "📁"
58 | rows.push(`${emoji} **${x.name}**`)
59 | options.push({ emoji, label: x.name, value: `config_dir_${x.folder}` })
60 | }
61 |
62 | else if (x.db) {
63 | let val = tools.getSettingFromID(x.db, settings)
64 | let sch = schema[x.db]
65 | rows.push(`**${x.name}**: ${previewSetting(val, x, sch)}`)
66 | options.push({ emoji: getDataEmoji(sch.type, val), label: x.name, description: tools.limitLength(x.desc, 95), value: `config_val_${dirName}_${x.db}` })
67 | }
68 |
69 | if (x.space || x.folder == "home") rows.push("")
70 | })
71 |
72 | let embed = tools.createEmbed({
73 | color: tools.COLOR,
74 | title: groupName,
75 | description: rows.join("\n"),
76 | footer: isHome ? "Most basic settings can be toggled from here" : null
77 | })
78 |
79 | let dropdown = new Discord.StringSelectMenuBuilder()
80 | .setCustomId(`configmenu_${int.user.id}`)
81 | .setPlaceholder(isHome ? "Choose category..." : "Choose setting...")
82 | .addOptions(...options)
83 |
84 | tools.editOrReply({ embeds: [embed], components: tools.row(dropdown) }, !buttonData[2])
85 | }}
--------------------------------------------------------------------------------
/commands/button/settings_view.js:
--------------------------------------------------------------------------------
1 | const config = require("../../json/quick_settings.json")
2 | const schema = require("../../database_schema.js").settingsIDs
3 |
4 | module.exports = {
5 | metadata: {
6 | name: "button:settings_view",
7 | },
8 |
9 | async run(client, int, tools, selected) {
10 |
11 | let db = await tools.fetchSettings()
12 | if (!db) return tools.warn("*noData")
13 |
14 | let settings = db.settings
15 | if (!tools.canManageServer(int.member, settings.manualPerms)) return tools.warn("*notMod")
16 |
17 | let group = selected[1]
18 | let settingID = selected[2]
19 | let setting = schema[settingID]
20 |
21 | if (!setting) return tools.warn("Invalid setting!")
22 |
23 | // find group the hard way, if not provided
24 | if (!group) {
25 | for (const [g, x] of Object.entries(config)) {
26 | if (x.find(z => z.db == settingID)) {
27 | group = g
28 | break;
29 | }
30 | }
31 | }
32 |
33 | let val = tools.getSettingFromID(settingID, settings)
34 | let data = config[group].find(x => x.db == settingID)
35 |
36 | function previewSetting(val) {
37 | if (data.zeroText && val === 0) return `0 (${data.zeroText})`
38 | else switch(setting.type) {
39 | case "bool": return ((data.invert ? !val : val) ? "True" : "False");
40 | case "int": return tools.commafy(+val);
41 | case "float": return tools.commafy(Number(val.toFixed(setting.precision || 8)));
42 | }
43 | return val.toString()
44 | }
45 |
46 | let currentVal = previewSetting(val)
47 |
48 | let footer = data.tip || ""
49 | if (setting.default !== undefined) footer += `${footer ? "\n" : ""}Default: ${previewSetting(setting.default)}`
50 |
51 | let embed = tools.createEmbed({
52 | color: tools.COLOR,
53 | title: data.name,
54 | description: `**Current value:** ${currentVal}\n\n💡 ${data.desc}`,
55 | footer: footer || null
56 | })
57 |
58 | let buttons = tools.button([
59 | {style: "Secondary", label: "Back", customID: `settings_list~${group}~${int.user.id}`},
60 | {style: "Primary", label: (setting.type == "bool") ? "Toggle" : "Edit", customId: `settings_edit~${settingID}~${int.user.id}`}
61 | ])
62 |
63 | tools.editOrReply({embeds: [embed], components: tools.row(buttons)})
64 |
65 | }}
--------------------------------------------------------------------------------
/commands/button/toggle_xp.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js')
2 | module.exports = {
3 | metadata: {
4 | name: "button:toggle_xp",
5 | },
6 |
7 | async run(client, int, tools) {
8 | let enabled = int.component.style == Discord.ButtonStyle.Success
9 | let db = await tools.fetchSettings()
10 | if (!db) return tools.warn("*noData")
11 |
12 | let settings = db.settings
13 |
14 | if (!tools.canManageServer(int.member, settings.manualPerms)) return tools.warn("*notMod")
15 |
16 | if (enabled == settings.enabled) return tools.warn(`XP is already ${enabled ? "enabled" : "disabled"} in this server!`)
17 |
18 | client.db.update(int.guild.id, { $set: { 'settings.enabled': enabled, 'info.lastUpdate': Date.now() }}).then(() => {
19 | int.reply(`✅ **XP is now ${enabled ? "enabled" : "disabled"} in this server!**`)
20 | }).catch(() => tools.warn("Something went wrong while trying to toggle XP!"))
21 | }}
--------------------------------------------------------------------------------
/commands/events/message.js:
--------------------------------------------------------------------------------
1 | const LevelUpMessage = require("../../classes/LevelUpMessage.js")
2 | const config = require("../../config.json")
3 |
4 | module.exports = {
5 |
6 | async run(client, message, tools) {
7 |
8 | if (config.lockBotToDevOnly && !tools.isDev(message.author)) return
9 |
10 | // fetch server xp settings, this can probably be optimized with caching but shrug
11 | let author = message.author.id
12 | let db = await tools.fetchSettings(author, message.guild.id)
13 | if (!db || !db.settings?.enabled) return
14 |
15 | let settings = db.settings
16 |
17 | // fetch user's xp, or give them 0
18 | let userData = db.users[author] || { xp: 0, cooldown: 0 }
19 | if (userData.cooldown > Date.now()) return // on cooldown, stop here
20 |
21 | // check role+channel multipliers, exit if 0x
22 | let multiplierData = tools.getMultiplier(message.member, settings, message.channel)
23 | if (multiplierData.multiplier <= 0) return
24 |
25 | // randomly choose an amount of XP to give
26 | let oldXP = userData.xp
27 | let xpRange = [settings.gain.min, settings.gain.max].map(x => Math.round(x * multiplierData.multiplier))
28 | let xpGained = tools.rng(...xpRange) // number between min and max, inclusive
29 |
30 | if (xpGained > 0) userData.xp += Math.round(xpGained)
31 | else return
32 |
33 | // set xp cooldown
34 | if (settings.gain.time > 0) userData.cooldown = Date.now() + (settings.gain.time * 1000)
35 |
36 | // if hidden from leaderboard, unhide since they're no longer inactive
37 | if (userData.hidden) userData.hidden = false
38 |
39 | // database update
40 | client.db.update(message.guild.id, { $set: { [`users.${author}`]: userData } }).exec();
41 |
42 | // check for level up
43 | let oldLevel = tools.getLevel(oldXP, settings)
44 | let newLevel = tools.getLevel(userData.xp, settings)
45 | let levelUp = newLevel > oldLevel
46 |
47 | // auto sync roles on xp gain or level up
48 | let syncMode = settings.rewardSyncing.sync
49 | if (syncMode == "xp" || (syncMode == "level" && levelUp)) {
50 | let roleCheck = tools.checkLevelRoles(message.guild.roles.cache, message.member.roles.cache, newLevel, settings.rewards, null, oldLevel)
51 | tools.syncLevelRoles(message.member, roleCheck).catch(() => {})
52 | }
53 |
54 | // level up message
55 | if (levelUp && settings.levelUp.enabled && settings.levelUp.message) {
56 | let useMultiple = (settings.levelUp.multiple > 1 && (settings.levelUp.multipleUntil == 0 || (newLevel < settings.levelUp.multipleUntil)))
57 | if (!useMultiple || (newLevel % settings.levelUp.multiple == 0)) {
58 | let lvlMessage = new LevelUpMessage(settings, message, { oldLevel, level: newLevel, userData })
59 | lvlMessage.send()
60 | }
61 | }
62 |
63 | }}
--------------------------------------------------------------------------------
/commands/misc/json_import.js:
--------------------------------------------------------------------------------
1 | const Tools = require("../../classes/Tools.js")
2 | let tools = Tools.global
3 |
4 | module.exports = {
5 |
6 | async run(client, serverID, importSettings={}, jsonData) {
7 |
8 | let details = []
9 | let newData = {}
10 | let importedUsers = 0
11 |
12 | if (jsonData.xp) jsonData.users = jsonData.xp // in case someone messes this up
13 | if (!jsonData.users && !jsonData.settings && jsonData instanceof Object && !Array.isArray(jsonData)) jsonData = { users: jsonData } // if no keys provided, assume it's just XP
14 |
15 | if (jsonData.users && importSettings.xp) {
16 | let userEntries = Object.entries(jsonData.users)
17 | if (!importSettings.isDev && userEntries.length > 2000) return { error: "You can only import up to 2000 users, unless you're a developer of the bot! Remove any invalid IDs, or users with low XP." }
18 |
19 | userEntries.forEach(u => {
20 | const [id, x] = u
21 | if (id.match(/\d{16,20}/g) && !isNaN(x?.xp)) {
22 | importedUsers++
23 |
24 | // validate the values here, since the db doesn't
25 | let obj = { xp: Number(x.xp) }
26 | if (!isNaN(x.cooldown)) obj.cooldown = Math.round(x.cooldown)
27 | if (x.hidden) obj.hidden = true
28 |
29 | newData[`users.${id}`] = obj
30 | }
31 | })
32 | details.push(`${tools.commafy(importedUsers)} user${importedUsers == 1 ? "" : "s"}`)
33 | }
34 |
35 | if (jsonData.settings && importSettings.settings) {
36 | newData["settings"] = jsonData.settings // this should really really really really be validated but the schema is enough for me ¯\_(ツ)_/¯
37 | details.push(`Server settings`)
38 | }
39 |
40 | if (!details.length) return { error: `No JSON data found! Syntax is { users: {...} }, settings: {...} }` }
41 |
42 | return { data: newData, details }
43 |
44 | }
45 |
46 | }
--------------------------------------------------------------------------------
/commands/misc/polaris_transfer.js:
--------------------------------------------------------------------------------
1 | const Tools = require("../../classes/Tools.js")
2 | let tools = Tools.global
3 |
4 | module.exports = {
5 |
6 | async run(client, serverID, importSettings={}, guilds) {
7 |
8 | let transferFrom = importSettings.serverID
9 | let foundServer = guilds.find(x => x.id == transferFrom)
10 |
11 | if (!foundServer) return { error: "Not in server" }
12 | else if (foundServer && !foundServer.owner) return { error: "Not owner in server" }
13 | else if (foundServer.id == serverID) return { error: "Cannot transfer from the same server" }
14 |
15 | let toTransfer = []
16 | if (importSettings.xp) toTransfer.push("users")
17 | if (importSettings.settings) toTransfer.push("settings")
18 | if (!toTransfer.length) return { error: "Invalid import options!" }
19 |
20 | let details = []
21 | let importedUsers = 0
22 |
23 | let transferData = await client.db.fetch(transferFrom, toTransfer)
24 | if (!transferData) return { error: `No Polaris data found for ${foundServer.name}`, code: "invalidImport" }
25 |
26 | let newData = {}
27 |
28 | if (importSettings.xp) {
29 | let now = Date.now();
30 | Object.entries(transferData.users).forEach(u => {
31 | importedUsers++
32 | let xpVal = { xp: u[1].xp }
33 | if (u[1].cooldown && u[1].cooldown > now) xpVal.cooldown = u[1].cooldown
34 | newData[`users.${u[0]}`] = xpVal
35 | })
36 | details.push(`${tools.commafy(importedUsers)} user${importedUsers == 1 ? "" : "s"}`)
37 | }
38 |
39 | if (importSettings.settings) {
40 | let currentSettings = await client.db.fetch(serverID, "settings").then(x => x.settings)
41 | let transferSettings = transferData.settings
42 | transferSettings.rewards = currentSettings.rewards
43 | transferSettings.multipliers.roles = currentSettings.multipliers.roles
44 | transferSettings.multipliers.channels = currentSettings.multipliers.channels
45 | if (transferSettings.levelUp.channel.length > 8) transferSettings.levelUp.channel = currentSettings.levelUp.channel
46 | newData["settings"] = transferSettings
47 | details.push(`Server settings`)
48 | }
49 |
50 | return { data: newData, details }
51 |
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/commands/slash/addxp.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | metadata: {
3 | permission: "ManageGuild",
4 | name: "addxp",
5 | description: "Add or remove XP from a member. (requires manage server permission)",
6 | args: [
7 | { type: "user", name: "member", description: "Which member to modify", required: true },
8 | { type: "integer", name: "xp", description: "How much XP to add (negative number to remove XP)", min: -1e10, max: 1e10, required: true },
9 | { type: "string", name: "operation", description: "How the XP amount should be interpreted", required: false, choices: [
10 | {name: "Add XP", value: "add_xp"},
11 | {name: "Set XP to", value: "set_xp"},
12 | {name: "Add levels", value: "add_level"},
13 | {name: "Set level to", value: "set_level"},
14 | ]},
15 | ]
16 | },
17 |
18 | async run(client, int, tools) {
19 |
20 | const member = int.options.get("member")?.member
21 | const amount = int.options.get("xp")?.value
22 | const operation = int.options.get("operation")?.value || "add_xp"
23 |
24 | let user = member?.user
25 | if (!user) return tools.warn("I couldn't find that member!")
26 |
27 | let db = await tools.fetchSettings(user.id)
28 | if (!db) return tools.warn("*noData")
29 | else if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
30 | else if (!db.settings.enabled) return tools.warn("*xpDisabled")
31 |
32 | if (amount === 0 && operation.startsWith("add")) return tools.warn("Invalid amount of XP!")
33 | else if (user.bot) return tools.warn("You can't give XP to bots, silly!")
34 |
35 | let currentXP = db.users[user.id]
36 | let xp = currentXP?.xp || 0
37 | let level = tools.getLevel(xp, db.settings)
38 |
39 | let newXP = xp
40 | let newLevel = level
41 |
42 | switch (operation) {
43 | case "add_xp": newXP += amount; break;
44 | case "set_xp": newXP = amount; break;
45 | case "add_level": newLevel += amount; break;
46 | case "set_level": newLevel = amount; break;
47 | }
48 |
49 | newXP = Math.max(0, newXP) // min 0
50 | newLevel = tools.clamp(newLevel, 0, db.settings.maxLevel) // between 0 and max level
51 |
52 | if (newXP != xp) newLevel = tools.getLevel(newXP, db.settings)
53 | else if (newLevel != level) newXP = tools.xpForLevel(newLevel, db.settings)
54 |
55 | let syncMode = db.settings.rewardSyncing.sync
56 | if (syncMode == "xp" || (syncMode == "level" && newLevel != level) || (newLevel > level)) {
57 | let roleCheck = tools.checkLevelRoles(int.guild.roles.cache, member.roles.cache, newLevel, db.settings.rewards)
58 | tools.syncLevelRoles(member, roleCheck).catch(() => {})
59 | }
60 | let xpDiff = newXP - xp
61 |
62 | client.db.update(int.guild.id, { $set: { [`users.${user.id}.xp`]: newXP } }).then(() => {
63 | int.reply(`${newXP > xp ? "⏫" : "⏬"} ${user.displayName} now has **${tools.commafy(newXP)}** XP${newLevel != level ? ` and is **level ${newLevel}**` : ""}! (previously ${tools.commafy(xp)}, ${xpDiff >= 0 ? "+" : ""}${tools.commafy(xpDiff)})`)
64 | }).catch(() => tools.warn("Something went wrong while trying to modify XP!"))
65 |
66 | }}
--------------------------------------------------------------------------------
/commands/slash/botstatus.js:
--------------------------------------------------------------------------------
1 | const { dependencies } = require('../../package.json');
2 | const config = require("../../config.json")
3 |
4 | module.exports = {
5 | metadata: {
6 | name: "botstatus",
7 | description: "View some details about the bot"
8 | },
9 |
10 | async run(client, int, tools) {
11 |
12 | let versionNumber = client.version.version != Math.round(client.version.version) ? client.version.version : client.version.version.toFixed(1)
13 |
14 | let stats = await client.shard.broadcastEval(cl => ({ guilds: cl.guilds.cache.size, users: cl.users.cache.size }))
15 | let totalServers = stats.reduce((a, b) => a + b.guilds, 0)
16 |
17 | let botStatus = [
18 | `**Original creator:** **[Colon](https://gdcolon.com)** 🦊⛩️`,
19 | `**Version:** v${versionNumber} - updated `,
20 | `**Shard:** ${client.shard.id}/${client.shard.count - 1}`,
21 | `**Uptime:** ${tools.timestamp(client.uptime)}`,
22 | `**Servers:** ${tools.commafy(totalServers)}${client.shard.count == 1 ? "" : ` (on shard: ${tools.commafy(client.guilds.cache.size)})`}`,
23 | `**Memory usage:** ${Number((process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2))} MB`
24 | ]
25 |
26 | let embed = tools.createEmbed({
27 | author: { name: client.user.displayName, iconURL: client.user.avatarURL() },
28 | color: tools.COLOR, timestamp: true, footer: "Pinging...",
29 | description: botStatus.join("\n")
30 | })
31 |
32 | let infoButtons = [{style: "Link", label: "Website", url: `${tools.WEBSITE}`}]
33 | if (config.changelogURL) infoButtons.push({style: "Link", label: "Changelog", url: config.changelogURL})
34 | if (config.supportURL) infoButtons.push({style: "Link", label: "Support", url: config.supportURL})
35 |
36 | int.reply({embeds: [embed], components: tools.row(tools.button(infoButtons)), fetchReply: true}).then(msg => {
37 | embed.setFooter({ text: `Ping: ${tools.commafy(msg.createdTimestamp - int.createdAt)}ms`})
38 | int.editReply({ embeds: [embed], components: msg.components })
39 | })
40 |
41 | }}
--------------------------------------------------------------------------------
/commands/slash/calculate.js:
--------------------------------------------------------------------------------
1 | const multiplierModes = require("../../json/multiplier_modes.json")
2 |
3 | module.exports = {
4 | metadata: {
5 | name: "calculate",
6 | description: "Check how much XP you need to reach a certain level.",
7 | args: [
8 | { type: "integer", name: "target", description: "The desired level", min: 1, max: 1000, required: true },
9 | { type: "user", name: "member", description: "Which member to check", required: false }
10 | ]
11 | },
12 |
13 | async run(client, int, tools) {
14 |
15 | let member = int.member
16 | let foundUser = int.options.get("member")
17 | if (foundUser) member = foundUser.member
18 |
19 | let db = await tools.fetchSettings(member.id)
20 | if (!db) return tools.warn("*noData")
21 | else if (!db.settings.enabled) return tools.warn("*xpDisabled")
22 |
23 | let targetLevel = Math.min(int.options.get("target").value, db.settings.maxLevel)
24 | let targetXP = tools.xpForLevel(targetLevel, db.settings)
25 |
26 | let cardCol = db.settings.rankCard.embedColor
27 | if (cardCol == -1) cardCol = null
28 |
29 | if (db.settings.rankCard.disabled) {
30 | let miniEmbed = tools.createEmbed({
31 | title: `Level ${tools.commafy(targetLevel)}`,
32 | color: cardCol || member.displayColor || await member.user.fetch().then(x => x.accentColor),
33 | description: `${tools.commafy(targetXP)} XP required`,
34 | footer: "Rank cards are disabled, so calculations are hidden!"
35 | })
36 | return int.reply({embeds: [miniEmbed]})
37 | }
38 |
39 | let currentXP = db.users[member.id]
40 | if (!currentXP || !currentXP.xp) return tools.noXPYet(foundUser ? foundUser.user : int.user)
41 | let xp = currentXP.xp
42 | let userLevel = tools.getLevel(xp, db.settings)
43 |
44 | let remaining = targetXP - xp
45 | let reached = remaining <= 0
46 | let percent = xp / targetXP * 100
47 |
48 | let barSize = 33
49 | let barRepeat = Math.min(barSize, Math.round(percent / (100 / barSize)))
50 | let progressBar = `${"▓".repeat(barRepeat)}${"░".repeat(barSize - barRepeat)} (${Number(percent.toFixed(2))}%)`
51 |
52 | if (targetLevel == userLevel && userLevel >= db.settings.maxLevel) progressBar += `\n🎉 You reached the maximum level${db.settings.maxLevel < 1000 ? " in this server" : ""}! Congratulations!`
53 |
54 | let multiplierData = tools.getMultiplier(member, db.settings)
55 | let multiplier = multiplierData.multiplier || multiplierData.role
56 | if (multiplier <= 0) return int.reply("Your multiplier prevents you from gaining any XP!")
57 |
58 | let estimatedMin = Math.ceil(remaining / (db.settings.gain.min * multiplier))
59 | let estimatedMax = Math.ceil(remaining / (db.settings.gain.max * multiplier))
60 | let estimatedAvg = Math.round((estimatedMax + estimatedMin) / 2)
61 | let estimatedTime = estimatedAvg * db.settings.gain.time
62 |
63 | let estimatedRange = (estimatedMax == estimatedMin) ? `${tools.commafy(estimatedMax)}` : `${tools.commafy(estimatedMax)} - ${tools.commafy(estimatedMin)} (avg. ${tools.commafy(estimatedAvg)})`
64 |
65 | let levelDetails = [
66 | `**Current XP: **${tools.commafy(xp)} (Level ${tools.commafy(userLevel)})`,
67 | `**Target XP: **${tools.commafy(targetXP)}`,
68 | `**Remaining XP: **${reached? "0 (" : ""}${tools.commafy(targetXP - xp)}${reached ? ")" : ""}`
69 | ]
70 |
71 | if (!reached) levelDetails = levelDetails.concat([
72 | "",
73 | `**XP per message: **${db.settings.gain.min == db.settings.gain.max ? tools.commafy(Math.round(db.settings.gain.min * multiplier)) : `${tools.commafy(Math.round(db.settings.gain.min * multiplier))} - ${tools.commafy(Math.round(db.settings.gain.max * multiplier))}`}`,
74 | `**Messages remaining: **${estimatedRange}`,
75 | `**Cooldown remaining: **${estimatedTime == Infinity ? "Until the end of time" : tools.time(estimatedTime * 1000, 1)}`,
76 | ])
77 |
78 | let embed = tools.createEmbed({
79 | author: { name: member.user.displayName, iconURL: member.displayAvatarURL() },
80 | title: `Level ${tools.commafy(targetLevel)}${reached ? " (reached!)" : ""}`,
81 | color: cardCol || member.displayColor || await member.user.fetch().then(x => x.accentColor),
82 | description: levelDetails.join("\n"), footer: progressBar
83 | })
84 |
85 | return int.reply({embeds: [embed]})
86 |
87 | }}
--------------------------------------------------------------------------------
/commands/slash/clear.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | metadata: {
3 | permission: "ManageGuild",
4 | name: "clear",
5 | description: "Clear a member's cooldown. (requires manage server permission)",
6 | args: [
7 | { type: "user", name: "member", description: "Which member to clear", required: true }
8 | ]
9 | },
10 |
11 | async run(client, int, tools) {
12 |
13 | const user = int.options.get("member")?.user
14 |
15 | let db = await tools.fetchSettings(user.id)
16 | if (!db) return tools.warn("*noData")
17 | else if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
18 | else if (!db.settings.enabled) return tools.warn("*xpDisabled")
19 |
20 | if (user.bot) return tools.warn("Bots don't have cooldowns, silly!")
21 |
22 | let current = db.users[user.id]
23 | let cooldown = current?.cooldown
24 | if (!cooldown || cooldown <= Date.now()) return tools.warn("This member doesn't have an active cooldown!")
25 |
26 | client.db.update(int.guild.id, { $set: { [`users.${user.id}.cooldown`]: 0 } }).then(() => {
27 | int.reply(`🔄 **${tools.pluralS(user.displayName)} cooldown has been reset!** (previously ${tools.timestamp(cooldown - Date.now())})`)
28 | }).catch(() => tools.warn("Something went wrong while trying to reset the cooldown!"))
29 |
30 | }}
--------------------------------------------------------------------------------
/commands/slash/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | metadata: {
3 | permission: "ManageGuild",
4 | name: "config",
5 | description: "Toggle XP gain, or visit the dashboard to tweak server settings. (requires manage server permission)",
6 | },
7 |
8 | async run(client, int, tools) {
9 |
10 | let db = await tools.fetchSettings()
11 | let settings = db.settings
12 | if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
13 |
14 | let polarisSettings = [
15 | `**✨ XP enabled: __${settings.enabled ? "Yes!" : "No!"}__**`,
16 | `**XP per message:** ${settings.gain.min == settings.gain.max ? tools.commafy(settings.gain.min) : `${tools.commafy(settings.gain.min)} - ${tools.commafy(settings.gain.max)}`}`,
17 | `**XP cooldown:** ${tools.commafy(settings.gain.time)} ${tools.extraS("sec", settings.gain.time)}`,
18 | `**XP curve:** ${settings.curve[3]}x³ + ${settings.curve[2]}x² + ${settings.curve[1]}x`,
19 | `**Level up message:** ${settings.levelUp.enabled && settings.levelUp.message ? (settings.levelUp.embed ? "Enabled (embed)" : "Enabled") : "Disabled"}`,
20 | `**Rank cards:** ${settings.rankCard.disabled ? "Disabled" : settings.rankCard.ephemeral ? "Enabled (forced hidden)" : "Enabled"}`,
21 | `**Leaderboard:** ${settings.leaderboard.disabled ? "Disabled" : `[${settings.leaderboard.private ? "Private" : "Public"}](<${tools.WEBSITE}/leaderboard/${int.guild.id}>)`}`
22 | ]
23 |
24 | let embed = tools.createEmbed({
25 | author: { name: "Settings for " + int.guild.name, iconURL: int.guild.iconURL() },
26 | footer: "Visit the online dashboard to change server settings",
27 | color: tools.COLOR, timestamp: true,
28 | description: polarisSettings.join("\n")
29 | })
30 |
31 | let toggleButton = settings.enabled ?
32 | {style: "Danger", label: "Disable XP", emoji: "❕", customId: "toggle_xp" }
33 | : {style: "Success", label: "Enable XP", emoji: "✨", customId: "toggle_xp" }
34 |
35 | let buttons = tools.button([
36 | {style: "Success", label: "Edit Settings", emoji: "🛠", customID: "settings_list"},
37 | toggleButton,
38 | {style: "Link", label: "Edit Online", emoji: "🌎", url: `${tools.WEBSITE}/settings/${int.guild.id}`},
39 | {style: "Secondary", label: "Export Data", emoji: "⏏️", customId: "export_xp"}
40 | ])
41 |
42 | let listButtons = tools.button([
43 | {style: "Primary", label: `Reward Roles (${settings.rewards.length})`, customId: "list_reward_roles"},
44 | {style: "Primary", label: `Role Multipliers (${settings.multipliers.roles.length})`, customId: "list_multipliers~roles"},
45 | {style: "Primary", label: `Channel Multipliers (${settings.multipliers.channels.length})`, customId: "list_multipliers~channels"}
46 | ])
47 |
48 | return int.reply({embeds: [embed], components: [tools.row(buttons)[0], tools.row(listButtons)[0]]})
49 |
50 | }}
--------------------------------------------------------------------------------
/commands/slash/dev_db.js:
--------------------------------------------------------------------------------
1 | const util = require("util")
2 |
3 | module.exports = {
4 | metadata: {
5 | dev: true,
6 | name: "db",
7 | description: "(dev) View or modify database stuff.",
8 | args: [
9 | { type: "string", name: "property", description: "Property name (e.g. settings.enabled)", required: false },
10 | { type: "string", name: "new_value", description: "New value for the property (parsed as JSON)", required: false },
11 | { type: "string", name: "guild_id", description: "Guild ID to use (defaults to current guild)", required: false }
12 | ]
13 | },
14 |
15 | async run(client, int, tools) {
16 |
17 | const propertyName = int.options.get("property")
18 | const newValue = int.options.get("new_value")
19 | const providedGuild = int.options.get("guild_id")
20 |
21 | let guildID = providedGuild?.value || int.guild.id
22 | let db = await client.db.fetch(guildID)
23 | if (!db) return int.reply("No data!")
24 |
25 | let cleanDB = { _id: db._id, settings: db.settings || {}, users: db.users || {} }
26 |
27 | if (!propertyName) {
28 | let uniqueMembers = Object.keys(cleanDB.users).length
29 | if (uniqueMembers > 16) cleanDB.users = `(${uniqueMembers} entries)`
30 | return int.reply(util.inspect(cleanDB))
31 | }
32 |
33 | else if (!newValue) {
34 | Promise.resolve().then(() => eval(`db.${propertyName.value}`)) // lmao
35 | .then(x => int.reply(tools.limitLength(util.inspect(x), 1900)))
36 | .catch(e => int.reply(`**Error:** ${e.message}`))
37 | }
38 |
39 | else {
40 | let val = newValue.value
41 | try { val = JSON.parse(newValue.value) }
42 | catch(e) { newValue.value }
43 |
44 | let confirmMsg = { content: `Click to update **${propertyName.value}** to: [${typeof val}] ${tools.limitLength(JSON.stringify(val), 256)}` }
45 | tools.createConfirmationButtons({
46 | message: confirmMsg, buttons: "Update!", secs: 30, timeoutMessage: "Update cancelled",
47 | onClick: function(confirmed, msg, b) {
48 | if (!confirmed) return msg.reply("Update cancelled")
49 | else {
50 | client.db.update(guildID, { $set: { [propertyName.value]: val } }).exec().then(() => {
51 | msg.reply(`✅ Successfully updated **${propertyName.value}**!`)
52 | }).catch(e => msg.reply("Update failed! " + e.message))
53 | }
54 | }
55 | })
56 | }
57 |
58 |
59 | }}
--------------------------------------------------------------------------------
/commands/slash/dev_deploy.js:
--------------------------------------------------------------------------------
1 | const config = require("../../config.json")
2 | const DiscordBuilders = require("@discordjs/builders")
3 | const Discord = require("discord.js")
4 | const { REST } = require("@discordjs/rest")
5 | const { Routes } = require("discord-api-types/v9")
6 |
7 | function prepareOption(option, arg) {
8 | option.setName(arg.name.toLowerCase())
9 | if (arg.description) option.setDescription(arg.description)
10 | if (arg.required) option.setRequired(true)
11 | return option
12 | }
13 |
14 | function createSlashArg(data, arg) {
15 | switch (arg.type) {
16 | case "subcommand":
17 | return data.addSubcommand(cmd => {
18 | cmd.setName(arg.name)
19 | cmd.setDescription(arg.description)
20 | if (arg.args?.length) arg.args.forEach(a => { createSlashArg(cmd, a) })
21 | return cmd
22 | })
23 | case "string":
24 | return data.addStringOption(option => {
25 | prepareOption(option, arg)
26 | if (arg.choices) option.setChoices(...arg.choices)
27 | return option
28 | })
29 | case "integer": case "number":
30 | return data.addIntegerOption(option => {
31 | prepareOption(option, arg)
32 | if (arg.choices) option.setChoices(...arg.choices)
33 | if (!isNaN(arg.min)) option.setMinValue(arg.min)
34 | if (!isNaN(arg.max)) option.setMaxValue(arg.max)
35 | return option
36 | })
37 | case "float":
38 | return data.addNumberOption(option => {
39 | prepareOption(option, arg)
40 | if (arg.choices) option.setChoices(...arg.choices)
41 | if (!isNaN(arg.min)) option.setMinValue(arg.min)
42 | if (!isNaN(arg.max)) option.setMaxValue(arg.max)
43 | return option
44 | })
45 | case "channel":
46 | return data.addChannelOption(option => {
47 | prepareOption(option, arg)
48 | if (arg.types) option.addChannelTypes(arg.types)
49 | else if (arg.acceptAll) option.addChannelTypes([0, 2, 4, 5, 10, 11, 12, 13, 15, 16]) // lol
50 | else option.addChannelTypes([Discord.ChannelType.GuildText, Discord.ChannelType.GuildAnnouncement])
51 | return option
52 | })
53 | case "bool": return data.addBooleanOption(option => prepareOption(option, arg))
54 | case "file": return data.addAttachmentOption(option => prepareOption(option, arg))
55 | case "user": return data.addUserOption(option => prepareOption(option, arg))
56 | case "role": return data.addRoleOption(option => prepareOption(option, arg))
57 | }
58 | }
59 |
60 |
61 | module.exports = {
62 | metadata: {
63 | dev: true,
64 | name: "deploy",
65 | description: "(dev) Deploy/sync the bot's commands.",
66 | args: [
67 | { type: "bool", name: "global", description: "Publish the public global commands instead of dev ones", required: false },
68 | { type: "string", name: "server_id", description: "Deploy dev commands to a specific server", required: false },
69 | { type: "bool", name: "undeploy", description: "Clears all dev commands from the server (or global if it's set to true)", required: false }
70 | ]
71 | },
72 |
73 | // I made my own slash command builder because discord.js's one is ass
74 | // https://discord.js.org/#/docs/builders/main/class/SlashCommandBuilder
75 | async run(client, int, tools) {
76 |
77 | let isPublic = int && !!int.options.get("global")?.value
78 | let undeploy = int && !!int.options.get("undeploy")?.value
79 | let targetServer = (!int || isPublic) ? null : int.options.get("server_id")?.value
80 |
81 | let interactionList = []
82 | if (!undeploy) client.commands.forEach(cmd => {
83 | let metadata = cmd.metadata
84 | if (isPublic && metadata.dev) return
85 | else if (!isPublic && !metadata.dev) return
86 |
87 | switch (metadata.type) {
88 |
89 | case "user_context": case "message_context": // context menu, user
90 | let ctx = { name: metadata.name, type: metadata.type == "user_context" ? 2 : 3, dm_permission: !!metadata.dm, contexts: [0] }
91 | interactionList.push(ctx);
92 | break;
93 |
94 | case "slash": // slash commands
95 | let data = new DiscordBuilders.SlashCommandBuilder()
96 | data.setName(metadata.name.toLowerCase())
97 | data.setContexts([0])
98 | if (metadata.dev) data.setDefaultMemberPermissions(0)
99 | else if (metadata.permission) data.setDefaultMemberPermissions(Discord.PermissionFlagsBits[metadata.permission])
100 | if (metadata.description) data.setDescription(metadata.description)
101 | if (metadata.args) metadata.args.forEach(arg => {
102 | return createSlashArg(data, arg)
103 | })
104 | interactionList.push(data.toJSON())
105 | break;
106 | }
107 | })
108 |
109 | const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
110 |
111 | if (isPublic) {
112 | const route = Routes.applicationCommands(process.env.DISCORD_ID)
113 | rest.put(route, { body: interactionList })
114 | .then(() => {
115 | if (int) int.reply(`**${!undeploy ? `${interactionList.length} global commands registered!` : "Global commands cleared!"}** (Wait a bit, or refresh with Ctrl+R to see changes)`)
116 | else console.info("Global commands registered!")
117 | client.shard.broadcastEval(cl => { cl.application.commands.fetch(); return }) // cache new slash commands
118 | }).catch(e => console.error(`Error deploying global commands to ${id}: ${e.message}`));
119 | }
120 |
121 | else {
122 | let serverIDs = targetServer ? [targetServer] : (int?.guild) ? [int.guild.id] : config.test_server_ids
123 | if (!serverIDs) return console.warn("Cannot deploy dev commands! No test server IDs provided in config.")
124 |
125 | serverIDs.forEach(id => {
126 | const route = Routes.applicationGuildCommands(process.env.DISCORD_ID, id)
127 | rest.put(route, { body: interactionList })
128 | .then(() => {
129 | let msg = `Dev commands registered to ${id}!`
130 | if (int) int.reply(undeploy ? "Dev commands cleared!" : id == int.guild.id ? "Dev commands registered!" : msg)
131 | else console.info(msg)
132 | }).catch(e => console.error(`Error deploying dev commands to ${id}: ${e.message}`));
133 | })
134 | }
135 |
136 |
137 | }}
--------------------------------------------------------------------------------
/commands/slash/dev_run.js:
--------------------------------------------------------------------------------
1 | const util = require("util")
2 |
3 | module.exports = {
4 | metadata: {
5 | dev: true,
6 | name: "run",
7 | description: "(dev) Evalute JS code, 100% very much safely.",
8 | args: [
9 | { type: "string", name: "code", description: "Some JS code to very safely evaluate", required: true }
10 | ]
11 | },
12 |
13 | async run(client, int, tools) {
14 |
15 | let code = int.options.get("code").value
16 | let db = await client.db.fetch(int.guild.id)
17 |
18 | return Promise.resolve().then(() => {
19 | return eval(code)
20 | })
21 | .then(x => {
22 | if (typeof x !== "string") x = util.inspect(x)
23 | int.reply(x || "** **").catch((e) => {
24 | int.reply("✅").catch(() => {})
25 | });
26 | })
27 | .catch(e => { int.reply(`**Error:** ${e.message}`); console.warn(e) })
28 |
29 | }}
--------------------------------------------------------------------------------
/commands/slash/dev_setactivity.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs")
2 |
3 | module.exports = {
4 | metadata: {
5 | dev: true,
6 | name: "setactivity",
7 | description: "(dev) Change the bot's status",
8 | args: [
9 | { type: "string", name: "type", description: "Activity type", required: true, choices: [
10 | {name: "Custom", value: "Custom"},
11 | {name: "Playing", value: "Playing"},
12 | {name: "Watching", value: "Watching"},
13 | {name: "Listening to", value: "Listening"},
14 | {name: "*Current", value: "current"},
15 | {name: "*Reset", value: "reset"},
16 | {name: "*Clear", value: "clear"},
17 | ]},
18 | { type: "string", name: "name", description: "Custom activity name", required: true },
19 | { type: "string", name: "state", description: "Custom state", required: false },
20 | { type: "string", name: "url", description: "Stream URL", required: false },
21 | { type: "string", name: "status", description: "Online status", required: false, choices: [
22 | {name: "Online", value: "online"},
23 | {name: "Idle", value: "idle"},
24 | {name: "Do Not Disturb", value: "dnd"},
25 | {name: "Offline", value: "offline"},
26 | ]},
27 | ]
28 | },
29 |
30 | async run(client, int, tools) {
31 |
32 | const statusInfo = require("../../json/auto/status.json") // placed inside run to guarantee it exists
33 |
34 | let type = int.options.get("type")?.value
35 | let name = int.options.get("name")?.value
36 | let state = int.options.get("state")?.value
37 | let status = int.options.get("status")?.value || "online"
38 | let url = int.options.get("url")?.value || null
39 |
40 | if (!state && type == "Custom") state = name
41 |
42 | if (url) type = "Streaming"
43 |
44 | else if (type == "current") {
45 | type = statusInfo.type
46 | name = statusInfo.name
47 | status = statusInfo.status
48 | }
49 |
50 | else if (type == "reset") {
51 | type = statusInfo.default.type
52 | name = statusInfo.default.name
53 | status = "online"
54 | }
55 |
56 | else if (type == "clear") {
57 | type = ""
58 | name = ""
59 | }
60 |
61 | int.reply("✅ **Status updated!**")
62 |
63 | statusInfo.name = name
64 | statusInfo.state = state || ""
65 | statusInfo.type = type
66 | statusInfo.url = url
67 | statusInfo.status = status
68 | client.statusData = statusInfo
69 | fs.writeFileSync('./json/auto/status.json', JSON.stringify(statusInfo, null, 2))
70 |
71 | client.shard.broadcastEval(async (cl, xd) => {
72 | cl.statusData = xd
73 | cl.updateStatus()
74 | }, { context: statusInfo })
75 |
76 | }}
--------------------------------------------------------------------------------
/commands/slash/dev_setversion.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs")
2 |
3 | module.exports = {
4 | metadata: {
5 | dev: true,
6 | name: "setversion",
7 | description: "(dev) Change the bot's version number",
8 | args: [
9 | { type: "string", name: "version", description: "Version number", required: true },
10 | { type: "bool", name: "change_timestamp", description: "Use current time as update timestamp", required: true },
11 | { type: "integer", name: "custom_timestamp", description: "Custom update timestamp", required: false },
12 | ]
13 | },
14 |
15 | async run(client, int, tools) {
16 |
17 | let versionNumber = int.options.get("version")?.value
18 | let updateTimestamp = !!int.options.get("change_timestamp")?.value ? Date.now() : (int.options.get("custom_timestamp")?.value || client.version.updated)
19 |
20 | client.shard.broadcastEval((cl, xd) => {
21 | cl.version = { version: xd.versionNumber, updated: xd.updateTimestamp }
22 | }, { context: { versionNumber, updateTimestamp } })
23 |
24 | fs.writeFileSync('./json/auto/version.json', JSON.stringify({ version: versionNumber, updated: updateTimestamp }, null, 2))
25 |
26 | int.reply(`Bot updated to **v${versionNumber}** ( / ${updateTimestamp})`)
27 |
28 | }}
--------------------------------------------------------------------------------
/commands/slash/multiplier.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | metadata: {
3 | permission: "ManageGuild",
4 | name: "multiplier",
5 | description: "Add or remove an XP multiplier. (requires manage server permission)",
6 | args: [
7 | { type: "subcommand", name: "role", description: "Add or remove a role multiplier", args: [
8 | { type: "role", name: "role_name", description: "The role to add a multiplier for", required: true },
9 | { type: "float", name: "multiplier", description: "Multiply XP gain by this amount (0.5, 2, etc), or 0 to disable XP gain", min: 0, max: 100, required: true },
10 | { type: "bool", name: "remove", description: "Removes this multiplier, if it exists" }
11 | ]},
12 |
13 | { type: "subcommand", name: "channel", description: "Add or remove a channel multiplier", args: [
14 | { type: "channel", name: "channel_name", description: "The channel or category to add a multiplier for", required: true, acceptAll: true },
15 | { type: "float", name: "multiplier", description: "Multiply XP gain by this amount (0.5, 2, etc), or 0 to disable XP gain", min: 0, max: 100, required: true },
16 | { type: "bool", name: "remove", description: "Removes this multiplier, if it exists" }
17 | ]}
18 | ]
19 | },
20 |
21 | async run(client, int, tools) {
22 |
23 | let db = await tools.fetchSettings()
24 | if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
25 |
26 | let type = int.options.getSubcommand(false)
27 |
28 | let boostVal = int.options.get("multiplier")?.value ?? 1
29 |
30 | let role = int.options.getRole("role_name")
31 | let channel = int.options.getChannel("channel_name")
32 | let boost = tools.clamp(+boostVal.toFixed(2), 0, 100)
33 | let remove = !!int.options.get("remove")?.value
34 |
35 | if (!channel && !role) return
36 | let target = (channel || role)
37 | let tag = role ? `<@&${role.id}>` : `<#${channel.id}>`
38 |
39 | let typeIndex = role ? "roles" : "channels"
40 | let mults = db.settings.multipliers[typeIndex]
41 | let existingIndex = mults.findIndex(x => x.id == target.id)
42 | let foundExisting = (existingIndex >= 0) ? mults[existingIndex] : null
43 |
44 | let newList = db.settings.multipliers
45 | if (foundExisting) db.settings.multipliers[typeIndex].splice(existingIndex, 1) // remove by default
46 |
47 | function finish(msg) {
48 | let viewMultipliers = tools.row([
49 | tools.button({style: role ? "Primary" : "Secondary", label: `Role multipliers (${newList.roles.length})`, customId: "list_multipliers~roles"}),
50 | tools.button({style: role ? "Secondary" : "Primary", label: `Channel multipliers (${newList.channels.length})`, customId: "list_multipliers~channels"})
51 | ])
52 |
53 | client.db.update(int.guild.id, { $set: { [`settings.multipliers.${typeIndex}`]: newList[typeIndex], 'info.lastUpdate': Date.now() }}).then(() => {
54 | return int.reply({ content: msg, components: viewMultipliers })
55 | })
56 | }
57 |
58 | // deleting a multiplier
59 | if (remove) {
60 | if (!foundExisting) return tools.warn(`This ${type} never had a multiplier to begin with!`)
61 | return finish(`❌ **Successfully deleted ${foundExisting.boost}x multiplier for ${tag}.**`)
62 | }
63 |
64 | // set up multiplier data
65 | let boostData = { id: target.id, boost }
66 | newList[typeIndex].push(boostData)
67 | let boostStr = boost == 0 ? "no XP" : `${boost}x XP`
68 |
69 | // if multiplier already exists, replace it
70 | if (foundExisting) {
71 | if (foundExisting.boost == boost) return tools.warn(`This ${type} already gives a ${boost}x multiplier!`)
72 | return finish(`📝 **${tag} now gives ${boostStr}!** (previously ${foundExisting.boost}x)`)
73 | }
74 |
75 | return finish(`✅ **${tag} now gives ${boostStr}!**`)
76 |
77 | }}
--------------------------------------------------------------------------------
/commands/slash/rank.js:
--------------------------------------------------------------------------------
1 | const multiplierModes = require("../../json/multiplier_modes.json")
2 |
3 | module.exports = {
4 | metadata: {
5 | name: "rank",
6 | description: "View your current XP, level, and cooldown.",
7 | args: [
8 | { type: "user", name: "member", description: "Which member to view", required: false },
9 | { type: "bool", name: "hidden", description: "Hides the reply so only you can see it", required: false }
10 | ]
11 | },
12 |
13 | async run(client, int, tools) {
14 |
15 | // fetch member
16 | let member = int.member
17 | let foundUser = int.options.get("user") || int.options.get("member") // option is "user" if from context menu
18 | if (foundUser) member = foundUser.member
19 | if (!member) return tools.warn("That member couldn't be found!")
20 |
21 | // fetch server xp settings
22 | let db = await tools.fetchSettings(member.id)
23 | if (!db) return tools.warn("*noData")
24 | else if (!db.settings.enabled) return tools.warn("*xpDisabled")
25 |
26 | let currentXP = db.users[member.id]
27 |
28 | if (db.settings.rankCard.disabled) return tools.warn("Rank cards are disabled in this server!")
29 |
30 | // if user has no xp, stop here
31 | if (!currentXP || !currentXP.xp) return tools.noXPYet(foundUser ? foundUser.user : int.user)
32 |
33 | let xp = currentXP.xp
34 |
35 | let levelData = tools.getLevel(xp, db.settings, true) // get user's level
36 | let maxLevel = levelData.level >= db.settings.maxLevel // check if level is maxxed
37 |
38 | let remaining = levelData.xpRequired - xp
39 | let levelPercent = maxLevel ? 100 : (xp - levelData.previousLevel) / (levelData.xpRequired - levelData.previousLevel) * 100
40 |
41 | let multiplierData = tools.getMultiplier(member, db.settings)
42 | let multiplier = multiplierData.multiplier
43 |
44 | let barSize = 33 // how many characters the xp bar is
45 | let barRepeat = Math.round(levelPercent / (100 / barSize)) // .round() so bar can sometimes display as completely full and completely empty
46 | let progressBar = `${"▓".repeat(barRepeat)}${"░".repeat(barSize - barRepeat)} (${!maxLevel ? Number(levelPercent.toFixed(2)) + "%" : "MAX"})`
47 |
48 | let estimatedMin = Math.ceil(remaining / (db.settings.gain.min * (multiplier || multiplierData.role)))
49 | let estimatedMax = Math.ceil(remaining / (db.settings.gain.max * (multiplier || multiplierData.role)))
50 |
51 | // estimated number of messages to level up
52 | let estimatedRange = (estimatedMax == estimatedMin) ? `${tools.commafy(estimatedMax)} ${tools.extraS("message", estimatedMax)}` : `${tools.commafy(estimatedMax)}-${tools.commafy(estimatedMin)} messages`
53 |
54 | // xp required to level up
55 | let nextLevelXP = (db.settings.rankCard.relativeLevel ? `${tools.commafy(xp - levelData.previousLevel)}/${tools.commafy(levelData.xpRequired - levelData.previousLevel)}` : `${tools.commafy(levelData.xpRequired)}`) + ` (${tools.commafy(remaining)} more)`
56 |
57 | let cardCol = db.settings.rankCard.embedColor
58 | if (cardCol == -1) cardCol = null
59 |
60 | let memberAvatar = member.displayAvatarURL()
61 | let memberColor = cardCol || member.displayColor || await member.user.fetch().then(x => x.accentColor)
62 |
63 | let embed = tools.createEmbed({
64 | author: { name: member.user.displayName, iconURL: memberAvatar },
65 | color: memberColor,
66 | footer: maxLevel ? progressBar : ((estimatedMin == Infinity || estimatedMin < 0) ? "You are unable to gain XP!" : `${progressBar}\n${estimatedRange} to go!`),
67 | fields: [
68 | { name: "✨ XP", value: `${tools.commafy(xp)} (lv. ${levelData.level})`, inline: true },
69 | { name: "⏩ Next level", value: !maxLevel ? nextLevelXP : "Max level! Woah!", inline: true },
70 | ]
71 | })
72 |
73 | if (!db.settings.rankCard.hideCooldown) {
74 | let foundCooldown = currentXP.cooldown || 0
75 | let cooldown = foundCooldown > Date.now() ? tools.timestamp(foundCooldown - Date.now()) : "None!"
76 | embed.addFields([{ name: "🕓 Cooldown", value: cooldown, inline: true }])
77 | }
78 |
79 | let hideMult = db.settings.hideMultipliers
80 |
81 | let multRoles = multiplierData.roleList
82 | let multiplierInfo = []
83 | if ((!hideMult || multiplierData.role == 0) && multRoles.length) {
84 | let xpStr = multiplierData.role > 0 ? `${multiplierData.role}x XP` : "Cannot gain XP!"
85 | let roleMultiplierStr = multRoles.length == 1 ? `${int.guild.id != multRoles[0].id ? `<@&${multRoles[0].id}>` : "Everyone"} - ${xpStr}` : `**${multRoles.length} roles** - ${xpStr}`
86 | multiplierInfo.push(roleMultiplierStr)
87 | }
88 |
89 | let multChannels = multiplierData.channelList
90 | if ((!hideMult || multiplierData.channel == 0) && multChannels.length && multiplierData.role > 0 && (multiplierData.role != 1 || multiplierData.channel != 1)) {
91 | let chXPStr = multChannels[0].boost > 0 ? `${multiplierData.channel}x XP` : "Cannot gain XP!"
92 | let chMultiplierStr = `<#${multChannels[0].id}> - ${chXPStr}` // leaving room for multiple channels, via categories or vcs or something
93 | multiplierInfo.push(chMultiplierStr)
94 | if (multRoles.length) multiplierInfo.push(`**Total multiplier: ${multiplier}x XP** (${multiplierModes.channelStacking[multiplierData.channelStacking].toLowerCase()})`)
95 | }
96 |
97 | if (multiplierInfo.length) embed.addFields([{ name: "🌟 Multiplier", value: multiplierInfo.join("\n") }])
98 |
99 | else if (!db.settings.rewardSyncing.noManual && !db.settings.rewardSyncing.noWarning) {
100 | let syncCheck = tools.checkLevelRoles(int.guild.roles.cache, member.roles.cache, levelData.level, db.settings.rewards)
101 | if (syncCheck.incorrect.length || syncCheck.missing.length) embed.addFields([{ name: "⚠ Note", value: `Your level roles are not properly synced! Type ${tools.commandTag("sync")} to fix this.` }])
102 | }
103 |
104 | let isHidden = db.settings.rankCard.ephemeral || !!int.options.get("hidden")?.value
105 | return int.reply({embeds: [embed], ephemeral: isHidden})
106 |
107 | }}
--------------------------------------------------------------------------------
/commands/slash/rewardrole.js:
--------------------------------------------------------------------------------
1 | const Discord = require("discord.js")
2 |
3 | module.exports = {
4 | metadata: {
5 | permission: "ManageGuild",
6 | name: "rewardrole",
7 | description: "Add or remove a reward role. (requires manage server permission)",
8 | args: [
9 | { type: "role", name: "role_name", description: "The role to add or remove", required: true },
10 | { type: "integer", name: "level", description: "The level to grant the role at, or 0 to remove", min: 0, max: 1000, required: true },
11 | { type: "bool", name: "keep", description: "Keep this role even when a higher one is reached" },
12 | { type: "bool", name: "dont_sync", description: "Advanced: Ignores this role when syncing roles" }
13 | ]
14 | },
15 |
16 | async run(client, int, tools) {
17 |
18 | let db = await tools.fetchSettings()
19 | if (!tools.canManageServer(int.member, db.settings.manualPerms)) return tools.warn("*notMod")
20 |
21 | let role = int.options.getRole("role_name")
22 | let level = tools.clamp(Math.round(int.options.get("level")?.value), 0, 1000)
23 |
24 | let isKeep = !!int.options.get("keep")?.value
25 | let isDontSync = !!int.options.get("dont_sync")?.value
26 |
27 | let existingIndex = db.settings.rewards.findIndex(x => x.id == role.id)
28 | let foundExisting = (existingIndex >= 0) ? db.settings.rewards[existingIndex] : null
29 |
30 | let newRoles = db.settings.rewards
31 | if (foundExisting) newRoles.splice(existingIndex, 1) // remove by default
32 |
33 | function finish(msg) {
34 | let viewRewardRoles = tools.row(tools.button({style: "Primary", label: `View all rewards (${newRoles.length})`, customId: "list_reward_roles"}))
35 |
36 | client.db.update(int.guild.id, { $set: { 'settings.rewards': newRoles, 'info.lastUpdate': Date.now() }}).then(() => {
37 | return int.reply({ content: msg, components: viewRewardRoles })
38 | })
39 | }
40 |
41 | // deleting a reward role
42 | if (level == 0) {
43 | if (!foundExisting) return tools.warn("Reward roles can't be granted at level 0! Use this to delete existing reward roles.")
44 | return finish(`❌ **Successfully deleted reward role <@&${role.id}> for level ${foundExisting.level}.**`, newRoles)
45 | }
46 |
47 | // no manage roles perm
48 | if (!int.guild.members.me.permissions.has(Discord.PermissionFlagsBits.ManageRoles)) return tools.warn("*cantManageRoles")
49 |
50 | // can't grant role
51 | if (!role.editable) return tools.warn(`I don't have permission to grant <@&${role.id}>!`)
52 |
53 | // set up new role data
54 | let roleData = { id: role.id, level }
55 | let extraStrings = []
56 | if (isKeep) { roleData.keep = true; extraStrings.push("always kept") }
57 | if (isDontSync) { roleData.noSync = true; extraStrings.push("ignores sync") }
58 |
59 | newRoles.push(roleData)
60 | let extraStr = (extraStrings.length < 1) ? "" : ` (${extraStrings.join(", ")})`
61 |
62 | // if reward already exists, replace existing role
63 | if (foundExisting) {
64 | if (foundExisting.level == level) return tools.warn(`This role is already granted at level ${level}!`)
65 | return finish(`📝 **<@&${role.id}> will now be granted at level ${level}!** (previously ${foundExisting.level})${extraStr}`)
66 | }
67 |
68 | // otherwise, just add the role
69 | return finish(`✅ **<@&${role.id}> will now be granted at level ${level}!**${extraStr}`)
70 |
71 | }}
--------------------------------------------------------------------------------
/commands/slash/sync.js:
--------------------------------------------------------------------------------
1 | const Discord = require('discord.js')
2 | module.exports = {
3 | metadata: {
4 | name: "sync",
5 | description: "Sync your level roles by adding missing ones and removing incorrect ones.",
6 | args: [
7 | { type: "user", name: "member", description: "Which member to sync (requires manage server permission)", required: false }
8 | ]
9 | },
10 |
11 | async run(client, int, tools) {
12 |
13 | let foundUser = int.options.get("member")
14 | let member = foundUser ? foundUser.member : int.member
15 | if (!int.guild.members.me.permissions.has(Discord.PermissionFlagsBits.ManageRoles)) return tools.warn("*cantManageRoles")
16 |
17 | let db = await tools.fetchSettings(member.id)
18 | if (!db) return tools.warn("*noData")
19 | else if (!db.settings.enabled) return tools.warn("*xpDisabled")
20 |
21 | let isMod = db.settings.manualPerms ? tools.canManageRoles() : tools.canManageServer()
22 | if (member.id != int.user.id && !isMod) return tools.warn("You don't have permission to sync someone else's roles!")
23 |
24 | else if (db.settings.noManual && !isMod) return tools.warn("You don't have permission to sync your level roles!")
25 | else if (!db.settings.rewards.length) return tools.warn("This server doesn't have any reward roles!")
26 |
27 | let currentXP = db.users[member.id]
28 | if (!currentXP || !currentXP.xp) return tools.noXPYet(member.user)
29 |
30 | let xp = currentXP.xp
31 | let level = tools.getLevel(xp, db.settings)
32 |
33 | let currentRoles = member.roles.cache
34 | let roleCheck = tools.checkLevelRoles(int.guild.roles.cache, currentRoles, level, db.settings.rewards)
35 | if (!roleCheck.incorrect.length && !roleCheck.missing.length) return int.reply("✅ Your level roles are already properly synced!")
36 |
37 | tools.syncLevelRoles(member, roleCheck).then(() => {
38 | let replyStr = ["🔄 **Level roles successfully synced!**"]
39 | if (roleCheck.missing.length) replyStr.push(`Added: ${roleCheck.missing.map(x => `<@&${x.id}>`).join(" ")}`)
40 | if (roleCheck.incorrect.length) replyStr.push(`Removed: ${roleCheck.incorrect.map(x => `<@&${x.id}>`).join(" ")}`)
41 | return int.reply(replyStr.join("\n"))
42 | }).catch(e => int.reply(`Error syncing roles! ${e.message}`))
43 |
44 | }}
--------------------------------------------------------------------------------
/commands/slash/top.js:
--------------------------------------------------------------------------------
1 | const PageEmbed = require("../../classes/PageEmbed.js")
2 |
3 | module.exports = {
4 | metadata: {
5 | name: "top",
6 | description: "View the server's XP leaderboard.",
7 | args: [
8 | { type: "integer", name: "page", description: "Which page to view (negative to start from last page)", required: false },
9 | { type: "user", name: "member", description: "Finds a certain member's position on the leaderboard (overrides page)", required: false },
10 | { type: "bool", name: "hidden", description: "Hides the reply so only you can see it", required: false }
11 | ]
12 | },
13 |
14 | async run(client, int, tools) {
15 |
16 | let lbLink = `${tools.WEBSITE}/leaderboard/${int.guild.id}`
17 |
18 | let db = await tools.fetchAll()
19 | if (!db || !db.users || !Object.keys(db.users).length) return tools.warn(`Nobody in this server is ranked yet!`);
20 | else if (!db.settings.enabled) return tools.warn("*xpDisabled")
21 | else if (db.settings.leaderboard.disabled) return tools.warn("The leaderboard is disabled in this server!" + (tools.canManageServer(int.member) ? `\nAs a moderator, you can still privately view the leaderboard here: ${lbLink}` : ""))
22 |
23 | let pageNumber = int.options.get("page")?.value || 1
24 | let pageSize = 10
25 |
26 | let minLeaderboardXP = db.settings.leaderboard.minLevel > 1 ? tools.xpForLevel(db.settings.leaderboard.minLevel, db.settings) : 0
27 | let rankings = tools.xpObjToArray(db.users)
28 | rankings = rankings.filter(x => x.xp > minLeaderboardXP && !x.hidden).sort(function(a, b) {return b.xp - a.xp})
29 |
30 | if (db.settings.leaderboard.maxEntries > 0) rankings = rankings.slice(0, db.settings.leaderboard.maxEntries)
31 |
32 | if (!rankings.length) return tools.warn("Nobody in this server is on the leaderboard yet!")
33 |
34 | let highlight = null
35 | let userSearch = int.options.get("user") || int.options.get("member") // option is "user" if from context menu
36 | if (userSearch) {
37 | let foundRanking = rankings.findIndex(x => x.id == userSearch.user.id)
38 | if (isNaN(foundRanking) || foundRanking < 0) return tools.warn(int.user.id == userSearch.user.id ? "You aren't on the leaderboard!" : "This member isn't on the leaderboard!")
39 | else pageNumber = Math.floor(foundRanking / pageSize) + 1
40 | highlight = userSearch.user.id
41 | }
42 |
43 | let listCol = db.settings.leaderboard.embedColor
44 | if (listCol == -1) listCol = null
45 |
46 | let embed = tools.createEmbed({
47 | color: listCol || tools.COLOR,
48 | author: {name: 'Leaderboard for ' + int.guild.name, iconURL: int.guild.iconURL()}
49 | })
50 |
51 | let isHidden = db.settings.leaderboard.ephemeral || !!int.options.get("hidden")?.value
52 |
53 | let xpEmbed = new PageEmbed(embed, rankings, {
54 | page: pageNumber, size: pageSize, owner: int.user.id, ephemeral: isHidden,
55 | mapFunction: (x, y, p) => `**${p})** ${x.id == highlight ? "**" : ""}Lv. ${tools.getLevel(x.xp, db.settings)} - <@${x.id}> (${tools.commafy(x.xp)} XP)${x.id == highlight ? "**" : ""}`,
56 | extraButtons: [ tools.button({style: "Link", label: "Online Leaderboard", url: lbLink}) ]
57 | })
58 | if (!xpEmbed.data.length) return tools.warn("There are no members on this page!")
59 |
60 | xpEmbed.post(int)
61 |
62 | }}
--------------------------------------------------------------------------------
/commands/user_context/view_on_leaderboard.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | metadata: {
3 | name: "View on leaderboard",
4 | slashEquivalent: "top"
5 | }
6 | }
--------------------------------------------------------------------------------
/commands/user_context/view_xp.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | metadata: {
3 | name: "Check XP",
4 | slashEquivalent: "rank"
5 | }
6 | }
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "test_server_ids": ["12345678"],
3 |
4 | "developer_ids": ["12345678"],
5 |
6 | "lockBotToDevOnly": false,
7 |
8 | "enableWebServer": true,
9 | "serverPort": 6880,
10 | "siteURL": "http://localhost:6880",
11 |
12 | "changelogURL": "",
13 | "supportURL": ""
14 | }
--------------------------------------------------------------------------------
/database_schema.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose")
2 |
3 | // Most of the properties below are used for the web server, they are not built into the mongo schema
4 |
5 | // type: the value's data type (bool, int, float, string, collection)
6 | // default: the default value
7 | // min+max: for numbers, forces between those values
8 | // precision: for floats, how many decimal places
9 | // maxlength: for strings, max length
10 | // accept: for strings, accepted values. discord:channel and discord:role accept any of those kind of ids
11 |
12 | const settings = {
13 | enabled: { type: "bool", default: false },
14 |
15 | gain: {
16 | min: { type: "int", default: 50, min: 0, max: 5000 },
17 | max: { type: "int", default: 100, min: 0, max: 5000 },
18 | time: { type: "float", precision: 4, default: 60, min: 0, max: 31536000 },
19 | },
20 |
21 | curve: {
22 | 3: { type: "float", precision: 10, default: 1, min: 0, max: 100 },
23 | 2: { type: "float", precision: 10, default: 50, min: 0, max: 10000 },
24 | 1: { type: "float", precision: 10, default: 100, min: 0, max: 100000 },
25 | },
26 | rounding: { type: "int", default: 100, min: 1, max: 1000 },
27 | maxLevel: { type: "int", default: 1000, min: 1, max: 1000 },
28 |
29 | levelUp: {
30 | enabled: { type: "bool", default: false },
31 | embed: { type: "bool", default: false },
32 | rewardRolesOnly: { type: "bool", default: false },
33 | message: { type: "string", maxlength: 6000, default: "" },
34 | channel: { type: "string", default: "current", accept: ["dm", "current", "discord:channel"] },
35 | multiple: { type: "int", default: 1, min: 1, max: 1000 },
36 | multipleUntil: { type: "int", default: 20, min: 0, max: 1000 }
37 | },
38 |
39 | multipliers: {
40 | roles: { type: "collection", values: {
41 | id: { type: "string", accept: ["discord:role"] },
42 | boost: { type: "float", min: 0, max: 100, precision: 4 },
43 | }},
44 | rolePriority: { type: "string", default: "largest", accept: ["largest", "smallest", "highest", "add", "combine"] },
45 | channels: { type: "collection", values: {
46 | id: { type: "string", accept: ["discord:channel"] },
47 | boost: { type: "float", min: 0, max: 100, precision: 4 },
48 | }},
49 | channelStacking: { type: "string", default: "multiply", accept: ["multiply", "add", "largest", "channel", "role"] }
50 | },
51 |
52 | rewards: { type: "collection", values: {
53 | id: { type: "string", accept: ["discord:role"] },
54 | level: { type: "int", min: 1, max: 1000 },
55 | keep: { type: "bool" },
56 | noSync: { type: "bool" },
57 | }},
58 |
59 | rewardSyncing: {
60 | sync: { type: "string", default: "level", accept: ["level", "xp", "never"] },
61 | noManual: { type: "bool", default: false },
62 | noWarning: { type: "bool", default: false }
63 | },
64 |
65 | leaderboard: {
66 | disabled: { type: "bool", default: false },
67 | private: { type: "bool", default: false },
68 | hideRoles: { type: "bool", default: false },
69 | maxEntries: { type: "int", default: 0, min: 0, max: 1000000 },
70 | minLevel: { type: "int", default: 0, min: 0, max: 1000 },
71 | ephemeral: { type: "bool", default: false },
72 | embedColor: { type: "int", default: -1, min: -1, max: 0xffffff }
73 | },
74 |
75 | rankCard: {
76 | disabled: { type: "bool", default: false },
77 | relativeLevel: { type: "bool", default: false },
78 | hideCooldown: { type: "bool", default: false },
79 | ephemeral: { type: "bool", default: false },
80 | embedColor: { type: "int", default: -1, min: -1, max: 0xffffff }
81 | },
82 |
83 | hideMultipliers: { type: "bool", default: false },
84 | manualPerms: { type: "bool", default: false }
85 | }
86 |
87 | const settingsArray = []
88 | const settingsObj = {}
89 | const settingsIDs = {}
90 |
91 | const schemaTypes = {
92 | "bool": Boolean,
93 | "int": Number,
94 | "float": Number,
95 | "string": String,
96 | "collection": [Object]
97 | }
98 |
99 | function schemaVal(val) {
100 | let result = { type: schemaTypes[val.type] }
101 | if (val.type == "collection") result.default = []
102 | else if (val.default !== undefined) result.default = val.default
103 | return result
104 | }
105 |
106 | function addToSettingsArray(value, name) {
107 | let obj = value
108 | obj.db = name
109 | settingsArray.push(obj)
110 | settingsIDs[name] = obj
111 | }
112 |
113 | // for settings, create the actual mongo schema
114 | Object.entries(settings).forEach(x => {
115 | let [key, val] = x
116 | if (!val.type) {
117 | let collection = {}
118 | Object.entries(val).forEach(z => {
119 | let [innerKey, innerVal] = z
120 | collection[innerKey] = schemaVal(innerVal)
121 | addToSettingsArray(innerVal, `${key}.${innerKey}`)
122 | })
123 | settingsObj[key] = collection
124 | }
125 | else {
126 | addToSettingsArray(val, key)
127 | settingsObj[key] = schemaVal(val)
128 | }
129 | })
130 |
131 | const schema = {
132 | _id: String,
133 | users: { type: Object }, // xp, cooldown, hidden. should be validated but it just slows things down
134 | settings: settingsObj,
135 | info: {
136 | lastUpdate: { type: Number, default: 0 },
137 | }
138 | }
139 |
140 | const finalSchema = new mongoose.Schema(schema)
141 |
142 | module.exports = {
143 | settings, settingsArray, settingsIDs, schema: finalSchema
144 | }
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const Discord = require("discord.js")
2 | const fs = require("fs")
3 |
4 | const config = require("./config.json")
5 |
6 | const Tools = require("./classes/Tools.js")
7 | const Model = require("./classes/DatabaseModel.js")
8 |
9 | // automatic files: these handle discord status and version number, manage them with the dev commands
10 | const autoPath = "./json/auto/"
11 | if (!fs.existsSync(autoPath)) fs.mkdirSync(autoPath)
12 | if (!fs.existsSync(autoPath + "status.json")) fs.copyFileSync("./json/default_status.json", autoPath + "status.json")
13 | if (!fs.existsSync(autoPath + "version.json")) fs.writeFileSync(autoPath + "version.json", JSON.stringify({ version: "1.0.0", updated: Date.now() }, null, 2))
14 |
15 | const rawStatus = require("./json/auto/status.json")
16 | const version = require("./json/auto/version.json")
17 |
18 | const startTime = Date.now()
19 |
20 | // create client
21 | const client = new Discord.Client({
22 | allowedMentions: { parse: ["users"] },
23 | makeCache: Discord.Options.cacheWithLimits({ MessageManager: 0 }),
24 | intents: ['Guilds', 'GuildMessages', 'DirectMessages', 'GuildVoiceStates'].map(i => Discord.GatewayIntentBits[i]),
25 | partials: ['Channel'].map(p => Discord.Partials[p]),
26 | failIfNotExists: false
27 | })
28 |
29 | if (!client.shard) {
30 | console.error("No sharding info found!\nMake sure you start the bot from polaris.js, not index.js")
31 | return process.exit()
32 | }
33 |
34 | client.shard.id = client.shard.ids[0]
35 |
36 | client.globalTools = new Tools(client);
37 |
38 | // connect to db
39 | client.db = new Model("servers", require("./database_schema.js").schema)
40 |
41 | // command files
42 | const dir = "./commands/"
43 | client.commands = new Discord.Collection()
44 | fs.readdirSync(dir).forEach(type => {
45 | fs.readdirSync(dir + type).filter(x => x.endsWith(".js")).forEach(file => {
46 | let command = require(dir + type + "/" + file)
47 | if (!command.metadata) command.metadata = { name: file.split(".js")[0] }
48 | command.metadata.type = type
49 | client.commands.set(command.metadata.name, command)
50 | })
51 | })
52 |
53 | client.statusData = rawStatus
54 | client.updateStatus = function() {
55 | let status = client.statusData
56 | client.user.setPresence({ activities: status.type ? [{ name: status.name, state: status.state || undefined, type: Discord.ActivityType[status.type], url: status.url }] : [], status: status.status })
57 | }
58 |
59 | // when online
60 | client.on("ready", () => {
61 | if (client.shard.id == client.shard.count - 1) console.log(`Bot online! (${+process.uptime().toFixed(2)} secs)`)
62 | client.startupTime = Date.now() - startTime
63 | client.version = version
64 |
65 | client.application.commands.fetch() // cache slash commands
66 | .then(cmds => {
67 | if (cmds.size < 1) { // no commands!! deploy to test server
68 | console.info("!!! No global commands found, deploying dev commands to test server (Use /deploy global=true to deploy global commands)")
69 | client.commands.get("deploy").run(client, null, client.globalTools)
70 | }
71 | })
72 |
73 | client.updateStatus()
74 | setInterval(client.updateStatus, 15 * 60000);
75 |
76 | // run the web server
77 | if (client.shard.id == 0 && config.enableWebServer) require("./web_app.js")(client)
78 | })
79 |
80 | // on message
81 | client.on("messageCreate", async message => {
82 | if (message.system || message.author.bot) return
83 | else if (!message.guild || !message.member) return // dm stuff
84 | else client.commands.get("message").run(client, message, client.globalTools)
85 | })
86 |
87 | // on interaction
88 | client.on("interactionCreate", async int => {
89 |
90 | if (!int.guild) return int.reply("You can't use commands in DMs!")
91 |
92 | // for setting changes
93 | if (int.isStringSelectMenu()) {
94 | if (int.customId.startsWith("configmenu_")) {
95 | if (int.customId.split("_")[1] != int.user.id) return int.deferUpdate()
96 | let configData = int.values[0].split("_").slice(1)
97 | let configCmd = (configData[0] == "dir" ? "button:settings_list" : "button:settings_view")
98 | client.commands.get(configCmd).run(client, int, new Tools(client, int), configData)
99 | }
100 | return;
101 | }
102 |
103 | // also for setting changes
104 | else if (int.isModalSubmit()) {
105 | if (int.customId.startsWith("configmodal")) {
106 | let modalData = int.customId.split("~")
107 | if (modalData[2] != int.user.id) return int.deferUpdate()
108 | client.commands.get("button:settings_edit").run(client, int, new Tools(client, int), modalData[1])
109 | }
110 | return;
111 | }
112 |
113 | // general commands and buttons
114 | let foundCommand = client.commands.get(int.isButton() ? `button:${int.customId.split("~")[0]}` : int.commandName)
115 | if (!foundCommand) return
116 | else if (foundCommand.metadata.slashEquivalent) foundCommand = client.commands.get(foundCommand.metadata.slashEquivalent)
117 |
118 | let tools = new Tools(client, int)
119 |
120 | // dev perm check
121 | if (foundCommand.metadata.dev && !tools.isDev()) return tools.warn("Only developers can use this!")
122 | else if (config.lockBotToDevOnly && !tools.isDev()) return tools.warn("Only developers can use this bot!")
123 |
124 | try { await foundCommand.run(client, int, tools) }
125 | catch(e) { console.error(e); int.reply({ content: "**Error!** " + e.message, ephemeral: true }) }
126 | })
127 |
128 | client.on('error', e => console.warn(e))
129 | client.on('warn', e => console.warn(e))
130 |
131 | process.on('uncaughtException', e => console.warn(e))
132 | process.on('unhandledRejection', (e, p) => console.warn(e))
133 |
134 | client.login(process.env.DISCORD_TOKEN)
--------------------------------------------------------------------------------
/json/curve_presets.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | {
4 | "name": "Polaris (Default)",
5 | "desc": "The default and recommended curve. Decently balanced.",
6 | "curve": { "3": 1, "2": 50, "1": 100 },
7 | "round": 100,
8 | "bestRange": [50, 100]
9 | },
10 |
11 | {
12 | "name": "RoboTop",
13 | "desc": "The curve used by RoboTop, the predecessor to Polaris. Starts fine, but gets absurdly difficult.",
14 | "curve": { "3": 5, "2": 100, "1": 150 },
15 | "round": 100,
16 | "bestRange": [50, 100]
17 | },
18 |
19 | {
20 | "name": "Generous",
21 | "desc": "Too hard to level up? This curve is a little more on the friendly side.",
22 | "curve": { "3": 0.5, "2": 20, "1": 50 },
23 | "round": 50,
24 | "bestRange": [50, 100]
25 | },
26 |
27 | {
28 | "name": "Linear",
29 | "desc": "Completely linear. Set XP per message to 100-100 to level up exactly every 100 messages.",
30 | "curve": { "3": 0, "2": 0, "1": 10000 },
31 | "round": 1,
32 | "bestRange": [100, 100]
33 | },
34 |
35 | {
36 | "name": "Exponential",
37 | "desc": "Not too easy, not too difficult. And it scales really smoothly.",
38 | "curve": { "3": 0, "2": 250, "1": 0 },
39 | "round": 1,
40 | "bestRange": [50, 100]
41 | },
42 |
43 | {
44 | "name": "Minimalist",
45 | "desc": "1-10 XP per message. Why not?.",
46 | "curve": { "3": 1, "2": 2, "1": 3 },
47 | "round": 10,
48 | "bestRange": [1, 10]
49 | },
50 |
51 | {
52 | "name": "Ultra Minimalist",
53 | "desc": "1 XP per message, level up every 100 messages. Alternatively, I also recommend a 12 hour cooldown where you level up every 10 messages!",
54 | "curve": { "3": 0, "2": 0, "1": 100 },
55 | "round": 1,
56 | "bestRange": [1, 1]
57 | },
58 |
59 | {
60 | "name": "Minecraft",
61 | "desc": "Minecraft's XP curve changes a bit depending on your level, but this is the curve from level 0-15. Close enough. I recommend 3-11 XP per message, which is how much a Bottle o' Enchanting grants.",
62 | "curve": { "3": 0, "2": 1, "1": 6 },
63 | "round": 1,
64 | "bestRange": [3, 11]
65 | },
66 |
67 | {
68 | "name": "Arcane",
69 | "desc": "What a mysterious curve that definitely doesn't come from another bot!",
70 | "curve": { "3": 0, "2": 50, "1": 25 },
71 | "round": 5,
72 | "bestRange": [15, 40]
73 | },
74 |
75 | {
76 | "name": "Definitely not Mee6",
77 | "desc": "I can assure you that this oddly specific curve was not lifted from that one bot people don't like. Also last time I checked, you can't own a cubic function.",
78 | "curve": { "3": 1.6666666667, "2": 22.5, "1": 75.8333333333 },
79 | "round": 5,
80 | "bestRange": [15, 25]
81 | }
82 | ],
83 |
84 | "difficultyRatings": {
85 | "0": "Linear",
86 | "1": "Basically linear",
87 | "25": "Almost no scaling",
88 | "50": "Extremely generous",
89 | "100": "Very generous",
90 | "225": "Generous",
91 | "350": "Slightly generous",
92 | "500": "Normal",
93 | "1000": "Slightly spicy",
94 | "1500": "Moderately spicy",
95 | "2000": "Spicy",
96 | "2500": "Very spicy",
97 | "3250": "Extremely spicy",
98 | "4000": "Dangerously spicy",
99 | "5000": "Insanely spicy",
100 | "6500": "Brutal",
101 | "10000": "Extremely brutal",
102 | "20000": "What the actual heck",
103 | "35000": "Are you out of your mind",
104 | "50000": "AAAAAAAAAAAAAAAAAAAAAAA"
105 | }
106 | }
--------------------------------------------------------------------------------
/json/default_status.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "Custom",
3 | "name": "Polaris",
4 | "state": "/rank",
5 | "status": "online"
6 | }
--------------------------------------------------------------------------------
/json/multiplier_modes.json:
--------------------------------------------------------------------------------
1 | {
2 | "rolePriority": {
3 | "largest": "Role with highest multiplier",
4 | "smallest": "Role with lowest multiplier",
5 | "highest": "Highest role",
6 | "add": "All multipliers summed, n-1 each",
7 | "combine": "All multipliers combined"
8 | },
9 |
10 | "channelStacking": {
11 | "multiply": "Multipliers combined",
12 | "add": "Multipliers summed, n-1 each",
13 | "largest": "Largest between role and channel",
14 | "channel": "Channel multiplier priority",
15 | "role": "Role multiplier priority"
16 | }
17 | }
--------------------------------------------------------------------------------
/json/quick_settings.json:
--------------------------------------------------------------------------------
1 | {
2 |
3 | "home": [
4 | { "groupName": "Quick settings" },
5 | { "emoji": "✨", "name": "General", "folder": "general" },
6 | { "emoji": "🎁", "name": "Levelling", "folder": "level" },
7 | { "emoji": "📊", "name": "Rank and Leaderboard", "folder": "rank" }
8 | ],
9 |
10 | "general": [
11 | { "emoji": "⬅️", "name": "Back", "folder": "home", "groupName": "General settings" },
12 | { "db": "enabled", "name": "Enable XP", "desc": "Allows members to gain XP", "tip": "Disabling will prevent anyone from gaining XP, but will not reset or remove existing XP." },
13 | { "db": "gain.min", "name": "Min XP", "desc": "Minimum amount of XP to earn per message", "tip": "Each message gives a random amount of XP, this changes the lower bound" },
14 | { "db": "gain.max", "name": "Max XP", "desc": "Maximum amount of XP to earn per message", "tip": "Each message gives a random amount of XP, this changes the upper bound" },
15 | { "db": "gain.time", "name": "XP Cooldown", "desc": "Members must wait this many seconds before they can gain XP again", "tip": "Messages sent during the cooldown period will not gain", "str": "# seconds" },
16 | { "db": "maxLevel", "name": "Level cap", "desc": "The highest level members can reach", "tip": "Members can still gain XP past this, unless you reward a role with a 0x multiplier" }
17 | ],
18 |
19 | "level": [
20 | { "emoji": "⬅️", "name": "Back", "folder": "home", "groupName": "Levelling settings" },
21 | { "db": "levelUp.enabled", "name": "Enable level up message", "desc": "Sends a message or DM when a member levels up" },
22 | { "db": "levelUp.multiple", "name": "Level up multiple", "desc": "Only sends the level up message when reaching a multiple of this number", "tip": "A multiple of 5 sends on levels 5, 10, 15, 20, 25, ...", "str": "Every # level(s)" },
23 | { "db": "levelUp.multipleUntil", "name": "Level up multiple cap", "desc": "After reaching this level, the level up multiple is no longer used and a message is posted on every level reached", "tip": "Set to 0 to disable", "str": "Level #", "zeroText": "Disabled" },
24 | { "space": true },
25 | { "db": "rewardSyncing.noManual", "name": "Manual sync", "invert": true, "desc": "Allows members to manually sync their roles whenever they want, via /sync" },
26 | { "db": "rewardSyncing.noWarning", "name": "Sync warning", "invert": true, "desc": "Show a warning message in /rank when a member's roles aren't synced properly" }
27 | ],
28 |
29 | "rank": [
30 | { "emoji": "⬅️", "name": "Back", "folder": "home", "groupName": "Rank and Leaderboard settings" },
31 | { "db": "rankCard.disabled", "name": "Enable /rank", "invert": true, "desc": "Enables /rank cards", "tip": "Disabling this also hides most info in /calculate" },
32 | { "db": "rankCard.ephemeral", "name": "Hide /rank messages", "desc": "Forces /rank messages to be ephemeral, meaning only the member who typed the commmand can see it" },
33 | { "db": "rankCard.hideCooldown", "name": "Hide cooldown", "desc": "Hides the amount of time until XP can be gained again " },
34 | { "db": "hideMultipliers", "name": "Hide multipliers", "desc": "Hides which roles have multipliers (except 0x)" },
35 | { "db": "rankCard.relativeLevel", "name": "Show relative XP", "desc": "Changes the 'next level' section of /rank to start at 0 and only include XP from that level", "tip": "e.g. If level 10 requires 2000 XP and level 11 requires 3000, it will display \"500/1000 XP until level 11\" for a member with 2500 XP" },
36 | { "space": true },
37 | { "db": "leaderboard.disabled", "name": "Enable leaderboard", "invert": true, "desc": "Enables the server XP leaderboard" },
38 | { "db": "leaderboard.private", "name": "Private leaderboard", "desc": "Restricts the online leaderboard so only server members can access it (requires logging in)" },
39 | { "db": "leaderboard.hideRoles", "name": "Hide reward roles", "desc": "Hides the list of reward roles on the online leaderboard" },
40 | { "db": "leaderboard.ephemeral", "name": "Hide /top messages", "desc": "Forces /top messages to be ephemeral, meaning only the member who typed the commmand can see it" },
41 | { "db": "leaderboard.minLevel", "name": "Minimum leaderboard level", "desc": "Restricts the leaderboard to only show members above this level", "tip": "Set to 0 to disable", "zeroText": "None" },
42 | { "db": "leaderboard.maxEntries", "name": "Max leaderboard entries", "desc": "Only shows the top X members on the leaderboard", "tip": "Set to 0 to display everyone", "zeroText": "Unlimited" }
43 | ]
44 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@discordjs/builders": "^1.9.0",
4 | "@discordjs/rest": "^2.4.0",
5 | "bufferutil": "^4.0.8",
6 | "connect-timeout": "^1.9.0",
7 | "cookie-parser": "^1.4.6",
8 | "cors": "^2.8.5",
9 | "discord-api-types": "^0.37.99",
10 | "discord.js": "^14.16.1",
11 | "dotenv": "^16.4.5",
12 | "express": "^4.19.2",
13 | "express-async-errors": "^3.1.1",
14 | "install": "^0.13.0",
15 | "mongoose": "^8.6.1",
16 | "ordinal": "^1.0.3",
17 | "utf-8-validate": "^6.0.4",
18 | "zlib-sync": "^0.1.9"
19 | },
20 | "name": "polaris",
21 | "version": "1.0.0",
22 | "main": "index.js",
23 | "scripts": {
24 | "test": "echo \"Error: no test specified\" && exit 1"
25 | },
26 | "author": "",
27 | "license": "ISC",
28 | "description": ""
29 | }
30 |
--------------------------------------------------------------------------------
/polaris.code-workspace:
--------------------------------------------------------------------------------
1 | {
2 | "folders": [
3 | {
4 | "path": "."
5 | }
6 | ]
7 | }
--------------------------------------------------------------------------------
/polaris.js:
--------------------------------------------------------------------------------
1 | const Discord = require("discord.js")
2 | require('dotenv').config();
3 |
4 | const token = process.env.DISCORD_TOKEN
5 | if (!token) return console.log("No Discord token provided! Put one in your .env file")
6 |
7 | const Shard = new Discord.ShardingManager('./index.js', { token } );
8 | const guildsPerShard = 2000
9 |
10 | Discord.fetchRecommendedShardCount(token, {guildsPerShard}).then(shards => {
11 | let shardCount = Math.floor(shards)
12 | console.info(shardCount == 1 ? "Starting up..." : `Preparing ${shardCount} shards...`)
13 | Shard.spawn({amount: shardCount, timeout: 60000}).catch(console.error)
14 | Shard.on('shardCreate', shard => {
15 | shard.on("disconnect", (event) => {
16 | console.warn(`Shard ${shard.id} disconnected!`); console.log(event);
17 | });
18 | shard.on("death", (event) => {
19 | console.warn(`Shard ${shard.id} died!\nExit code: ${event.exitCode}`);
20 | });
21 | shard.on("reconnecting", () => {
22 | console.info(`Shard ${shard.id} is reconnecting!`);
23 | });
24 |
25 | })
26 | }).catch(e => {console.log(e.headers || e)})
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Polaris!
2 | ...is a super customizable XP bot for Discord with all sorts of neat features!
3 | Unfortunately, it's become increasingly annoying to host, so I'm passing the torch by open-sourcing all the messy code and allowing anyone to host this thing!
4 |
5 | If you're an experienced software dev i am so sorry for what comes next
6 |
7 | ## How do I host it?
8 | It's ""easy!""
9 |
10 | ### Step 0: Node.js
11 | 1. If you don't have node.js, [go get it](https://nodejs.org/en)
12 | 2. Once you've set up node, run `npm i` in the bot's root directory
13 | - If you're new around here, and you're on Windows, you can open up the terminal in a specific directory by shift+rightclicking a blank spot in the folder and pressing Open in Terminal/Powershell/cmd/etc.
14 |
15 | ### Step 1: Setting up the bot
16 | Fortunately Discord makes this super easy, thanks Discord
17 | 1. Create a Discord Application via the [developer portal](https://discord.com/developers/applications)
18 | - Name it, decorate it, etc
19 | 2. Copy the file named `.env.example`, rename it to `.env`, and open it in a text editor
20 | 3. Remove the placeholder values and paste in the application's actual ID, token, and secret.
21 | - ID can be found in the general tab on the dev portal. It's literally just the bot's account ID
22 | - A token can be generated from the bot tab. Keep it top secret, you should know this
23 | - A secret can be generated from the OAuth2 tab
24 | 3. On the OAuth2 tab, add `http://localhost:6880/auth` as a redirect URI. You can change the port as long as you do so in config.json as well.
25 | 4. Invite the bot to a server by going to [this link](https://discord.com/oauth2/authorize?client_id=123456789&permissions=429765545024&scope=bot%20applications.commands) and replacing "123456789" in the URL with your bot's ID
26 |
27 | ### Step 2: Set up the config file
28 | 1. Open the `config.json` file in the codebase
29 | - Add a server ID to `test_server_ids`, this is where dev commands will be deployed when you run the bot for the first time
30 | - Add your own user ID to `developer_ids` so you can run dev commands
31 | - `lockBotToDevOnly` makes it so only you can use the bot, for local testing and such
32 | - There's a couple settings for the web server, you probably don't really need to touch them
33 | - `siteURL` does **NOT** control the actual URL for your server - it just changes where the bot links users to
34 | - If you provide a `changelogURL` or `supportURL` they'll appear in /botstatus
35 | 2. Test out the bot by opening up your terminal in the root directory and typing `node polaris.js`
36 | - Most commands won't work due to the lack of a database, but if the bot appears online it means you're good
37 | - Only dev commands will be present by default, the rest will be deployed in step 4. Dev commands are only visible to server admins and only work if you're specified as a dev in the config file
38 |
39 | ### Step 3: Setting up the database
40 | Personally I know very little about this topic so if it sounds like I have no idea what I'm saying, it's because I don't. This is just what I do for my own projects.
41 | This step is like the equivalent of learning about port forwarding for your Minecraft server, so don't feel bad if this is the point where you give up
42 |
43 | There's many different ways to set up MongoDB, but I recommend one of these methods:
44 |
45 | **Option 1: [MongoDB Atlas](https://cloud.mongodb.com/)**
46 | - This is MongoDB's cloud service. It's by far the easiest to set up, but all the data is stored on their cloud, not yours. The free tier has a storage limit, but you won't go anywhere close to exceeding it.
47 |
48 | **Option 2: Host it on a server**
49 | - My personal choice, because I have one. If you have an Ubuntu server, [this tutorial](https://www.digitalocean.com/community/tutorials/how-to-install-mongodb-on-ubuntu-20-04) followed by [this one](https://www.digitalocean.com/community/tutorials/how-to-secure-mongodb-on-ubuntu-20-04) should be good. (if the `mongo` command doesn't work, use `mongosh`)
50 | - If you don't have an Ubuntu server, just google around and there will probably be a guide for your platform
51 | - If you created a DB along with a username and password, you did it correctly
52 | - I also recommend setting up [MongoDB compass](https://www.mongodb.com/products/tools/compass) so you have a GUI!
53 |
54 | **Option 3: Host it on your computer**
55 | - Probably not the wisest idea since it needs to be running 24/7. I would only do this if you're hosting the entire bot on it for some reason, or just want to test things out. But if that sounds like a plan to you, go follow this [absurdly long tutorial](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-windows/).
56 |
57 | All set? Awesome. Polaris uses two collections: `servers` for server data, and `auth` for website logins. I'm pretty sure the bot automatically creates these for you.
58 |
59 | 1. Find your **connection string**. The way you obtain it depends on how you set up MongoDB, but there will definitely be one. It should start with `mongodb://` or `mongodb+srv://` or something similar. **If you're self-hosting and can't find the string, you can skip this and just use the username + password you set up.**
60 |
61 | 2. Open up `.env` and paste in your database name as well as the connection string (MONGO_DB_URI in the file). If you don't want to use a connection string you can leave the value blank and provide the IP, username, and password instead.
62 |
63 | 3. Fire up the bot and check the console to see if it connected!
64 | - To double check, you can run /db with no arguments - if the bot responds (likely "No data!") it means you actually did it correctly!!! If not, cry
65 |
66 | If it's not connecting, try checking:
67 | - Is the database actually running?
68 | - Did you paste the connection string incorrectly?
69 | - Did you enter the correct database name into .env?
70 | - Did you enter the right username and password?
71 |
72 |
73 | ### Step 4: Final steps
74 | 1. Deploy the bot's commands by running /deploy with the global argument set to true
75 | 2. If you have the web server enabled, it should be running on localhost. From there you can authorize your Discord account and change server settings
76 | - The web server uses a lot of resources but is also needed to modify advanced settings for the bot. It also contains the leaderboard page
77 | - If you're happy with your settings and only plan on using the bot for one server, you can disable the server in `config.json` and only enable it when you need to tweak things
78 | - Note that most simple settings (booleans and numbers) can be tweaked from /config
79 | - Reward roles and multipliers can be configured via /rewardrole and /multiplier
80 | - If you're running Polaris from a hosted server, make sure the port you chose is open. Then you should be able to visit `http://:`, e.g. http://7.7.7.7:6880. Make sure to also add it as an OAuth2 redirect in the Discord dev portal, ending in /auth. (e.g. http://7.7.7.7:6880/auth)
81 | - If you don't want the URL to be a shady looking IP, you're going to need to buy a domain then reverse proxy your localhost into an actual public URL. I wish you luck. (add that one as an OAuth2 redirect as well)
82 | - Just google "localhost to public URL" and you should get some info on how to do this
83 | - Alternatively, you can try [Cloudflare Tunnel](https://developers.cloudflare.com/pages/how-to/preview-with-cloudflare-tunnel/) or [ngrok](https://ngrok.com/) - though these are usually more temporary solutions
84 |
85 | 3. If you want the bot to be public, set that up in the Discord dev portal. But make sure you can handle it.
86 | - The bot should work fine until sharding kicks in (at ~2500 servers), then it might start to break down a little
87 | - Really, it comes down to your server specs and the number of members in a server
88 |
89 | ---
90 |
91 | ## Some other tips
92 | ### Transferring data from the original Polaris
93 | **NOTE**: If you are listed as a bot developer, you can access the dashboard for any server your bot is in. The JSON import feature is heavily limited for non-devs (security reasons), so feel free to use this power in order to import .json files for others.
94 | 1. On the [original Polaris dashboard](https://gdcolon.com/polaris), go to the Data tab of your server settings and press **Download all data**. This will download a .json file
95 | 2. On your own hosted dashboard, go to the Data tab of your server settings, scroll down, and scroll down to the import settings section
96 | 3. Upload the .json file and press import
97 | 4. All data from Polaris should be transferred!
98 |
99 | ### Using dev commands
100 | `/db` allows you to view a server's raw data, or modify it
101 | - e.g. `/db property:settings.multipliers` returns the data in `settings.multipliers`
102 | - e.g. `/db property:users.123456.xp new_value:10` sets the user with ID 123456's XP to 10
103 |
104 | `/setactivity`lets you change the bot's custom status, the args should walk you through it
105 |
106 | `/setversion` updates the version number in /botstatus
107 |
108 | `/deploy` deploys dev commands to the server, or the global commands everyone uses.
109 | - There's also an option to undeploy the dev commands, but make sure at least one server has them or you'll need to dive into the code to get them back
110 | - If this happens, open `index.js` and change `if (cmds.size < 1)` to `if (true)` in order to force-deploy the commands on the next startup
111 |
112 | `/run` simply lets you evaluate js code, not much need for this unless you're adding new stuff and are familiar with discord.js
113 |
114 | Devs can also view and modify any server from the web dashboard. The main use for this is importing from .json files, since only bot devs can do that (security reasons)
115 |
116 | ## Want to modify the bot?
117 | Do whatever you want as long as you credit me and use your own fork for it.
118 |
119 | * If you're hosting this publicly, credit me extra hard
120 | * Do not add any paid or monetized features
121 | * Issues and PRs on this repo are only for things that improve the open-source code, it's not a place for feature requests and new stuff as I'm no longer maintaining this bot
122 |
123 | If you ever have any questions feel free to reach out to me, the Polaris support server is a good place for it
124 |
125 | And if the code is bad, forgive me
--------------------------------------------------------------------------------