Compare commits

...

No commits in common. "9c48f600515ac45d27e7958d2412eee8e78ea2b7" and "ebc793d833f7e6b4720ac155893c7e00464641f9" have entirely different histories.

127 changed files with 12670 additions and 262 deletions

47
.eslintrc.json Normal file
View File

@ -0,0 +1,47 @@
{
"extends": "@nhcarrigan",
"rules": {
"camelcase": [
"error",
{
"allow": [
"serverId_userId",
"userId_serverId",
"pull_request",
"issue_number",
"issue_comment",
"serverId_level_roleId",
"serverId_roleId",
"invites_disabled_until",
"dms_disabled_until"
]
}
]
},
"overrides": [
{
"files": ["src/server/github/*.ts"],
"rules": {
"camelcase": [
"error",
{
"allow": ["icon_url"]
}
],
"jsdoc/require-jsdoc": "off"
}
},
{
"files": ["src/modules/subcommands/config/*.ts"],
"rules": {
"jsdoc/require-param": "off"
}
},
{
"files": ["src/modules/data/*.ts"],
"rules": {
"require-atomic-updates": "off"
}
}
]
}

View File

@ -1,69 +0,0 @@
name: 🐛 Bug Report
description: Something isn't working as expected? Let us know!
title: '[BUG] - '
labels:
- "status/awaiting triage"
body:
- type: checkboxes
id: attestations
attributes:
label: Attestations
description: "By checking the boxes below, I certify that:"
options:
- label: "I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)"
validations:
required: true
- label: I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
validations:
required: true
- label: I have confirmed that the issue I am opening is unique, and has not already been reported (whether closed or not).
validations:
required: true
- label: I have reviewed the [Security Policy](https://docs.nhcarrigan.com/legal/security/) and have determined that this is not a security vulnerability.
validations:
required: true
- type: textarea
id: description
attributes:
label: "Describe your Issue:"
description: A clear and concise description of what the bug is.
validations:
required: true
- type: dropdown
id: reproduce
attributes:
label: Can you reproduce this issue?
options:
- Yes
- No
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: "Steps to Reproduce:"
description: Steps to reproduce the behavior.
- type: input
id: os
attributes:
label: "Operating System:"
description: The operating system you are using, including the version/build number.
validations:
required: true
# Remove this section for non-web apps.
- type: input
id: browser
attributes:
label: "Browser:"
description: The browser you are using, including the version number.
validations:
required: true
- type: dropdown
attributes:
label: Are you willing and able to contribute a fix?
options:
- Yes
- No
validations:
required: true

View File

@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: "Discord"
url: "https://chat.nhcarrigan.com"
about: "Chat with us directly."

View File

@ -1,46 +0,0 @@
name: 💭 Feature Proposal
description: Have an idea for how we can improve? Share it here!
title: '[FEAT] - '
labels:
- "status/awaiting triage"
body:
- type: checkboxes
id: attestations
attributes:
label: Attestations
description: "By checking the boxes below, I certify that:"
options:
- label: "I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)"
validations:
required: true
- label: I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
validations:
required: true
- label: I have confirmed that the issue I am opening is unique, and has not already been reported (whether closed or not).
validations:
required: true
- label: I have reviewed the [Security Policy](https://docs.nhcarrigan.com/legal/security/) and have determined that this is not a security vulnerability.
validations:
required: true
- type: textarea
id: description
attributes:
label: "Describe your Idea:"
description: A clear and concise description of the feature you would like added.
validations:
required: true
- type: textarea
id: solution
attributes:
label: "What problem does this feature solve?"
description: Why are you requesting this feature? How would it improve your experience with the product?
validations:
required: true
- type: dropdown
attributes:
label: Are you willing and able to contribute this feature?
options:
- Yes
- No
validations:
required: true

View File

@ -1,34 +0,0 @@
name: ❓ Other Issue
description: I have something that is neither a bug nor a feature request.
title: '[OTHER] - '
labels:
- "status/awaiting triage"
body:
- type: checkboxes
id: attestations
attributes:
label: Attestations
description: "By checking the boxes below, I certify that:"
options:
- label: "I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)"
validations:
required: true
- label: I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
validations:
required: true
- label: I have confirmed that the issue I am opening is unique, and has not already been reported (whether closed or not).
validations:
required: true
- label: I have reviewed the [Security Policy](https://docs.nhcarrigan.com/legal/security/) and have determined that this is not a security vulnerability.
validations:
required: true
- label: This is not a feature request or bug report that I am mis-filing to avoid the issue template.
validations:
required: true
- type: textarea
id: description
attributes:
label: "Share your thoughts:"
description: Why are you opening this issue?
validations:
required: true

View File

@ -1,91 +0,0 @@
name: "Pull Request Template"
about: "Template for pulls"
body:
- type: textarea
id: explain
attributes:
label: "Explanation"
description: "Briefly explain WHY this pull request is necessary. Do not explain what it does, as that's evidenced in the changes."
validations:
required: true
- type: input
id: issue
attributes:
label: "Issue"
description: "My pull request relates to or resolves the following issue number:"
validations:
required: true
is_number: true
- type: checkboxes
id: attestations
attributes:
label: Attestations
description: "By checking the boxes below, I certify that:"
options:
- label: "I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/)"
validations:
required: true
- label: I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/).
validations:
required: true
- label: My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/).
validations:
required: true
- type: checkboxes
id: dependencies
attributes:
label: Dependencies
description: "My pull request adds or updates dependencies, so:"
options:
- label: I have pinned the dependencies to a specific patch version.
validations:
required: false
- type: checkboxes
id: style
attributes:
label: Style
description: "My contribution adheres to the following style guidelines:"
options:
- label: I have run the linter and resolved any errors.
validations:
required: true
- label: My pull request uses an appropriate title, matching the conventional commit standards.
validations:
required: true
- label: My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request.
validations:
required: true
- type: checkboxes
id: tests
attributes:
label: Tests
description: "My contribution includes the following tests:"
options:
- label: My contribution adds new code, and I have added tests to cover it.
validations:
required: false
- label: My contribution modifies existing code, and I have updated the tests to reflect these changes.
validations:
required: false
- label: All new and existing tests pass locally with my changes.
validations:
required: true
- label: Code coverage remains at or above the configured threshold.
validations:
required: true
- type: input
id: docs
attributes:
label: Documentation
description: "I have made the following PR to update the documentation site with my changes:"
validations:
required: true
- type: dropdown
id: version
attributes:
label: Versioning
description: "I believe my changes should be included in the following release:"
options:
- "Major - My pull request introduces a breaking change."
- "Minor - My pull request introduces a new non-breaking feature."
- "Patch - My pull request introduces bug fixes ONLY."

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/node_modules/
/prod/
.env
.scannerwork

15
.knip.jsonc Normal file
View File

@ -0,0 +1,15 @@
{
"$schema": "https://unpkg.com/knip@2/schema.json",
"entry": [
"src/index.ts",
/**
* Because the commands are dynamically generated, knip can't follow them.
* Treat them as entry files to allow for dependency and import/export validation.
*/
"src/commands/*.ts",
"src/contexts/*.ts"
],
"project": ["src/**/*.ts"],
"ignore": [],
"ignoreDependencies": []
}

1
.npmrc Normal file
View File

@ -0,0 +1 @@
enable-pre-post-scripts=true

1
.prettierrc.json Normal file
View File

@ -0,0 +1 @@
"@nhcarrigan/prettier-config"

View File

@ -1,20 +1,6 @@
# New Repository Template # Mod Bot
This template contains all of our basic files for a new GitHub repository. There is also a handy workflow that will create an issue on a new repository made from this template, with a checklist for the steps we usually take in setting up a new repository. My personal mod bot
If you're starting a Node.JS project with TypeScript, we have a [specific template](https://github.com/naomi-lgbt/nodejs-typescript-template) for that purpose.
## Readme
Delete all of the above text (including this line), and uncomment the below text to use our standard readme template.
<!-- # Project Name
Project Description
## Live Version
This page is currently deployed. [View the live website.]
## Feedback and Bugs ## Feedback and Bugs
@ -36,4 +22,4 @@ Copyright held by Naomi Carrigan.
## Contact ## Contact
We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`. --> We may be contacted through our [Chat Server](http://chat.nhcarrigan.com) or via email at `contact@nhcarrigan.com`.

464
export.csv Normal file
View File

@ -0,0 +1,464 @@
userId,birthday
960685326425276467,950947200000
593911525396447252,9.544896e+11
806306626779742238,9.571644e+11
369158860906299393,9.47232e+11
897994614139682816,9.70902e+11
301972134471663616,9.664956e+11
814106185615540244,9.724572e+11
930992122004381746,9.499104e+11
842325321990406144,9.468864e+11
804601870454751233,9.525024e+11
942411503980642305,9.510336e+11
884137592910659714,9.776448e+11
952725532590604319,9.620892e+11
817257291081842728,9.482688e+11
836140378985594930,9.630396e+11
706727650663596073,9.766944e+11
942773725491785798,9.530208e+11
799701704521547828,9.54576e+11
883789675200913429,9.568188e+11
751930684679127091,9.693468e+11
920308846809985034,9.7047e+11
751434580460044298,9.777312e+11
937849237545320508,9.571644e+11
762170361486770196,9.655452e+11
969858846015238184,9.737568e+11
925855248902402138,9.739296e+11
986617067547529267,9.608796e+11
889603830738739211,9.749664e+11
958318665755922462,9.537984e+11
982393273446441010,9.58374e+11
949446766187667477,9.734112e+11
969491819014520882,9.715068e+11
978141442524471386,9.469728e+11
501147423603949568,9.469728e+11
850106457471189067,9.470592e+11
883623136883515442,9.471456e+11
814497580679561256,9.471456e+11
536306727482818573,9.473184e+11
968335702620266527,9.473184e+11
864468315756822528,9.474048e+11
940054871686651974,9.477504e+11
929252976256778251,9.477504e+11
748954804298186893,9.478368e+11
829236923951480852,9.482688e+11
569996979086819349,9.484416e+11
888500205287247932,9.484416e+11
847509003214127114,9.484416e+11
892914216976154634,9.487008e+11
830696113291919389,9.487872e+11
978249464030584843,9.4896e+11
926800470616461353,9.49392e+11
900175581193338900,9.49824e+11
916960888861392896,9.499104e+11
809579580318416897,9.499104e+11
885625974887186495,9.501696e+11
970862084692803644,9.50256e+11
817972713188360253,9.510336e+11
528887802386055179,9.513792e+11
933364111885086780,9.513792e+11
911030353949507644,9.517248e+11
125851949131235328,9.518112e+11
828997905025204244,9.521568e+11
963338353644818482,9.526752e+11
954079146932326431,9.533664e+11
776866758933479434,9.537984e+11
927782055645962270,9.540576e+11
964177218660102244,9.544896e+11
927747007857172520,9.546624e+11
923716338474688652,9.54918e+11
698916189962895470,9.550908e+11
772100671271862292,9.550908e+11
938297364387606548,9.550908e+11
896276665397813318,9.554364e+11
883909693976739881,9.563004e+11
782025516668289075,9.564732e+11
943321273575038976,9.565596e+11
798279539158941716,9.56646e+11
869849413592449064,9.5751e+11
737642652329050112,9.575964e+11
822853107871514684,9.582012e+11
960353974798647376,9.58374e+11
929168350544810004,9.584604e+11
949735064374239334,9.584604e+11
405583819786289152,9.584604e+11
947041852127854592,9.585468e+11
902936994156544111,9.587196e+11
898030525573386290,9.589788e+11
923373033194917918,9.591516e+11
949966822273331230,9.591516e+11
968976718545174529,9.591516e+11
392171690672521218,9.593244e+11
884854878751567902,9.593244e+11
793752852367540235,9.593244e+11
921211997406756954,9.594108e+11
795731344784752701,9.594972e+11
786504021417918494,9.595836e+11
909975385301712948,9.600156e+11
789271116433850398,9.601884e+11
727451062729965598,9.602748e+11
938771903328419901,9.602748e+11
903677510821220383,9.604476e+11
888350648935002113,9.604476e+11
777874981072928768,9.60534e+11
961635755107696761,9.606204e+11
320350590091919361,9.607068e+11
947616232646017085,9.608796e+11
899571770334523402,9.610524e+11
901423313551773706,9.611388e+11
933270792421134366,9.613116e+11
939754221807501342,9.61398e+11
929652531934691419,9.6183e+11
933736442356715540,9.6183e+11
927939361037754399,9.619164e+11
830705497798869012,9.619164e+11
907848580092526622,9.620892e+11
952330255622864928,9.621756e+11
773386458475659285,9.62694e+11
624943341003341824,9.628668e+11
883785280023302164,9.629532e+11
731578489588809850,9.630396e+11
876113741157138475,9.63126e+11
954268835127640084,9.632988e+11
952339587135590420,9.632988e+11
884455553361920100,9.634716e+11
857271670876143636,9.634716e+11
957140361208283166,9.636444e+11
355163845330731019,9.637308e+11
944742898707099699,9.637308e+11
955926904169447444,9.638172e+11
818024688122462218,9.638172e+11
753353117419962388,9.639036e+11
969766207785934898,9.640764e+11
916072244504035359,9.642492e+11
870697425453584444,9.64422e+11
863804410521452614,9.645948e+11
946617741744406530,9.64854e+11
751780892585885716,9.651132e+11
706860532195524720,9.65286e+11
873030420919377951,9.654588e+11
926993801581572127,9.655452e+11
892987879423348777,9.65718e+11
928935641587261440,9.659772e+11
976400825532436500,9.660636e+11
619944471726915584,9.664092e+11
853697904529899531,9.664092e+11
690240980925808652,9.66582e+11
822804221425614903,9.669276e+11
745682978822291602,9.671868e+11
962098921482633246,9.675324e+11
807440022282305556,9.677916e+11
871906778684747778,9.67878e+11
797727287968923649,9.679644e+11
750469279131893760,9.679644e+11
822624645374279713,9.681372e+11
498134028768116736,9.683964e+11
938612713901396029,9.684828e+11
813983780452171806,9.684828e+11
903991458472800297,9.688284e+11
892462297962926130,9.690012e+11
860523345890902018,9.690876e+11
817975261748002836,9.69174e+11
725034380061442058,9.692604e+11
865665545646899222,9.694332e+11
766085113514950688,9.694332e+11
871119390551183391,9.695196e+11
821207737253494804,9.701244e+11
862420898852896797,9.702972e+11
925757921869070336,9.705564e+11
982551055231492136,9.707292e+11
677085534920900650,9.710748e+11
911739781048827964,9.711612e+11
853995029643264030,9.711612e+11
917903365453779074,9.71334e+11
851599010308423710,9.719388e+11
897553187723182120,9.720252e+11
869067139938664460,9.722844e+11
834910688983384156,9.722844e+11
908168554409496596,9.724572e+11
784535670932373515,9.7263e+11
801027228208726046,9.727164e+11
909520793241813024,9.728928e+11
870252602238066708,9.73152e+11
828145876873641985,9.73584e+11
939127356931600434,9.738432e+11
934907505946021939,9.739296e+11
881406978969174038,9.74016e+11
815144239893577728,9.74016e+11
848601063485472818,9.74448e+11
342990951834189824,9.74448e+11
943133386329968712,9.747936e+11
905204204241584161,9.75312e+11
815994799769124874,9.755712e+11
876897882173739018,9.759168e+11
667192664202149898,9.759168e+11
887128769071038464,9.76176e+11
890997720930721823,9.763488e+11
889639434927095869,9.763488e+11
805802130064670751,9.766944e+11
707976108104220673,9.768672e+11
970360770036641852,9.769536e+11
901092878376386600,9.773856e+11
740133215687409674,9.77472e+11
825749318140166144,9.77904e+11
986737571369480232,9.547452e+11
716005462654320741,9.507744e+11
753493140681523260,9.482688e+11
942122987149230191,9.645948e+11
723587327343067187,9.559548e+11
808534409136701472,9.488736e+11
713234483972669451,9.723708e+11
861765725429039104,9.739296e+11
943284431312023552,9.780768e+11
988711957244821514,9.684828e+11
465650873650118659,9.639036e+11
906540161251303465,9.743616e+11
885786249867177984,9.54918e+11
964698414837272587,9.74016e+11
986242698744823808,9.50256e+11
993145412233994330,9.6399e+11
994034635988287619,9.638172e+11
781227238745833513,9.769536e+11
999713824611512390,9.492192e+11
856553597352148992,9.73584e+11
996092350944247878,9.671004e+11
716784769874133042,9.663228e+11
466350927800958976,9.742752e+11
886741601928486914,9.491328e+11
962530967799619584,9.67878e+11
933714145109491722,9.730656e+11
978325559807459398,9.72198e+11
456109470284644362,9.552636e+11
1000385188460773466,9.491328e+11
830973274720174100,9.468864e+11
1005415001877655563,9.7047e+11
783417366708486154,9.483552e+11
934212646696259634,9.76176e+11
947582240353812580,9.64854e+11
675462080488407040,9.544032e+11
967186748553695242,9.750528e+11
658623086266023967,9.5112e+11
931498801468936222,9.512928e+11
880972339339223122,9.486144e+11
745024269255442454,9.529344e+11
1018935880792166420,9.671004e+11
993504739373363240,9.508608e+11
983097023261712524,9.634716e+11
969459597867896853,9.551772e+11
699310108693495851,9.681372e+11
998018739817693205,9.536256e+11
1026133045343953008,9.747936e+11
1013502860090155080,9.54144e+11
1004202436807897099,9.703836e+11
1042061436236611645,9.590652e+11
888458203577327686,9.51984e+11
1013685906139533385,9.664092e+11
940514631636631593,9.499968e+11
616678407253655612,9.561276e+11
1043477069914452049,9.5112e+11
447224284243427329,9.484416e+11
840804417476362270,9.561276e+11
929838053332246549,9.781632e+11
766382066852954192,9.518976e+11
1050487602412789821,9.680508e+11
885464067240779816,9.47232e+11
1032714210133688380,9.689148e+11
1056146445889183796,9.614844e+11
1047295595074830336,9.500832e+11
1003207378184982538,9.47232e+11
1043104995194716170,9.743616e+11
923369644520247296,9.606204e+11
1032574355516969002,9.540576e+11
1002390101227208714,9.584604e+11
1058975267713982565,9.531072e+11
980743925180530708,9.624348e+11
795057210940981318,9.499104e+11
812841462248767499,9.724572e+11
1037356055933497364,9.772992e+11
587719422643535873,9.659772e+11
1066473378816467054,9.732384e+11
1027400955249041480,9.639036e+11
1043228779876716584,9.564732e+11
858058607731146762,9.513792e+11
838060361894068264,9.548316e+11
971147645475258368,9.539712e+11
936056120420761611,9.550908e+11
1041288670570889297,9.671868e+11
952332159979503677,9.573372e+11
708153903425912952,9.77472e+11
345756943450898433,9.702972e+11
692453290427941005,9.737568e+11
654102447320596510,9.629532e+11
722597580214632461,9.533664e+11
476907947277025295,9.555228e+11
349328356338302976,9.561276e+11
443126436157456394,9.530208e+11
758788702389403648,9.556092e+11
478752726612967435,9.578556e+11
836048120718688266,9.590652e+11
796537667218178119,9.667548e+11
1083891438393233568,9.63126e+11
1048169103950614638,9.733248e+11
1081648317823463488,9.712476e+11
1065061496842879017,9.685692e+11
804529541716508694,9.542304e+11
1053901402772078652,9.682236e+11
1000032163414163476,9.573372e+11
1085892495470239834,9.656316e+11
893146906329501756,9.693468e+11
1013149541035425878,9.782496e+11
1006707714803650590,9.638172e+11
1064576791051776010,9.69606e+11
1064167725800370176,9.467136e+11
914058373857706034,9.633852e+11
1096401339996717108,9.772128e+11
1073783318799196301,9.5328e+11
1050421303775080469,9.561276e+11
816328583810383922,9.572508e+11
1068941746026852423,9.629532e+11
1083443026149519411,9.76608e+11
1066726987139391579,9.7263e+11
812796288666959892,9.509472e+11
975232564270886983,9.711612e+11
922956516624052285,9.646812e+11
996471123623546890,9.544032e+11
697750438887555072,9.569916e+11
1100069245565546506,9.664956e+11
1073834635320107059,9.782496e+11
936771696575262751,9.642492e+11
1089986976888918158,9.612252e+11
959020201540718596,9.632124e+11
886181760587665468,9.645084e+11
1092800350383255612,9.55782e+11
1079749357789786193,9.64854e+11
993247623265914880,9.658044e+11
1083189908992163871,9.715068e+11
882843628907671562,9.539712e+11
736566913597177907,9.638172e+11
1099193351531671659,9.619164e+11
1111663268134662224,9.538848e+11
892147474091888680,9.76176e+11
986427592972308490,9.654588e+11
160794456004624385,9.696924e+11
924667929142919209,9.734112e+11
1097067552926076989,9.709884e+11
1121177427327058010,9.720252e+11
1122221632916815882,9.702108e+11
893610763137200148,9.620028e+11
925962397926182942,9.484416e+11
792776499777896460,9.503424e+11
1124390303269408889,9.74016e+11
1124127967208018022,9.632124e+11
793268834739159060,9.74448e+11
946646420834906114,9.715932e+11
948342978169151488,9.633852e+11
700324066128822334,9.504288e+11
1076338388547928064,9.550044e+11
1013593008232476723,9.575964e+11
1124112953550053378,9.651132e+11
1048569035022934036,9.742752e+11
1129738558324883587,9.719388e+11
824775420070199346,9.758304e+11
1044373961502367754,9.646812e+11
971870039772901436,9.673596e+11
982494441766010930,9.672732e+11
958360658238382112,9.473184e+11
1029865425397354536,9.76176e+11
966845743635787846,9.51984e+11
1015670876827553792,9.494784e+11
1052253666414972938,9.725436e+11
1088634101977845770,9.499968e+11
1107924271386333314,9.52848e+11
1099709536585138217,9.664956e+11
1089269275040174080,9.539712e+11
1134924437876129943,9.732384e+11
974006072497020928,9.736704e+11
1145995037423968386,9.564732e+11
1124005776114061393,9.755712e+11
1074110463111090256,9.676188e+11
1024785963756560394,9.559548e+11
931805733807329331,9.772128e+11
747208441155813386,9.514656e+11
1112116656794255413,9.555228e+11
1155432498612932669,9.663228e+11
975433081060216843,9.625212e+11
1108729730758344704,9.480096e+11
992644193849716766,9.591516e+11
1161920897809129473,9.520704e+11
1130970063512535050,9.544032e+11
970352694827036712,9.582876e+11
1086855977401340016,9.602748e+11
966731025155768351,9.736704e+11
962594606879502356,9.47664e+11
910537967473414174,9.61398e+11
1142887920366260397,9.747936e+11
1173460045850214472,9.61398e+11
1113241385689161819,9.636444e+11
1048366042130415647,9.76608e+11
1143672078898315364,9.501696e+11
882437468899659797,9.688284e+11
1123479840306241546,9.54144e+11
832341844695711804,9.548316e+11
1202014923660730442,9.501696e+11
1059181289560875038,9.702972e+11
1138800113318383688,9.747936e+11
1214577235223257171,9.62262e+11
969983248384032890,9.526752e+11
1191169941299282048,9.53712e+11
1221863172538110112,9.57078e+11
1173287717853986840,9.727164e+11
791344978529091624,9.637308e+11
1113449820229730314,9.582876e+11
1072593794123452567,9.499968e+11
965324768712736809,9.58806e+11
1152295675254550558,9.493056e+11
1158434359259430982,9.538848e+11
1129051334302236683,9.620028e+11
1222277835746574339,9.659772e+11
245692567172284416,9.64422e+11
1113794793588396115,9.666684e+11
998373604288892948,9.752256e+11
1106896766873391185,9.547452e+11
1209356657746771992,9.73152e+11
1085080386217967656,9.480096e+11
1089260336441471157,9.5967e+11
1076529106793009182,9.508608e+11
947300599119085618,9.51552e+11
1234432274670555207,9.76176e+11
878870207182032906,9.627804e+11
1117584772793897061,9.588924e+11
1071461349328175306,9.4896e+11
1236077514103853089,9.523296e+11
904008772446453781,9.581148e+11
1114332546755465288,9.780768e+11
929654937565155359,9.710748e+11
1236391959535550524,9.520704e+11
986011723448328284,9.645084e+11
1091538778444808202,9.671004e+11
1227230559751639115,9.718524e+11
1084799653771481108,9.6831e+11
1069059534435389460,9.599292e+11
582536628904394762,9.638172e+11
872829194210512987,9.6831e+11
1159667407841927188,9.483552e+11
1247109607864664156,9.494784e+11
1238867447944056856,9.525024e+11
1181335824424509563,9.654588e+11
1169915475518558318,9.490464e+11
1236647018420502570,9.54144e+11
961988895829012540,9.69174e+11
1089619893902651502,9.508608e+11
1162165396988764241,9.65718e+11
1147045357658853428,9.513792e+11
1212163717655961695,9.616572e+11
778641350006013982,9.73152e+11
949815669023703050,9.756576e+11
570621131652988953,9.779904e+11
1189442227827654677,9.572508e+11
703924532959772682,9.5967e+11
988803994161905705,9.604476e+11
1201179652488699985,9.483552e+11
808282311887159326,9.483552e+11
1224875525890375863,9.54144e+11
1197350521116311742,9.479232e+11
1 userId birthday
2 960685326425276467 950947200000
3 593911525396447252 9.544896e+11
4 806306626779742238 9.571644e+11
5 369158860906299393 9.47232e+11
6 897994614139682816 9.70902e+11
7 301972134471663616 9.664956e+11
8 814106185615540244 9.724572e+11
9 930992122004381746 9.499104e+11
10 842325321990406144 9.468864e+11
11 804601870454751233 9.525024e+11
12 942411503980642305 9.510336e+11
13 884137592910659714 9.776448e+11
14 952725532590604319 9.620892e+11
15 817257291081842728 9.482688e+11
16 836140378985594930 9.630396e+11
17 706727650663596073 9.766944e+11
18 942773725491785798 9.530208e+11
19 799701704521547828 9.54576e+11
20 883789675200913429 9.568188e+11
21 751930684679127091 9.693468e+11
22 920308846809985034 9.7047e+11
23 751434580460044298 9.777312e+11
24 937849237545320508 9.571644e+11
25 762170361486770196 9.655452e+11
26 969858846015238184 9.737568e+11
27 925855248902402138 9.739296e+11
28 986617067547529267 9.608796e+11
29 889603830738739211 9.749664e+11
30 958318665755922462 9.537984e+11
31 982393273446441010 9.58374e+11
32 949446766187667477 9.734112e+11
33 969491819014520882 9.715068e+11
34 978141442524471386 9.469728e+11
35 501147423603949568 9.469728e+11
36 850106457471189067 9.470592e+11
37 883623136883515442 9.471456e+11
38 814497580679561256 9.471456e+11
39 536306727482818573 9.473184e+11
40 968335702620266527 9.473184e+11
41 864468315756822528 9.474048e+11
42 940054871686651974 9.477504e+11
43 929252976256778251 9.477504e+11
44 748954804298186893 9.478368e+11
45 829236923951480852 9.482688e+11
46 569996979086819349 9.484416e+11
47 888500205287247932 9.484416e+11
48 847509003214127114 9.484416e+11
49 892914216976154634 9.487008e+11
50 830696113291919389 9.487872e+11
51 978249464030584843 9.4896e+11
52 926800470616461353 9.49392e+11
53 900175581193338900 9.49824e+11
54 916960888861392896 9.499104e+11
55 809579580318416897 9.499104e+11
56 885625974887186495 9.501696e+11
57 970862084692803644 9.50256e+11
58 817972713188360253 9.510336e+11
59 528887802386055179 9.513792e+11
60 933364111885086780 9.513792e+11
61 911030353949507644 9.517248e+11
62 125851949131235328 9.518112e+11
63 828997905025204244 9.521568e+11
64 963338353644818482 9.526752e+11
65 954079146932326431 9.533664e+11
66 776866758933479434 9.537984e+11
67 927782055645962270 9.540576e+11
68 964177218660102244 9.544896e+11
69 927747007857172520 9.546624e+11
70 923716338474688652 9.54918e+11
71 698916189962895470 9.550908e+11
72 772100671271862292 9.550908e+11
73 938297364387606548 9.550908e+11
74 896276665397813318 9.554364e+11
75 883909693976739881 9.563004e+11
76 782025516668289075 9.564732e+11
77 943321273575038976 9.565596e+11
78 798279539158941716 9.56646e+11
79 869849413592449064 9.5751e+11
80 737642652329050112 9.575964e+11
81 822853107871514684 9.582012e+11
82 960353974798647376 9.58374e+11
83 929168350544810004 9.584604e+11
84 949735064374239334 9.584604e+11
85 405583819786289152 9.584604e+11
86 947041852127854592 9.585468e+11
87 902936994156544111 9.587196e+11
88 898030525573386290 9.589788e+11
89 923373033194917918 9.591516e+11
90 949966822273331230 9.591516e+11
91 968976718545174529 9.591516e+11
92 392171690672521218 9.593244e+11
93 884854878751567902 9.593244e+11
94 793752852367540235 9.593244e+11
95 921211997406756954 9.594108e+11
96 795731344784752701 9.594972e+11
97 786504021417918494 9.595836e+11
98 909975385301712948 9.600156e+11
99 789271116433850398 9.601884e+11
100 727451062729965598 9.602748e+11
101 938771903328419901 9.602748e+11
102 903677510821220383 9.604476e+11
103 888350648935002113 9.604476e+11
104 777874981072928768 9.60534e+11
105 961635755107696761 9.606204e+11
106 320350590091919361 9.607068e+11
107 947616232646017085 9.608796e+11
108 899571770334523402 9.610524e+11
109 901423313551773706 9.611388e+11
110 933270792421134366 9.613116e+11
111 939754221807501342 9.61398e+11
112 929652531934691419 9.6183e+11
113 933736442356715540 9.6183e+11
114 927939361037754399 9.619164e+11
115 830705497798869012 9.619164e+11
116 907848580092526622 9.620892e+11
117 952330255622864928 9.621756e+11
118 773386458475659285 9.62694e+11
119 624943341003341824 9.628668e+11
120 883785280023302164 9.629532e+11
121 731578489588809850 9.630396e+11
122 876113741157138475 9.63126e+11
123 954268835127640084 9.632988e+11
124 952339587135590420 9.632988e+11
125 884455553361920100 9.634716e+11
126 857271670876143636 9.634716e+11
127 957140361208283166 9.636444e+11
128 355163845330731019 9.637308e+11
129 944742898707099699 9.637308e+11
130 955926904169447444 9.638172e+11
131 818024688122462218 9.638172e+11
132 753353117419962388 9.639036e+11
133 969766207785934898 9.640764e+11
134 916072244504035359 9.642492e+11
135 870697425453584444 9.64422e+11
136 863804410521452614 9.645948e+11
137 946617741744406530 9.64854e+11
138 751780892585885716 9.651132e+11
139 706860532195524720 9.65286e+11
140 873030420919377951 9.654588e+11
141 926993801581572127 9.655452e+11
142 892987879423348777 9.65718e+11
143 928935641587261440 9.659772e+11
144 976400825532436500 9.660636e+11
145 619944471726915584 9.664092e+11
146 853697904529899531 9.664092e+11
147 690240980925808652 9.66582e+11
148 822804221425614903 9.669276e+11
149 745682978822291602 9.671868e+11
150 962098921482633246 9.675324e+11
151 807440022282305556 9.677916e+11
152 871906778684747778 9.67878e+11
153 797727287968923649 9.679644e+11
154 750469279131893760 9.679644e+11
155 822624645374279713 9.681372e+11
156 498134028768116736 9.683964e+11
157 938612713901396029 9.684828e+11
158 813983780452171806 9.684828e+11
159 903991458472800297 9.688284e+11
160 892462297962926130 9.690012e+11
161 860523345890902018 9.690876e+11
162 817975261748002836 9.69174e+11
163 725034380061442058 9.692604e+11
164 865665545646899222 9.694332e+11
165 766085113514950688 9.694332e+11
166 871119390551183391 9.695196e+11
167 821207737253494804 9.701244e+11
168 862420898852896797 9.702972e+11
169 925757921869070336 9.705564e+11
170 982551055231492136 9.707292e+11
171 677085534920900650 9.710748e+11
172 911739781048827964 9.711612e+11
173 853995029643264030 9.711612e+11
174 917903365453779074 9.71334e+11
175 851599010308423710 9.719388e+11
176 897553187723182120 9.720252e+11
177 869067139938664460 9.722844e+11
178 834910688983384156 9.722844e+11
179 908168554409496596 9.724572e+11
180 784535670932373515 9.7263e+11
181 801027228208726046 9.727164e+11
182 909520793241813024 9.728928e+11
183 870252602238066708 9.73152e+11
184 828145876873641985 9.73584e+11
185 939127356931600434 9.738432e+11
186 934907505946021939 9.739296e+11
187 881406978969174038 9.74016e+11
188 815144239893577728 9.74016e+11
189 848601063485472818 9.74448e+11
190 342990951834189824 9.74448e+11
191 943133386329968712 9.747936e+11
192 905204204241584161 9.75312e+11
193 815994799769124874 9.755712e+11
194 876897882173739018 9.759168e+11
195 667192664202149898 9.759168e+11
196 887128769071038464 9.76176e+11
197 890997720930721823 9.763488e+11
198 889639434927095869 9.763488e+11
199 805802130064670751 9.766944e+11
200 707976108104220673 9.768672e+11
201 970360770036641852 9.769536e+11
202 901092878376386600 9.773856e+11
203 740133215687409674 9.77472e+11
204 825749318140166144 9.77904e+11
205 986737571369480232 9.547452e+11
206 716005462654320741 9.507744e+11
207 753493140681523260 9.482688e+11
208 942122987149230191 9.645948e+11
209 723587327343067187 9.559548e+11
210 808534409136701472 9.488736e+11
211 713234483972669451 9.723708e+11
212 861765725429039104 9.739296e+11
213 943284431312023552 9.780768e+11
214 988711957244821514 9.684828e+11
215 465650873650118659 9.639036e+11
216 906540161251303465 9.743616e+11
217 885786249867177984 9.54918e+11
218 964698414837272587 9.74016e+11
219 986242698744823808 9.50256e+11
220 993145412233994330 9.6399e+11
221 994034635988287619 9.638172e+11
222 781227238745833513 9.769536e+11
223 999713824611512390 9.492192e+11
224 856553597352148992 9.73584e+11
225 996092350944247878 9.671004e+11
226 716784769874133042 9.663228e+11
227 466350927800958976 9.742752e+11
228 886741601928486914 9.491328e+11
229 962530967799619584 9.67878e+11
230 933714145109491722 9.730656e+11
231 978325559807459398 9.72198e+11
232 456109470284644362 9.552636e+11
233 1000385188460773466 9.491328e+11
234 830973274720174100 9.468864e+11
235 1005415001877655563 9.7047e+11
236 783417366708486154 9.483552e+11
237 934212646696259634 9.76176e+11
238 947582240353812580 9.64854e+11
239 675462080488407040 9.544032e+11
240 967186748553695242 9.750528e+11
241 658623086266023967 9.5112e+11
242 931498801468936222 9.512928e+11
243 880972339339223122 9.486144e+11
244 745024269255442454 9.529344e+11
245 1018935880792166420 9.671004e+11
246 993504739373363240 9.508608e+11
247 983097023261712524 9.634716e+11
248 969459597867896853 9.551772e+11
249 699310108693495851 9.681372e+11
250 998018739817693205 9.536256e+11
251 1026133045343953008 9.747936e+11
252 1013502860090155080 9.54144e+11
253 1004202436807897099 9.703836e+11
254 1042061436236611645 9.590652e+11
255 888458203577327686 9.51984e+11
256 1013685906139533385 9.664092e+11
257 940514631636631593 9.499968e+11
258 616678407253655612 9.561276e+11
259 1043477069914452049 9.5112e+11
260 447224284243427329 9.484416e+11
261 840804417476362270 9.561276e+11
262 929838053332246549 9.781632e+11
263 766382066852954192 9.518976e+11
264 1050487602412789821 9.680508e+11
265 885464067240779816 9.47232e+11
266 1032714210133688380 9.689148e+11
267 1056146445889183796 9.614844e+11
268 1047295595074830336 9.500832e+11
269 1003207378184982538 9.47232e+11
270 1043104995194716170 9.743616e+11
271 923369644520247296 9.606204e+11
272 1032574355516969002 9.540576e+11
273 1002390101227208714 9.584604e+11
274 1058975267713982565 9.531072e+11
275 980743925180530708 9.624348e+11
276 795057210940981318 9.499104e+11
277 812841462248767499 9.724572e+11
278 1037356055933497364 9.772992e+11
279 587719422643535873 9.659772e+11
280 1066473378816467054 9.732384e+11
281 1027400955249041480 9.639036e+11
282 1043228779876716584 9.564732e+11
283 858058607731146762 9.513792e+11
284 838060361894068264 9.548316e+11
285 971147645475258368 9.539712e+11
286 936056120420761611 9.550908e+11
287 1041288670570889297 9.671868e+11
288 952332159979503677 9.573372e+11
289 708153903425912952 9.77472e+11
290 345756943450898433 9.702972e+11
291 692453290427941005 9.737568e+11
292 654102447320596510 9.629532e+11
293 722597580214632461 9.533664e+11
294 476907947277025295 9.555228e+11
295 349328356338302976 9.561276e+11
296 443126436157456394 9.530208e+11
297 758788702389403648 9.556092e+11
298 478752726612967435 9.578556e+11
299 836048120718688266 9.590652e+11
300 796537667218178119 9.667548e+11
301 1083891438393233568 9.63126e+11
302 1048169103950614638 9.733248e+11
303 1081648317823463488 9.712476e+11
304 1065061496842879017 9.685692e+11
305 804529541716508694 9.542304e+11
306 1053901402772078652 9.682236e+11
307 1000032163414163476 9.573372e+11
308 1085892495470239834 9.656316e+11
309 893146906329501756 9.693468e+11
310 1013149541035425878 9.782496e+11
311 1006707714803650590 9.638172e+11
312 1064576791051776010 9.69606e+11
313 1064167725800370176 9.467136e+11
314 914058373857706034 9.633852e+11
315 1096401339996717108 9.772128e+11
316 1073783318799196301 9.5328e+11
317 1050421303775080469 9.561276e+11
318 816328583810383922 9.572508e+11
319 1068941746026852423 9.629532e+11
320 1083443026149519411 9.76608e+11
321 1066726987139391579 9.7263e+11
322 812796288666959892 9.509472e+11
323 975232564270886983 9.711612e+11
324 922956516624052285 9.646812e+11
325 996471123623546890 9.544032e+11
326 697750438887555072 9.569916e+11
327 1100069245565546506 9.664956e+11
328 1073834635320107059 9.782496e+11
329 936771696575262751 9.642492e+11
330 1089986976888918158 9.612252e+11
331 959020201540718596 9.632124e+11
332 886181760587665468 9.645084e+11
333 1092800350383255612 9.55782e+11
334 1079749357789786193 9.64854e+11
335 993247623265914880 9.658044e+11
336 1083189908992163871 9.715068e+11
337 882843628907671562 9.539712e+11
338 736566913597177907 9.638172e+11
339 1099193351531671659 9.619164e+11
340 1111663268134662224 9.538848e+11
341 892147474091888680 9.76176e+11
342 986427592972308490 9.654588e+11
343 160794456004624385 9.696924e+11
344 924667929142919209 9.734112e+11
345 1097067552926076989 9.709884e+11
346 1121177427327058010 9.720252e+11
347 1122221632916815882 9.702108e+11
348 893610763137200148 9.620028e+11
349 925962397926182942 9.484416e+11
350 792776499777896460 9.503424e+11
351 1124390303269408889 9.74016e+11
352 1124127967208018022 9.632124e+11
353 793268834739159060 9.74448e+11
354 946646420834906114 9.715932e+11
355 948342978169151488 9.633852e+11
356 700324066128822334 9.504288e+11
357 1076338388547928064 9.550044e+11
358 1013593008232476723 9.575964e+11
359 1124112953550053378 9.651132e+11
360 1048569035022934036 9.742752e+11
361 1129738558324883587 9.719388e+11
362 824775420070199346 9.758304e+11
363 1044373961502367754 9.646812e+11
364 971870039772901436 9.673596e+11
365 982494441766010930 9.672732e+11
366 958360658238382112 9.473184e+11
367 1029865425397354536 9.76176e+11
368 966845743635787846 9.51984e+11
369 1015670876827553792 9.494784e+11
370 1052253666414972938 9.725436e+11
371 1088634101977845770 9.499968e+11
372 1107924271386333314 9.52848e+11
373 1099709536585138217 9.664956e+11
374 1089269275040174080 9.539712e+11
375 1134924437876129943 9.732384e+11
376 974006072497020928 9.736704e+11
377 1145995037423968386 9.564732e+11
378 1124005776114061393 9.755712e+11
379 1074110463111090256 9.676188e+11
380 1024785963756560394 9.559548e+11
381 931805733807329331 9.772128e+11
382 747208441155813386 9.514656e+11
383 1112116656794255413 9.555228e+11
384 1155432498612932669 9.663228e+11
385 975433081060216843 9.625212e+11
386 1108729730758344704 9.480096e+11
387 992644193849716766 9.591516e+11
388 1161920897809129473 9.520704e+11
389 1130970063512535050 9.544032e+11
390 970352694827036712 9.582876e+11
391 1086855977401340016 9.602748e+11
392 966731025155768351 9.736704e+11
393 962594606879502356 9.47664e+11
394 910537967473414174 9.61398e+11
395 1142887920366260397 9.747936e+11
396 1173460045850214472 9.61398e+11
397 1113241385689161819 9.636444e+11
398 1048366042130415647 9.76608e+11
399 1143672078898315364 9.501696e+11
400 882437468899659797 9.688284e+11
401 1123479840306241546 9.54144e+11
402 832341844695711804 9.548316e+11
403 1202014923660730442 9.501696e+11
404 1059181289560875038 9.702972e+11
405 1138800113318383688 9.747936e+11
406 1214577235223257171 9.62262e+11
407 969983248384032890 9.526752e+11
408 1191169941299282048 9.53712e+11
409 1221863172538110112 9.57078e+11
410 1173287717853986840 9.727164e+11
411 791344978529091624 9.637308e+11
412 1113449820229730314 9.582876e+11
413 1072593794123452567 9.499968e+11
414 965324768712736809 9.58806e+11
415 1152295675254550558 9.493056e+11
416 1158434359259430982 9.538848e+11
417 1129051334302236683 9.620028e+11
418 1222277835746574339 9.659772e+11
419 245692567172284416 9.64422e+11
420 1113794793588396115 9.666684e+11
421 998373604288892948 9.752256e+11
422 1106896766873391185 9.547452e+11
423 1209356657746771992 9.73152e+11
424 1085080386217967656 9.480096e+11
425 1089260336441471157 9.5967e+11
426 1076529106793009182 9.508608e+11
427 947300599119085618 9.51552e+11
428 1234432274670555207 9.76176e+11
429 878870207182032906 9.627804e+11
430 1117584772793897061 9.588924e+11
431 1071461349328175306 9.4896e+11
432 1236077514103853089 9.523296e+11
433 904008772446453781 9.581148e+11
434 1114332546755465288 9.780768e+11
435 929654937565155359 9.710748e+11
436 1236391959535550524 9.520704e+11
437 986011723448328284 9.645084e+11
438 1091538778444808202 9.671004e+11
439 1227230559751639115 9.718524e+11
440 1084799653771481108 9.6831e+11
441 1069059534435389460 9.599292e+11
442 582536628904394762 9.638172e+11
443 872829194210512987 9.6831e+11
444 1159667407841927188 9.483552e+11
445 1247109607864664156 9.494784e+11
446 1238867447944056856 9.525024e+11
447 1181335824424509563 9.654588e+11
448 1169915475518558318 9.490464e+11
449 1236647018420502570 9.54144e+11
450 961988895829012540 9.69174e+11
451 1089619893902651502 9.508608e+11
452 1162165396988764241 9.65718e+11
453 1147045357658853428 9.513792e+11
454 1212163717655961695 9.616572e+11
455 778641350006013982 9.73152e+11
456 949815669023703050 9.756576e+11
457 570621131652988953 9.779904e+11
458 1189442227827654677 9.572508e+11
459 703924532959772682 9.5967e+11
460 988803994161905705 9.604476e+11
461 1201179652488699985 9.483552e+11
462 808282311887159326 9.483552e+11
463 1224875525890375863 9.54144e+11
464 1197350521116311742 9.479232e+11

463
export.json Normal file
View File

@ -0,0 +1,463 @@
[{"_id":{"$oid":"62abe68fac5a7307f5535a4f"},"userId":"960685326425276467","birthday":9.509472E+11,"__v":0},
{"_id":{"$oid":"62abe6f8ac5a7307f5535a5b"},"userId":"593911525396447252","birthday":9.544896E+11,"__v":0},
{"_id":{"$oid":"62abe718ac5a7307f5535a5f"},"userId":"806306626779742238","birthday":9.571644E+11,"__v":0},
{"_id":{"$oid":"62abe728ac5a7307f5535a63"},"userId":"369158860906299393","birthday":9.47232E+11,"__v":0},
{"_id":{"$oid":"62abe7a3ac5a7307f5535a6b"},"userId":"897994614139682816","birthday":9.70902E+11,"__v":0},
{"_id":{"$oid":"62abe819ac5a7307f5535a6f"},"userId":"301972134471663616","birthday":9.664956E+11,"__v":0},
{"_id":{"$oid":"62abeab4ac5a7307f5535a77"},"userId":"814106185615540244","birthday":9.724572E+11,"__v":0},
{"_id":{"$oid":"62abed38ac5a7307f5535a7b"},"userId":"930992122004381746","birthday":9.499104E+11,"__v":0},
{"_id":{"$oid":"62abeda1ac5a7307f5535a7f"},"userId":"842325321990406144","birthday":9.468864E+11,"__v":0},
{"_id":{"$oid":"62abef9cac5a7307f5535a83"},"userId":"804601870454751233","birthday":9.525024E+11,"__v":0},
{"_id":{"$oid":"62abf1d9ac5a7307f5535a87"},"userId":"942411503980642305","birthday":9.510336E+11,"__v":0},
{"_id":{"$oid":"62abf874ac5a7307f5535a8c"},"userId":"884137592910659714","birthday":9.776448E+11,"__v":0},
{"_id":{"$oid":"62ac0f0fac5a7307f5535a98"},"userId":"952725532590604319","birthday":9.620892E+11,"__v":0},
{"_id":{"$oid":"62ac14b1ac5a7307f5535a9c"},"userId":"817257291081842728","birthday":9.482688E+11,"__v":0},
{"_id":{"$oid":"62ac1c47ac5a7307f5535aa0"},"userId":"836140378985594930","birthday":9.630396E+11,"__v":0},
{"_id":{"$oid":"62ac2eb9ac5a7307f5535aa4"},"userId":"706727650663596073","birthday":9.766944E+11,"__v":0},
{"_id":{"$oid":"62ac448bac5a7307f5535aa8"},"userId":"942773725491785798","birthday":9.530208E+11,"__v":0},
{"_id":{"$oid":"62ac5391ac5a7307f5535aad"},"userId":"799701704521547828","birthday":9.54576E+11,"__v":0},
{"_id":{"$oid":"62ac5726ac5a7307f5535ab1"},"userId":"883789675200913429","birthday":9.568188E+11,"__v":0},
{"_id":{"$oid":"62ac6226ac5a7307f5535ab9"},"userId":"751930684679127091","birthday":9.693468E+11,"__v":0},
{"_id":{"$oid":"62ac6b02ac5a7307f5535ac1"},"userId":"920308846809985034","birthday":9.7047E+11,"__v":0},
{"_id":{"$oid":"62ac6da5ac5a7307f5535ac5"},"userId":"751434580460044298","birthday":9.777312E+11,"__v":0},
{"_id":{"$oid":"62aca34aac5a7307f5535aca"},"userId":"937849237545320508","birthday":9.571644E+11,"__v":0},
{"_id":{"$oid":"62ada767ac5a7307f5535ad7"},"userId":"762170361486770196","birthday":9.655452E+11,"__v":0},
{"_id":{"$oid":"62ae246fdb10fb7b32761f38"},"userId":"969858846015238184","birthday":9.737568E+11,"__v":0},
{"_id":{"$oid":"62ae4edfdb10fb7b32761f3c"},"userId":"925855248902402138","birthday":9.739296E+11,"__v":0},
{"_id":{"$oid":"62b0a93cdb10fb7b32761f4b"},"userId":"986617067547529267","birthday":9.608796E+11,"__v":0},
{"_id":{"$oid":"62b0ceecdb10fb7b32761f4f"},"userId":"889603830738739211","birthday":9.749664E+11,"__v":0},
{"_id":{"$oid":"62b321b6db10fb7b32761f5b"},"userId":"958318665755922462","birthday":9.537984E+11,"__v":0},
{"_id":{"$oid":"62b6207edb10fb7b32761f6b"},"userId":"982393273446441010","birthday":9.58374E+11,"__v":0},
{"_id":{"$oid":"62bc0ff9db10fb7b32761f9b"},"userId":"949446766187667477","birthday":9.734112E+11,"__v":0},
{"_id":{"$oid":"62be526fdb10fb7b32761faf"},"userId":"969491819014520882","birthday":9.715068E+11,"__v":0},
{"_id":{"$oid":"62c1f98ce6de6499a3c3954e"},"userId":"978141442524471386","birthday":9.469728E+11,"__v":0},
{"_id":{"$oid":"62c1f98ce6de6499a3c39551"},"userId":"501147423603949568","birthday":9.469728E+11,"__v":0},
{"_id":{"$oid":"62c1f98ce6de6499a3c39554"},"userId":"850106457471189067","birthday":9.470592E+11,"__v":0},
{"_id":{"$oid":"62c1f98ce6de6499a3c39557"},"userId":"883623136883515442","birthday":9.471456E+11,"__v":0},
{"_id":{"$oid":"62c1f98ce6de6499a3c3955a"},"userId":"814497580679561256","birthday":9.471456E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c3955e"},"userId":"536306727482818573","birthday":9.473184E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39561"},"userId":"968335702620266527","birthday":9.473184E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39567"},"userId":"864468315756822528","birthday":9.474048E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c3956a"},"userId":"940054871686651974","birthday":9.477504E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c3956d"},"userId":"929252976256778251","birthday":9.477504E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39570"},"userId":"748954804298186893","birthday":9.478368E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39573"},"userId":"829236923951480852","birthday":9.482688E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39577"},"userId":"569996979086819349","birthday":9.484416E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c3957a"},"userId":"888500205287247932","birthday":9.484416E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c3957d"},"userId":"847509003214127114","birthday":9.484416E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39580"},"userId":"892914216976154634","birthday":9.487008E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39583"},"userId":"830696113291919389","birthday":9.487872E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39589"},"userId":"978249464030584843","birthday":9.4896E+11,"__v":0},
{"_id":{"$oid":"62c1f98de6de6499a3c39590"},"userId":"926800470616461353","birthday":9.49392E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c39593"},"userId":"900175581193338900","birthday":9.49824E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c39596"},"userId":"916960888861392896","birthday":9.499104E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c3959a"},"userId":"809579580318416897","birthday":9.499104E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c3959d"},"userId":"885625974887186495","birthday":9.501696E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395a3"},"userId":"970862084692803644","birthday":9.50256E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395ac"},"userId":"817972713188360253","birthday":9.510336E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395af"},"userId":"528887802386055179","birthday":9.513792E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395b2"},"userId":"933364111885086780","birthday":9.513792E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395b5"},"userId":"911030353949507644","birthday":9.517248E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395b8"},"userId":"125851949131235328","birthday":9.518112E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395bb"},"userId":"828997905025204244","birthday":9.521568E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395bf"},"userId":"963338353644818482","birthday":9.526752E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395c5"},"userId":"954079146932326431","birthday":9.533664E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395c8"},"userId":"776866758933479434","birthday":9.537984E+11,"__v":0},
{"_id":{"$oid":"62c1f98ee6de6499a3c395cb"},"userId":"927782055645962270","birthday":9.540576E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395d2"},"userId":"964177218660102244","birthday":9.544896E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395d5"},"userId":"927747007857172520","birthday":9.546624E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395db"},"userId":"923716338474688652","birthday":9.54918E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395de"},"userId":"698916189962895470","birthday":9.550908E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395e1"},"userId":"772100671271862292","birthday":9.550908E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395e4"},"userId":"938297364387606548","birthday":9.550908E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395e7"},"userId":"896276665397813318","birthday":9.554364E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395ee"},"userId":"883909693976739881","birthday":9.563004E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395f1"},"userId":"782025516668289075","birthday":9.564732E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395f4"},"userId":"943321273575038976","birthday":9.565596E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395f7"},"userId":"798279539158941716","birthday":9.56646E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c395ff"},"userId":"869849413592449064","birthday":9.5751E+11,"__v":0},
{"_id":{"$oid":"62c1f98fe6de6499a3c39602"},"userId":"737642652329050112","birthday":9.575964E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c3960b"},"userId":"822853107871514684","birthday":9.582012E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c3960f"},"userId":"960353974798647376","birthday":9.58374E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c39612"},"userId":"929168350544810004","birthday":9.584604E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c39615"},"userId":"949735064374239334","birthday":9.584604E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c39618"},"userId":"405583819786289152","birthday":9.584604E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c3961e"},"userId":"947041852127854592","birthday":9.585468E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c39621"},"userId":"902936994156544111","birthday":9.587196E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c39624"},"userId":"898030525573386290","birthday":9.589788E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c3962a"},"userId":"923373033194917918","birthday":9.591516E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c3962d"},"userId":"949966822273331230","birthday":9.591516E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c39630"},"userId":"968976718545174529","birthday":9.591516E+11,"__v":0},
{"_id":{"$oid":"62c1f990e6de6499a3c39633"},"userId":"392171690672521218","birthday":9.593244E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39636"},"userId":"884854878751567902","birthday":9.593244E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39639"},"userId":"793752852367540235","birthday":9.593244E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c3963c"},"userId":"921211997406756954","birthday":9.594108E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c3963f"},"userId":"795731344784752701","birthday":9.594972E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39642"},"userId":"786504021417918494","birthday":9.595836E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39645"},"userId":"909975385301712948","birthday":9.600156E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39648"},"userId":"789271116433850398","birthday":9.601884E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c3964e"},"userId":"727451062729965598","birthday":9.602748E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39651"},"userId":"938771903328419901","birthday":9.602748E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39654"},"userId":"903677510821220383","birthday":9.604476E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39657"},"userId":"888350648935002113","birthday":9.604476E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c3965a"},"userId":"777874981072928768","birthday":9.60534E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c3965d"},"userId":"961635755107696761","birthday":9.606204E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39660"},"userId":"320350590091919361","birthday":9.607068E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c39669"},"userId":"947616232646017085","birthday":9.608796E+11,"__v":0},
{"_id":{"$oid":"62c1f991e6de6499a3c3966f"},"userId":"899571770334523402","birthday":9.610524E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39672"},"userId":"901423313551773706","birthday":9.611388E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39675"},"userId":"933270792421134366","birthday":9.613116E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39678"},"userId":"939754221807501342","birthday":9.61398E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c3967e"},"userId":"929652531934691419","birthday":9.6183E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39681"},"userId":"933736442356715540","birthday":9.6183E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39687"},"userId":"927939361037754399","birthday":9.619164E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c3968a"},"userId":"830705497798869012","birthday":9.619164E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c3968d"},"userId":"907848580092526622","birthday":9.620892E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39690"},"userId":"952330255622864928","birthday":9.621756E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39696"},"userId":"773386458475659285","birthday":9.62694E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c39699"},"userId":"624943341003341824","birthday":9.628668E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c3969c"},"userId":"883785280023302164","birthday":9.629532E+11,"__v":0},
{"_id":{"$oid":"62c1f992e6de6499a3c3969f"},"userId":"731578489588809850","birthday":9.630396E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396a5"},"userId":"876113741157138475","birthday":9.63126E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396a8"},"userId":"954268835127640084","birthday":9.632988E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396ab"},"userId":"952339587135590420","birthday":9.632988E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396ae"},"userId":"884455553361920100","birthday":9.634716E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396b1"},"userId":"857271670876143636","birthday":9.634716E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396b4"},"userId":"957140361208283166","birthday":9.636444E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396b7"},"userId":"355163845330731019","birthday":9.637308E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396ba"},"userId":"944742898707099699","birthday":9.637308E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396bd"},"userId":"955926904169447444","birthday":9.638172E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396c0"},"userId":"818024688122462218","birthday":9.638172E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396c3"},"userId":"753353117419962388","birthday":9.639036E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396c6"},"userId":"969766207785934898","birthday":9.640764E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396c9"},"userId":"916072244504035359","birthday":9.642492E+11,"__v":0},
{"_id":{"$oid":"62c1f993e6de6499a3c396cc"},"userId":"870697425453584444","birthday":9.64422E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396cf"},"userId":"863804410521452614","birthday":9.645948E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396d2"},"userId":"946617741744406530","birthday":9.64854E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396d5"},"userId":"751780892585885716","birthday":9.651132E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396db"},"userId":"706860532195524720","birthday":9.65286E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396de"},"userId":"873030420919377951","birthday":9.654588E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396e1"},"userId":"926993801581572127","birthday":9.655452E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396e4"},"userId":"892987879423348777","birthday":9.65718E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396ea"},"userId":"928935641587261440","birthday":9.659772E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396ed"},"userId":"976400825532436500","birthday":9.660636E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396f0"},"userId":"619944471726915584","birthday":9.664092E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396f3"},"userId":"853697904529899531","birthday":9.664092E+11,"__v":0},
{"_id":{"$oid":"62c1f994e6de6499a3c396f6"},"userId":"690240980925808652","birthday":9.66582E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c396fc"},"userId":"822804221425614903","birthday":9.669276E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c396ff"},"userId":"745682978822291602","birthday":9.671868E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39702"},"userId":"962098921482633246","birthday":9.675324E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39705"},"userId":"807440022282305556","birthday":9.677916E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39708"},"userId":"871906778684747778","birthday":9.67878E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c3970b"},"userId":"797727287968923649","birthday":9.679644E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c3970e"},"userId":"750469279131893760","birthday":9.679644E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39714"},"userId":"822624645374279713","birthday":9.681372E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c3971a"},"userId":"498134028768116736","birthday":9.683964E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c3971d"},"userId":"938612713901396029","birthday":9.684828E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39720"},"userId":"813983780452171806","birthday":9.684828E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39723"},"userId":"903991458472800297","birthday":9.688284E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39726"},"userId":"892462297962926130","birthday":9.690012E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c39729"},"userId":"860523345890902018","birthday":9.690876E+11,"__v":0},
{"_id":{"$oid":"62c1f995e6de6499a3c3972c"},"userId":"817975261748002836","birthday":9.69174E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c3972f"},"userId":"725034380061442058","birthday":9.692604E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39732"},"userId":"865665545646899222","birthday":9.694332E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39735"},"userId":"766085113514950688","birthday":9.694332E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39738"},"userId":"871119390551183391","birthday":9.695196E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c3973b"},"userId":"821207737253494804","birthday":9.701244E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c3973e"},"userId":"862420898852896797","birthday":9.702972E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39741"},"userId":"925757921869070336","birthday":9.705564E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39744"},"userId":"982551055231492136","birthday":9.707292E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c3974a"},"userId":"677085534920900650","birthday":9.710748E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c3974d"},"userId":"911739781048827964","birthday":9.711612E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39750"},"userId":"853995029643264030","birthday":9.711612E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39753"},"userId":"917903365453779074","birthday":9.71334E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39756"},"userId":"851599010308423710","birthday":9.719388E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c39759"},"userId":"897553187723182120","birthday":9.720252E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c3975c"},"userId":"869067139938664460","birthday":9.722844E+11,"__v":0},
{"_id":{"$oid":"62c1f996e6de6499a3c3975f"},"userId":"834910688983384156","birthday":9.722844E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39762"},"userId":"908168554409496596","birthday":9.724572E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39769"},"userId":"784535670932373515","birthday":9.7263E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c3976c"},"userId":"801027228208726046","birthday":9.727164E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c3976f"},"userId":"909520793241813024","birthday":9.728928E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39772"},"userId":"870252602238066708","birthday":9.73152E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c3977b"},"userId":"828145876873641985","birthday":9.73584E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c3977f"},"userId":"939127356931600434","birthday":9.738432E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39782"},"userId":"934907505946021939","birthday":9.739296E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39785"},"userId":"881406978969174038","birthday":9.74016E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39788"},"userId":"815144239893577728","birthday":9.74016E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39792"},"userId":"848601063485472818","birthday":9.74448E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39795"},"userId":"342990951834189824","birthday":9.74448E+11,"__v":0},
{"_id":{"$oid":"62c1f997e6de6499a3c39798"},"userId":"943133386329968712","birthday":9.747936E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c3979e"},"userId":"905204204241584161","birthday":9.75312E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397a1"},"userId":"815994799769124874","birthday":9.755712E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397a5"},"userId":"876897882173739018","birthday":9.759168E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397a8"},"userId":"667192664202149898","birthday":9.759168E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397ae"},"userId":"887128769071038464","birthday":9.76176E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397b1"},"userId":"890997720930721823","birthday":9.763488E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397b4"},"userId":"889639434927095869","birthday":9.763488E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397b7"},"userId":"805802130064670751","birthday":9.766944E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397ba"},"userId":"707976108104220673","birthday":9.768672E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397bd"},"userId":"970360770036641852","birthday":9.769536E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397c0"},"userId":"901092878376386600","birthday":9.773856E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397c3"},"userId":"740133215687409674","birthday":9.77472E+11,"__v":0},
{"_id":{"$oid":"62c1f998e6de6499a3c397c7"},"userId":"825749318140166144","birthday":9.77904E+11,"__v":0},
{"_id":{"$oid":"62c240930ec74ae79aabc0d6"},"userId":"986737571369480232","birthday":9.547452E+11,"__v":0},
{"_id":{"$oid":"62c67bb80ec74ae79aabc0fb"},"userId":"716005462654320741","birthday":9.507744E+11,"__v":0},
{"_id":{"$oid":"62c6d3220ec74ae79aabc0ff"},"userId":"753493140681523260","birthday":9.482688E+11,"__v":0},
{"_id":{"$oid":"62c6ee580ec74ae79aabc103"},"userId":"942122987149230191","birthday":9.645948E+11,"__v":0},
{"_id":{"$oid":"62c704360ec74ae79aabc108"},"userId":"723587327343067187","birthday":9.559548E+11,"__v":0},
{"_id":{"$oid":"62c9ac3a6806b181cdd154a6"},"userId":"808534409136701472","birthday":9.488736E+11,"__v":0},
{"_id":{"$oid":"62cb89a56806b181cdd154bb"},"userId":"713234483972669451","birthday":9.723708E+11,"__v":0},
{"_id":{"$oid":"62d024b3d5b72abf2f9dacfd"},"userId":"861765725429039104","birthday":9.739296E+11,"__v":0},
{"_id":{"$oid":"62d03297d5b72abf2f9dad01"},"userId":"943284431312023552","birthday":9.780768E+11,"__v":0},
{"_id":{"$oid":"62d04f47d5b72abf2f9dad08"},"userId":"988711957244821514","birthday":9.684828E+11,"__v":0},
{"_id":{"$oid":"62d0716ad5b72abf2f9dad0f"},"userId":"465650873650118659","birthday":9.639036E+11,"__v":0},
{"_id":{"$oid":"62d0a663d5b72abf2f9dad14"},"userId":"906540161251303465","birthday":9.743616E+11,"__v":0},
{"_id":{"$oid":"62d16d96d5b72abf2f9dad1a"},"userId":"885786249867177984","birthday":9.54918E+11,"__v":0},
{"_id":{"$oid":"62d34512d5b72abf2f9dad2d"},"userId":"964698414837272587","birthday":9.74016E+11,"__v":0},
{"_id":{"$oid":"62d450b1d5b72abf2f9dad34"},"userId":"986242698744823808","birthday":9.50256E+11,"__v":0},
{"_id":{"$oid":"62d9c433d5b72abf2f9dad54"},"userId":"993145412233994330","birthday":9.6399E+11,"__v":0},
{"_id":{"$oid":"62da1d96d5b72abf2f9dad58"},"userId":"994034635988287619","birthday":9.638172E+11,"__v":0},
{"_id":{"$oid":"62da2ef3d5b72abf2f9dad5c"},"userId":"781227238745833513","birthday":9.769536E+11,"__v":0},
{"_id":{"$oid":"62dafbbcd5b72abf2f9dad67"},"userId":"999713824611512390","birthday":9.492192E+11,"__v":0},
{"_id":{"$oid":"62f90bb1431a767828839993"},"userId":"856553597352148992","birthday":9.73584E+11,"__v":0},
{"_id":{"$oid":"62fa0169431a7678288399ac"},"userId":"996092350944247878","birthday":9.671004E+11,"__v":0},
{"_id":{"$oid":"62fa9739431a7678288399b5"},"userId":"716784769874133042","birthday":9.663228E+11,"__v":0},
{"_id":{"$oid":"62fa9982431a7678288399b9"},"userId":"466350927800958976","birthday":9.742752E+11,"__v":0},
{"_id":{"$oid":"6306a897431a767828839a17"},"userId":"886741601928486914","birthday":9.491328E+11,"__v":0},
{"_id":{"$oid":"6308f53d431a767828839a30"},"userId":"962530967799619584","birthday":9.67878E+11,"__v":0},
{"_id":{"$oid":"630ad109431a767828839a46"},"userId":"933714145109491722","birthday":9.730656E+11,"__v":0},
{"_id":{"$oid":"630b0f2f431a767828839a4a"},"userId":"978325559807459398","birthday":9.72198E+11,"__v":0},
{"_id":{"$oid":"630f4abd431a767828839a75"},"userId":"456109470284644362","birthday":9.552636E+11,"__v":0},
{"_id":{"$oid":"63165d5d431a767828839aae"},"userId":"1000385188460773466","birthday":9.491328E+11,"__v":0},
{"_id":{"$oid":"632191eb431a767828839afd"},"userId":"830973274720174100","birthday":9.468864E+11,"__v":0},
{"_id":{"$oid":"6321a086431a767828839b01"},"userId":"1005415001877655563","birthday":9.7047E+11,"__v":0},
{"_id":{"$oid":"63263089431a767828839b23"},"userId":"783417366708486154","birthday":9.483552E+11,"__v":0},
{"_id":{"$oid":"632798a7431a767828839b31"},"userId":"934212646696259634","birthday":9.76176E+11,"__v":0},
{"_id":{"$oid":"6327ac7a431a767828839b35"},"userId":"947582240353812580","birthday":9.64854E+11,"__v":0},
{"_id":{"$oid":"632b7e5b431a767828839b53"},"userId":"675462080488407040","birthday":9.544032E+11,"__v":0},
{"_id":{"$oid":"632f2b05431a767828839b6d"},"userId":"967186748553695242","birthday":9.750528E+11,"__v":0},
{"_id":{"$oid":"6335c258431a767828839b95"},"userId":"658623086266023967","birthday":9.5112E+11,"__v":0},
{"_id":{"$oid":"6341eb7b431a767828839bec"},"userId":"931498801468936222","birthday":9.512928E+11,"__v":0},
{"_id":{"$oid":"634c49c6431a767828839c35"},"userId":"880972339339223122","birthday":9.486144E+11,"__v":0},
{"_id":{"$oid":"634d6af1431a767828839c3d"},"userId":"745024269255442454","birthday":9.529344E+11,"__v":0},
{"_id":{"$oid":"635989b6431a767828839c8d"},"userId":"1018935880792166420","birthday":9.671004E+11,"__v":0},
{"_id":{"$oid":"635aa4ab431a767828839ca2"},"userId":"993504739373363240","birthday":9.508608E+11,"__v":0},
{"_id":{"$oid":"635abb3d431a767828839ca7"},"userId":"983097023261712524","birthday":9.634716E+11,"__v":0},
{"_id":{"$oid":"635d5493431a767828839cc1"},"userId":"969459597867896853","birthday":9.551772E+11,"__v":0},
{"_id":{"$oid":"63626a1f431a767828839ceb"},"userId":"699310108693495851","birthday":9.681372E+11,"__v":0},
{"_id":{"$oid":"63671108431a767828839d10"},"userId":"998018739817693205","birthday":9.536256E+11,"__v":0},
{"_id":{"$oid":"6368253a431a767828839d19"},"userId":"1026133045343953008","birthday":9.747936E+11,"__v":0},
{"_id":{"$oid":"6368e4c7431a767828839d1e"},"userId":"1013502860090155080","birthday":9.54144E+11,"__v":0},
{"_id":{"$oid":"636e982e431a767828839d48"},"userId":"1004202436807897099","birthday":9.703836E+11,"__v":0},
{"_id":{"$oid":"6374ebfd3c5a6e0d3faf8ca6"},"userId":"1042061436236611645","birthday":9.590652E+11,"__v":0},
{"_id":{"$oid":"6388b6a03c5a6e0d3faf8d29"},"userId":"888458203577327686","birthday":9.51984E+11,"__v":0},
{"_id":{"$oid":"638a785d3c5a6e0d3faf8d35"},"userId":"1013685906139533385","birthday":9.664092E+11,"__v":0},
{"_id":{"$oid":"638f6afb3c5a6e0d3faf8d5d"},"userId":"940514631636631593","birthday":9.499968E+11,"__v":0},
{"_id":{"$oid":"6391d5e93c5a6e0d3faf8d69"},"userId":"616678407253655612","birthday":9.561276E+11,"__v":0},
{"_id":{"$oid":"639302833c5a6e0d3faf8d79"},"userId":"1043477069914452049","birthday":9.5112E+11,"__v":0},
{"_id":{"$oid":"63942cb33c5a6e0d3faf8d81"},"userId":"447224284243427329","birthday":9.484416E+11,"__v":0},
{"_id":{"$oid":"639c38e53c5a6e0d3faf8da5"},"userId":"840804417476362270","birthday":9.561276E+11,"__v":0},
{"_id":{"$oid":"639d18f13c5a6e0d3faf8daf"},"userId":"929838053332246549","birthday":9.781632E+11,"__v":0},
{"_id":{"$oid":"639fd7c83c5a6e0d3faf8dc3"},"userId":"766382066852954192","birthday":9.518976E+11,"__v":0},
{"_id":{"$oid":"63ad610b3c5a6e0d3faf8e1d"},"userId":"1050487602412789821","birthday":9.680508E+11,"__v":0},
{"_id":{"$oid":"63ad670f3c5a6e0d3faf8e21"},"userId":"885464067240779816","birthday":9.47232E+11,"__v":0},
{"_id":{"$oid":"63adf70d3c5a6e0d3faf8e2b"},"userId":"1032714210133688380","birthday":9.689148E+11,"__v":0},
{"_id":{"$oid":"63afd9993c5a6e0d3faf8e40"},"userId":"1056146445889183796","birthday":9.614844E+11,"__v":0},
{"_id":{"$oid":"63b1b7a23c5a6e0d3faf8e5a"},"userId":"1047295595074830336","birthday":9.500832E+11,"__v":0},
{"_id":{"$oid":"63b873bb3c5a6e0d3faf8e96"},"userId":"1003207378184982538","birthday":9.47232E+11,"__v":0},
{"_id":{"$oid":"63b94f6b3c5a6e0d3faf8ea1"},"userId":"1043104995194716170","birthday":9.743616E+11,"__v":0},
{"_id":{"$oid":"63c1afb73c5a6e0d3faf8ee1"},"userId":"923369644520247296","birthday":9.606204E+11,"__v":0},
{"_id":{"$oid":"63c1ccf53c5a6e0d3faf8ee5"},"userId":"1032574355516969002","birthday":9.540576E+11,"__v":0},
{"_id":{"$oid":"63c5e83f3c5a6e0d3faf8f14"},"userId":"1002390101227208714","birthday":9.584604E+11,"__v":0},
{"_id":{"$oid":"63c8b0693c5a6e0d3faf8f2b"},"userId":"1058975267713982565","birthday":9.531072E+11,"__v":0},
{"_id":{"$oid":"63ca54d63c5a6e0d3faf8f3a"},"userId":"980743925180530708","birthday":9.624348E+11,"__v":0},
{"_id":{"$oid":"63d1b16b3c5a6e0d3faf8f69"},"userId":"795057210940981318","birthday":9.499104E+11,"__v":0},
{"_id":{"$oid":"63d1e2d43c5a6e0d3faf8f6f"},"userId":"812841462248767499","birthday":9.724572E+11,"__v":0},
{"_id":{"$oid":"63d8dcc83c5a6e0d3faf8fa6"},"userId":"1037356055933497364","birthday":9.772992E+11,"__v":0},
{"_id":{"$oid":"63dd404c3c5a6e0d3faf8fc5"},"userId":"587719422643535873","birthday":9.659772E+11,"__v":0},
{"_id":{"$oid":"63df4c7d3c5a6e0d3faf8fe5"},"userId":"1066473378816467054","birthday":9.732384E+11,"__v":0},
{"_id":{"$oid":"63e421953c5a6e0d3faf9007"},"userId":"1027400955249041480","birthday":9.639036E+11,"__v":0},
{"_id":{"$oid":"63e4219a3c5a6e0d3faf900b"},"userId":"1043228779876716584","birthday":9.564732E+11,"__v":0},
{"_id":{"$oid":"63e6a90b3c5a6e0d3faf901e"},"userId":"858058607731146762","birthday":9.513792E+11,"__v":0},
{"_id":{"$oid":"63fa823aa7dbdfe5f54def3d"},"userId":"838060361894068264","birthday":9.548316E+11,"__v":0},
{"_id":{"$oid":"63ff7aa4e89252f1e5413127"},"userId":"971147645475258368","birthday":9.539712E+11,"__v":0},
{"_id":{"$oid":"63ffeb11e89252f1e541312d"},"userId":"936056120420761611","birthday":9.550908E+11,"__v":0},
{"_id":{"$oid":"64031954e89252f1e5413145"},"userId":"1041288670570889297","birthday":9.671868E+11,"__v":0},
{"_id":{"$oid":"64077b2831c81c55c6db1a75"},"userId":"952332159979503677","birthday":9.573372E+11,"__v":0},
{"_id":{"$oid":"6407ed5931c81c55c6db1a80"},"userId":"708153903425912952","birthday":9.77472E+11,"__v":0},
{"_id":{"$oid":"6407ed6e31c81c55c6db1a84"},"userId":"345756943450898433","birthday":9.702972E+11,"__v":0},
{"_id":{"$oid":"6407ed7d31c81c55c6db1a88"},"userId":"692453290427941005","birthday":9.737568E+11,"__v":0},
{"_id":{"$oid":"6407ed8931c81c55c6db1a8c"},"userId":"654102447320596510","birthday":9.629532E+11,"__v":0},
{"_id":{"$oid":"6407ed9531c81c55c6db1a90"},"userId":"722597580214632461","birthday":9.533664E+11,"__v":0},
{"_id":{"$oid":"6407edef31c81c55c6db1a94"},"userId":"476907947277025295","birthday":9.555228E+11,"__v":0},
{"_id":{"$oid":"6407ee0531c81c55c6db1a98"},"userId":"349328356338302976","birthday":9.561276E+11,"__v":0},
{"_id":{"$oid":"6407ee1331c81c55c6db1a9c"},"userId":"443126436157456394","birthday":9.530208E+11,"__v":0},
{"_id":{"$oid":"6407ee9d31c81c55c6db1aa0"},"userId":"758788702389403648","birthday":9.556092E+11,"__v":0},
{"_id":{"$oid":"6407eec431c81c55c6db1aa4"},"userId":"478752726612967435","birthday":9.578556E+11,"__v":0},
{"_id":{"$oid":"6407efe531c81c55c6db1aa8"},"userId":"836048120718688266","birthday":9.590652E+11,"__v":0},
{"_id":{"$oid":"6408978331c81c55c6db1aae"},"userId":"796537667218178119","birthday":9.667548E+11,"__v":0},
{"_id":{"$oid":"640ceae631c81c55c6db1acf"},"userId":"1083891438393233568","birthday":9.63126E+11,"__v":0},
{"_id":{"$oid":"640dfb1231c81c55c6db1adf"},"userId":"1048169103950614638","birthday":9.733248E+11,"__v":0},
{"_id":{"$oid":"640e382e31c81c55c6db1ae7"},"userId":"1081648317823463488","birthday":9.712476E+11,"__v":0},
{"_id":{"$oid":"641125a331c81c55c6db1afa"},"userId":"1065061496842879017","birthday":9.685692E+11,"__v":0},
{"_id":{"$oid":"6415f5bd631db651639cec55"},"userId":"804529541716508694","birthday":9.542304E+11,"__v":0},
{"_id":{"$oid":"641b272f631db651639cec71"},"userId":"1053901402772078652","birthday":9.682236E+11,"__v":0},
{"_id":{"$oid":"641cffcc631db651639cec81"},"userId":"1000032163414163476","birthday":9.573372E+11,"__v":0},
{"_id":{"$oid":"641f29f7631db651639cec98"},"userId":"1085892495470239834","birthday":9.656316E+11,"__v":0},
{"_id":{"$oid":"6424a422631db651639cecbb"},"userId":"893146906329501756","birthday":9.693468E+11,"__v":0},
{"_id":{"$oid":"6428c359631db651639cecd1"},"userId":"1013149541035425878","birthday":9.782496E+11,"__v":0},
{"_id":{"$oid":"6429e5a4631db651639cecdd"},"userId":"1006707714803650590","birthday":9.638172E+11,"__v":0},
{"_id":{"$oid":"6431b78c229464ee5ecebd16"},"userId":"1064576791051776010","birthday":9.69606E+11,"__v":0},
{"_id":{"$oid":"64325b7a229464ee5ecebd1d"},"userId":"1064167725800370176","birthday":9.467136E+11,"__v":0},
{"_id":{"$oid":"6434f59c229464ee5ecebd2f"},"userId":"914058373857706034","birthday":9.633852E+11,"__v":0},
{"_id":{"$oid":"64394293229464ee5ecebd54"},"userId":"1096401339996717108","birthday":9.772128E+11,"__v":0},
{"_id":{"$oid":"6439c8b9229464ee5ecebd5a"},"userId":"1073783318799196301","birthday":9.5328E+11,"__v":0},
{"_id":{"$oid":"643ed185229464ee5ecebd82"},"userId":"1050421303775080469","birthday":9.561276E+11,"__v":0},
{"_id":{"$oid":"6440ae85229464ee5ecebd8f"},"userId":"816328583810383922","birthday":9.572508E+11,"__v":0},
{"_id":{"$oid":"6442980b229464ee5ecebd99"},"userId":"1068941746026852423","birthday":9.629532E+11,"__v":0},
{"_id":{"$oid":"644402eff59be8a132d3cd20"},"userId":"1083443026149519411","birthday":9.76608E+11,"__v":0},
{"_id":{"$oid":"64457b16f59be8a132d3cd2d"},"userId":"1066726987139391579","birthday":9.7263E+11,"__v":0},
{"_id":{"$oid":"64458773f59be8a132d3cd31"},"userId":"812796288666959892","birthday":9.509472E+11,"__v":0},
{"_id":{"$oid":"6445d3dcf59be8a132d3cd37"},"userId":"975232564270886983","birthday":9.711612E+11,"__v":0},
{"_id":{"$oid":"6446f8bff59be8a132d3cd3f"},"userId":"922956516624052285","birthday":9.646812E+11,"__v":0},
{"_id":{"$oid":"64473afdf59be8a132d3cd44"},"userId":"996471123623546890","birthday":9.544032E+11,"__v":0},
{"_id":{"$oid":"644c5f50f59be8a132d3cd64"},"userId":"697750438887555072","birthday":9.569916E+11,"__v":0},
{"_id":{"$oid":"644eebbdf59be8a132d3cd76"},"userId":"1100069245565546506","birthday":9.664956E+11,"__v":0},
{"_id":{"$oid":"645098b0f59be8a132d3cd7f"},"userId":"1073834635320107059","birthday":9.782496E+11,"__v":0},
{"_id":{"$oid":"6452a0fcf59be8a132d3cd8e"},"userId":"936771696575262751","birthday":9.642492E+11,"__v":0},
{"_id":{"$oid":"64540800f59be8a132d3cd9d"},"userId":"1089986976888918158","birthday":9.612252E+11,"__v":0},
{"_id":{"$oid":"645e6e37f59be8a132d3cdd7"},"userId":"959020201540718596","birthday":9.632124E+11,"__v":0},
{"_id":{"$oid":"646b78001ba9f3e4174c5996"},"userId":"886181760587665468","birthday":9.645084E+11,"__v":0},
{"_id":{"$oid":"646f01da1ba9f3e4174c59a1"},"userId":"1092800350383255612","birthday":9.55782E+11,"__v":0},
{"_id":{"$oid":"646f44211ba9f3e4174c59a6"},"userId":"1079749357789786193","birthday":9.64854E+11,"__v":0},
{"_id":{"$oid":"646f8ee21ba9f3e4174c59ac"},"userId":"993247623265914880","birthday":9.658044E+11,"__v":0},
{"_id":{"$oid":"64730d361ba9f3e4174c59c3"},"userId":"1083189908992163871","birthday":9.715068E+11,"__v":0},
{"_id":{"$oid":"64731d991ba9f3e4174c59c8"},"userId":"882843628907671562","birthday":9.539712E+11,"__v":0},
{"_id":{"$oid":"6476279c1ba9f3e4174c59d9"},"userId":"736566913597177907","birthday":9.638172E+11,"__v":0},
{"_id":{"$oid":"647721f71ba9f3e4174c59e2"},"userId":"1099193351531671659","birthday":9.619164E+11,"__v":0},
{"_id":{"$oid":"647a40c01ba9f3e4174c59f9"},"userId":"1111663268134662224","birthday":9.538848E+11,"__v":0},
{"_id":{"$oid":"647cd37d1ba9f3e4174c5a0d"},"userId":"892147474091888680","birthday":9.76176E+11,"__v":0},
{"_id":{"$oid":"6487d7fc1ba9f3e4174c5a31"},"userId":"986427592972308490","birthday":9.654588E+11,"__v":0},
{"_id":{"$oid":"648856ce1ba9f3e4174c5a37"},"userId":"160794456004624385","birthday":9.696924E+11,"__v":0},
{"_id":{"$oid":"64895cfa1ba9f3e4174c5a3d"},"userId":"924667929142919209","birthday":9.734112E+11,"__v":0},
{"_id":{"$oid":"648f6de41ba9f3e4174c5a5a"},"userId":"1097067552926076989","birthday":9.709884E+11,"__v":0},
{"_id":{"$oid":"6495d3a51ba9f3e4174c5a7f"},"userId":"1121177427327058010","birthday":9.720252E+11,"__v":0},
{"_id":{"$oid":"649935fb1ba9f3e4174c5a90"},"userId":"1122221632916815882","birthday":9.702108E+11,"__v":0},
{"_id":{"$oid":"649a10b91ba9f3e4174c5a99"},"userId":"893610763137200148","birthday":9.620028E+11,"__v":0},
{"_id":{"$oid":"649adb1b1ba9f3e4174c5aa0"},"userId":"925962397926182942","birthday":9.484416E+11,"__v":0},
{"_id":{"$oid":"649e5a75321252ba093ad0af"},"userId":"792776499777896460","birthday":9.503424E+11,"__v":0},
{"_id":{"$oid":"649f1431321252ba093ad0b9"},"userId":"1124390303269408889","birthday":9.74016E+11,"__v":0},
{"_id":{"$oid":"64a45e21321252ba093ad0d7"},"userId":"1124127967208018022","birthday":9.632124E+11,"__v":0},
{"_id":{"$oid":"64a483c0321252ba093ad0db"},"userId":"793268834739159060","birthday":9.74448E+11,"__v":0},
{"_id":{"$oid":"64a79ed9321252ba093ad0ef"},"userId":"946646420834906114","birthday":9.715932E+11,"__v":0},
{"_id":{"$oid":"64ad88b5321252ba093ad10f"},"userId":"948342978169151488","birthday":9.633852E+11,"__v":0},
{"_id":{"$oid":"64adb7d5321252ba093ad114"},"userId":"700324066128822334","birthday":9.504288E+11,"__v":0},
{"_id":{"$oid":"64adc26d321252ba093ad118"},"userId":"1076338388547928064","birthday":9.550044E+11,"__v":0},
{"_id":{"$oid":"64b31030321252ba093ad136"},"userId":"1013593008232476723","birthday":9.575964E+11,"__v":0},
{"_id":{"$oid":"64b829fe321252ba093ad15d"},"userId":"1124112953550053378","birthday":9.651132E+11,"__v":0},
{"_id":{"$oid":"64bce53309fa233882a3a7e2"},"userId":"1048569035022934036","birthday":9.742752E+11,"__v":0},
{"_id":{"$oid":"64c90b1d09fa233882a3a821"},"userId":"1129738558324883587","birthday":9.719388E+11,"__v":0},
{"_id":{"$oid":"64cfe93609fa233882a3a848"},"userId":"824775420070199346","birthday":9.758304E+11,"__v":0},
{"_id":{"$oid":"64d0414f09fa233882a3a84f"},"userId":"1044373961502367754","birthday":9.646812E+11,"__v":0},
{"_id":{"$oid":"64d15355c60c340085bb3de2"},"userId":"971870039772901436","birthday":9.673596E+11,"__v":0},
{"_id":{"$oid":"64d15a31c60c340085bb3de7"},"userId":"982494441766010930","birthday":9.672732E+11,"__v":0},
{"_id":{"$oid":"64d1a84cc60c340085bb3deb"},"userId":"958360658238382112","birthday":9.473184E+11,"__v":0},
{"_id":{"$oid":"64d28629c60c340085bb3df3"},"userId":"1029865425397354536","birthday":9.76176E+11,"__v":0},
{"_id":{"$oid":"64d3862dc60c340085bb3df8"},"userId":"966845743635787846","birthday":9.51984E+11,"__v":0},
{"_id":{"$oid":"64d4d5abc60c340085bb3e0c"},"userId":"1015670876827553792","birthday":9.494784E+11,"__v":0},
{"_id":{"$oid":"64dceda5243eff166d5301e8"},"userId":"1052253666414972938","birthday":9.725436E+11,"__v":0},
{"_id":{"$oid":"64e23d14243eff166d530211"},"userId":"1088634101977845770","birthday":9.499968E+11,"__v":0},
{"_id":{"$oid":"64e45f90aa8d00e46554b876"},"userId":"1107924271386333314","birthday":9.52848E+11,"__v":0},
{"_id":{"$oid":"64e464c1aa8d00e46554b87a"},"userId":"1099709536585138217","birthday":9.664956E+11,"__v":0},
{"_id":{"$oid":"64ea810799afd89b04a0a9a4"},"userId":"1089269275040174080","birthday":9.539712E+11,"__v":0},
{"_id":{"$oid":"64eab16099afd89b04a0a9a9"},"userId":"1134924437876129943","birthday":9.732384E+11,"__v":0},
{"_id":{"$oid":"64ef721c99afd89b04a0a9c4"},"userId":"974006072497020928","birthday":9.736704E+11,"__v":0},
{"_id":{"$oid":"64ef83b899afd89b04a0a9c8"},"userId":"1145995037423968386","birthday":9.564732E+11,"__v":0},
{"_id":{"$oid":"64f20ded99afd89b04a0a9d7"},"userId":"1124005776114061393","birthday":9.755712E+11,"__v":0},
{"_id":{"$oid":"64f90c2299afd89b04a0a9f8"},"userId":"1074110463111090256","birthday":9.676188E+11,"__v":0},
{"_id":{"$oid":"64fe6693230ff8808addf13a"},"userId":"1024785963756560394","birthday":9.559548E+11,"__v":0},
{"_id":{"$oid":"6510e8f9e5ad402eb45bf867"},"userId":"931805733807329331","birthday":9.772128E+11,"__v":0},
{"_id":{"$oid":"6510e996e5ad402eb45bf86b"},"userId":"747208441155813386","birthday":9.514656E+11,"__v":0},
{"_id":{"$oid":"651b90e6e5ad402eb45bf899"},"userId":"1112116656794255413","birthday":9.555228E+11,"__v":0},
{"_id":{"$oid":"651e62d6e5ad402eb45bf8ab"},"userId":"1155432498612932669","birthday":9.663228E+11,"__v":0},
{"_id":{"$oid":"65218f58e5ad402eb45bf8c5"},"userId":"975433081060216843","birthday":9.625212E+11,"__v":0},
{"_id":{"$oid":"6522d2afe5ad402eb45bf8d1"},"userId":"1108729730758344704","birthday":9.480096E+11,"__v":0},
{"_id":{"$oid":"65238bace5ad402eb45bf8d7"},"userId":"992644193849716766","birthday":9.591516E+11,"__v":0},
{"_id":{"$oid":"65281ad2e5ad402eb45bf8f1"},"userId":"1161920897809129473","birthday":9.520704E+11,"__v":0},
{"_id":{"$oid":"653d99cbe5ad402eb45bf96f"},"userId":"1130970063512535050","birthday":9.544032E+11,"__v":0},
{"_id":{"$oid":"6543c965e5ad402eb45bf98f"},"userId":"970352694827036712","birthday":9.582876E+11,"__v":0},
{"_id":{"$oid":"65484252e5ad402eb45bf9ab"},"userId":"1086855977401340016","birthday":9.602748E+11,"__v":0},
{"_id":{"$oid":"65524f77e5ad402eb45bf9d2"},"userId":"966731025155768351","birthday":9.736704E+11,"__v":0},
{"_id":{"$oid":"65582568e5ad402eb45bf9eb"},"userId":"962594606879502356","birthday":9.47664E+11,"__v":0},
{"_id":{"$oid":"6564d690e5ad402eb45bfa3f"},"userId":"910537967473414174","birthday":9.61398E+11,"__v":0},
{"_id":{"$oid":"65673218e5ad402eb45bfa54"},"userId":"1142887920366260397","birthday":9.747936E+11,"__v":0},
{"_id":{"$oid":"656c813ee5ad402eb45bfa6e"},"userId":"1173460045850214472","birthday":9.61398E+11,"__v":0},
{"_id":{"$oid":"65765d36e5ad402eb45bfaa2"},"userId":"1113241385689161819","birthday":9.636444E+11,"__v":0},
{"_id":{"$oid":"657788e5e5ad402eb45bfaab"},"userId":"1048366042130415647","birthday":9.76608E+11,"__v":0},
{"_id":{"$oid":"65875660e5ad402eb45bfafa"},"userId":"1143672078898315364","birthday":9.501696E+11,"__v":0},
{"_id":{"$oid":"65af744d48b225ceab353024"},"userId":"882437468899659797","birthday":9.688284E+11,"__v":0},
{"_id":{"$oid":"65b3aea248b225ceab353042"},"userId":"1123479840306241546","birthday":9.54144E+11,"__v":0},
{"_id":{"$oid":"65c54e9348b225ceab3530b4"},"userId":"832341844695711804","birthday":9.548316E+11,"__v":0},
{"_id":{"$oid":"65c70d7b48b225ceab3530c3"},"userId":"1202014923660730442","birthday":9.501696E+11,"__v":0},
{"_id":{"$oid":"65d7f94348b225ceab35313f"},"userId":"1059181289560875038","birthday":9.702972E+11,"__v":0},
{"_id":{"$oid":"65df9a2248b225ceab35317e"},"userId":"1138800113318383688","birthday":9.747936E+11,"__v":0},
{"_id":{"$oid":"65e9e21948b225ceab3531d1"},"userId":"1214577235223257171","birthday":9.62262E+11,"__v":0},
{"_id":{"$oid":"65f0797048b225ceab3531fa"},"userId":"969983248384032890","birthday":9.526752E+11,"__v":0},
{"_id":{"$oid":"65fc545748b225ceab353236"},"userId":"1191169941299282048","birthday":9.53712E+11,"__v":0},
{"_id":{"$oid":"6601ad6248b225ceab353257"},"userId":"1221863172538110112","birthday":9.57078E+11,"__v":0},
{"_id":{"$oid":"6602c74d48b225ceab35325c"},"userId":"1173287717853986840","birthday":9.727164E+11,"__v":0},
{"_id":{"$oid":"66059bf148b225ceab35326d"},"userId":"791344978529091624","birthday":9.637308E+11,"__v":0},
{"_id":{"$oid":"660a22fc48b225ceab353291"},"userId":"1113449820229730314","birthday":9.582876E+11,"__v":0},
{"_id":{"$oid":"6617aaab81e34be8b2892c18"},"userId":"1072593794123452567","birthday":9.499968E+11,"__v":0},
{"_id":{"$oid":"6619882b81e34be8b2892c23"},"userId":"965324768712736809","birthday":9.58806E+11,"__v":0},
{"_id":{"$oid":"66216f8681e34be8b2892c46"},"userId":"1152295675254550558","birthday":9.493056E+11,"__v":0},
{"_id":{"$oid":"66272f87083d1922b9fe5bc0"},"userId":"1158434359259430982","birthday":9.538848E+11,"__v":0},
{"_id":{"$oid":"6627991d083d1922b9fe5bc5"},"userId":"1129051334302236683","birthday":9.620028E+11,"__v":0},
{"_id":{"$oid":"6627dbdd083d1922b9fe5bcb"},"userId":"1222277835746574339","birthday":9.659772E+11,"__v":0},
{"_id":{"$oid":"663393c8083d1922b9fe5c05"},"userId":"245692567172284416","birthday":9.64422E+11,"__v":0},
{"_id":{"$oid":"663877e8083d1922b9fe5c27"},"userId":"1113794793588396115","birthday":9.666684E+11,"__v":0},
{"_id":{"$oid":"66388e36083d1922b9fe5c2b"},"userId":"998373604288892948","birthday":9.752256E+11,"__v":0},
{"_id":{"$oid":"663a5045083d1922b9fe5c36"},"userId":"1106896766873391185","birthday":9.547452E+11,"__v":0},
{"_id":{"$oid":"663eb69e083d1922b9fe5c4d"},"userId":"1209356657746771992","birthday":9.73152E+11,"__v":0},
{"_id":{"$oid":"66402948083d1922b9fe5c59"},"userId":"1085080386217967656","birthday":9.480096E+11,"__v":0},
{"_id":{"$oid":"66429b6e083d1922b9fe5c64"},"userId":"1089260336441471157","birthday":9.5967E+11,"__v":0},
{"_id":{"$oid":"6642d8fa083d1922b9fe5c69"},"userId":"1076529106793009182","birthday":9.508608E+11,"__v":0},
{"_id":{"$oid":"66441ff2083d1922b9fe5c71"},"userId":"947300599119085618","birthday":9.51552E+11,"__v":0},
{"_id":{"$oid":"66465cba083d1922b9fe5c88"},"userId":"1234432274670555207","birthday":9.76176E+11,"__v":0},
{"_id":{"$oid":"6648e652083d1922b9fe5c94"},"userId":"878870207182032906","birthday":9.627804E+11,"__v":0},
{"_id":{"$oid":"664bb922083d1922b9fe5caf"},"userId":"1117584772793897061","birthday":9.588924E+11,"__v":0},
{"_id":{"$oid":"664bbbc7083d1922b9fe5cb5"},"userId":"1071461349328175306","birthday":9.4896E+11,"__v":0},
{"_id":{"$oid":"664fc18f083d1922b9fe5ccf"},"userId":"1236077514103853089","birthday":9.523296E+11,"__v":0},
{"_id":{"$oid":"66549600083d1922b9fe5cf1"},"userId":"904008772446453781","birthday":9.581148E+11,"__v":0},
{"_id":{"$oid":"66550c29083d1922b9fe5cfa"},"userId":"1114332546755465288","birthday":9.780768E+11,"__v":0},
{"_id":{"$oid":"6657777a083d1922b9fe5d19"},"userId":"929654937565155359","birthday":9.710748E+11,"__v":0},
{"_id":{"$oid":"66578a3e083d1922b9fe5d1d"},"userId":"1236391959535550524","birthday":9.520704E+11,"__v":0},
{"_id":{"$oid":"665852a9083d1922b9fe5d2a"},"userId":"986011723448328284","birthday":9.645084E+11,"__v":0},
{"_id":{"$oid":"6658eee4083d1922b9fe5d38"},"userId":"1091538778444808202","birthday":9.671004E+11,"__v":0},
{"_id":{"$oid":"665a21c6083d1922b9fe5d48"},"userId":"1227230559751639115","birthday":9.718524E+11,"__v":0},
{"_id":{"$oid":"665b7754083d1922b9fe5d56"},"userId":"1084799653771481108","birthday":9.6831E+11,"__v":0},
{"_id":{"$oid":"665beb33083d1922b9fe5d62"},"userId":"1069059534435389460","birthday":9.599292E+11,"__v":0},
{"_id":{"$oid":"665c950c083d1922b9fe5d6a"},"userId":"582536628904394762","birthday":9.638172E+11,"__v":0},
{"_id":{"$oid":"665cc69c083d1922b9fe5d71"},"userId":"872829194210512987","birthday":9.6831E+11,"__v":0},
{"_id":{"$oid":"665cdda3083d1922b9fe5d76"},"userId":"1159667407841927188","birthday":9.483552E+11,"__v":0},
{"_id":{"$oid":"665d8821083d1922b9fe5d7d"},"userId":"1247109607864664156","birthday":9.494784E+11,"__v":0},
{"_id":{"$oid":"6660965c083d1922b9fe5da0"},"userId":"1238867447944056856","birthday":9.525024E+11,"__v":0},
{"_id":{"$oid":"6662edc1083d1922b9fe5db8"},"userId":"1181335824424509563","birthday":9.654588E+11,"__v":0},
{"_id":{"$oid":"6664027e083d1922b9fe5dc2"},"userId":"1169915475518558318","birthday":9.490464E+11,"__v":0},
{"_id":{"$oid":"6666ccbd083d1922b9fe5ddc"},"userId":"1236647018420502570","birthday":9.54144E+11,"__v":0},
{"_id":{"$oid":"66671dba083d1922b9fe5de1"},"userId":"961988895829012540","birthday":9.69174E+11,"__v":0},
{"_id":{"$oid":"666cdd90083d1922b9fe5e0c"},"userId":"1089619893902651502","birthday":9.508608E+11,"__v":0},
{"_id":{"$oid":"666dd4e0083d1922b9fe5e14"},"userId":"1162165396988764241","birthday":9.65718E+11,"__v":0},
{"_id":{"$oid":"666de816083d1922b9fe5e19"},"userId":"1147045357658853428","birthday":9.513792E+11,"__v":0},
{"_id":{"$oid":"666e4a43083d1922b9fe5e21"},"userId":"1212163717655961695","birthday":9.616572E+11,"__v":0},
{"_id":{"$oid":"66702757083d1922b9fe5e2d"},"userId":"778641350006013982","birthday":9.73152E+11,"__v":0},
{"_id":{"$oid":"6670518d083d1922b9fe5e32"},"userId":"949815669023703050","birthday":9.756576E+11,"__v":0},
{"_id":{"$oid":"66707294083d1922b9fe5e37"},"userId":"570621131652988953","birthday":9.779904E+11,"__v":0},
{"_id":{"$oid":"66748a22083d1922b9fe5e5b"},"userId":"1189442227827654677","birthday":9.572508E+11,"__v":0},
{"_id":{"$oid":"6676b191083d1922b9fe5e69"},"userId":"703924532959772682","birthday":9.5967E+11,"__v":0},
{"_id":{"$oid":"6679507b083d1922b9fe5e7b"},"userId":"988803994161905705","birthday":9.604476E+11,"__v":0},
{"_id":{"$oid":"6679fb40083d1922b9fe5e80"},"userId":"1201179652488699985","birthday":9.483552E+11,"__v":0},
{"_id":{"$oid":"667aa820083d1922b9fe5e88"},"userId":"808282311887159326","birthday":9.483552E+11,"__v":0},
{"_id":{"$oid":"667b42d6083d1922b9fe5e90"},"userId":"1224875525890375863","birthday":9.54144E+11,"__v":0},
{"_id":{"$oid":"667c55ba083d1922b9fe5e9b"},"userId":"1197350521116311742","birthday":9.479232E+11,"__v":0}]

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "naomis-moderation-bot",
"version": "1.3.0",
"description": "A public paid moderation bot for Discord.",
"main": "prod/index.js",
"scripts": {
"audit": "knip",
"prebuild": "rm -rf prod && prisma generate",
"build": "tsc",
"lint": "eslint src --max-warnings 0 && prettier src --check",
"start": "op run --env-file='./prod.env' -- node prod/index.js",
"test": "echo 'no tests yet'",
"scan": "SONAR_TOKEN='op://Environment Variables - Development/SonarCloud/mod-bot' op run -- sonar-scanner -Dsonar.organization=nhcarrigan -Dsonar.projectKey=nhcarrigan_mod-bot -Dsonar.sources=. -Dsonar.host.url=https://sonarcloud.io"
},
"repository": {
"type": "git",
"url": "git+https://github.com/nhcarrigan/mod-bot.git"
},
"author": "Naomi Carrigan",
"bugs": {
"url": "https://github.com/nhcarrigan/mod-bot/issues"
},
"engines": {
"node": "22",
"pnpm": "9"
},
"homepage": "https://github.com/nhcarrigan/mod-bot#readme",
"devDependencies": {
"@nhcarrigan/eslint-config": "3.2.0",
"@nhcarrigan/prettier-config": "3.2.0",
"@nhcarrigan/typescript-config": "3.0.0",
"@types/express": "4.17.21",
"@types/node-schedule": "2.1.7",
"eslint": "8.57.0",
"knip": "5.15.0",
"prettier": "3.2.5",
"prisma": "5.13.0",
"typescript": "5.4.5"
},
"dependencies": {
"@octokit/rest": "20.1.1",
"@prisma/client": "5.13.0",
"discord.js": "14.15.2",
"dotenv": "16.4.5",
"express": "4.19.2",
"node-html-to-image": "4.0.0",
"node-schedule": "2.1.1",
"prom-client": "15.1.3",
"winston": "3.13.0"
}
}

4783
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

101
prisma/schema.prisma Normal file
View File

@ -0,0 +1,101 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("MONGO_URI")
}
model cases {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
userId String
number Int
action String
reason String
evidence String[]
timestamp String
moderator String
@@unique([serverId, number])
@@index([serverId])
}
model levelRoles {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
level Int
roleId String
@@unique([serverId, level, roleId])
@@index([serverId], map: "serverId")
}
model levels {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
userId String
points Int @default(0)
level Int @default(0)
username String
avatar String
backgroundColour String @default("000000")
colour String @default("ffffff")
backgroundImage String @default("https://cdn.nhcarrigan.com/banner.png")
cooldown DateTime @default(now())
@@unique([serverId, userId], map: "serverId_userId")
}
model configs {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
inviteLink String @default("")
banAppealLink String @default("")
modLogChannel String @default("")
eventLogChannel String @default("")
messageReportChannel String @default("")
joinRole String @default("")
birthdayChannel String @default("")
@@unique([serverId], map: "serverId")
}
model entitlements {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
purchaserId String
notes String @default("")
@@unique([serverId])
}
model roles {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
roleId String
@@unique([serverId, roleId], map: "serverId_roleId")
}
model birthdays {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
userId String
birthday DateTime
@@unique([serverId, userId], map: "serverId_userId")
}
model security {
id String @id @default(auto()) @map("_id") @db.ObjectId
serverId String
lockDms Boolean @default(false)
lockInvites Boolean @default(false)
@@unique([serverId], map: "serverId")
}

13
prod.env Normal file
View File

@ -0,0 +1,13 @@
## Global Values
BOT_TOKEN="op://Environment Variables - Naomi/Mod Bot/token"
MONGO_URI="op://Environment Variables - Naomi/Mod Bot/mongo_uri"
DEBUG_HOOK="op://Environment Variables - Naomi/Mod Bot/webhook"
NODE_ENV="op://Environment Variables - Naomi/Mod Bot/environment"
LOG_HOOK="op://Environment Variables - Naomi/Mod Bot/webhook"
STAFF_GUILD="op://Environment Variables - Naomi/Mod Bot/home_server"
## Server Shit
GITHUB_WEBHOOK_SECRET="op://Environment Variables - Naomi/Mod Bot/github_webhook_secret"
PATREON_WEBHOOK_SECRET="op://Environment Variables - Naomi/Mod Bot/patreon_webhook_secret"
KOFI_WEBHOOK_SECRET="op://Environment Variables - Naomi/Mod Bot/kofi_webhook_secret"
GITHUB_TOKEN="op://Environment Variables - Naomi/Mod Bot/github_pat"

11
sample.env Normal file
View File

@ -0,0 +1,11 @@
## Global Values
BOT_TOKEN=-""
MONGO_URI=""
DEBUG_HOOK=""
NODE_ENV="development"
## Server Shit
GITHUB_WEBHOOK_SECRET=""
PATREON_WEBHOOK_SECRET=""
KOFI_WEBHOOK_SECRET=""
GITHUB_TOKEN=""

99
src/commands/ban.ts Normal file
View File

@ -0,0 +1,99 @@
import {
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
import { processModAction } from "../utils/processModAction";
export const ban: Command = {
data: new SlashCommandBuilder()
.setName("ban")
.setDescription("Ban a user from the server.")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to ban.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for banning.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
)
.addIntegerOption((option) =>
option
.setName("prune")
.setDescription("Number of days to prune messages.")
.setMinValue(0)
.setMaxValue(7)
)
.addStringOption((option) =>
option
.setName("evidence")
.setDescription(
"A link to the evidence for the ban. For multiple links, separate with a space."
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (
!(member as GuildMember).permissions.has(PermissionFlagsBits.BanMembers)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const reason = interaction.options.getString("reason", true);
const evidence =
interaction.options.getString("evidence")?.split(/\s+/) || [];
const prune = interaction.options.getInteger("prune") || 0;
const user = interaction.options.getUser("user", true);
const target =
guild.members.cache.get(user.id) ||
(await guild.members.fetch(user.id).catch(() => null));
if (target && isModerator(target)) {
await interaction.editReply({
content: "You cannot ban a moderator."
});
return;
}
await processModAction(
bot,
interaction,
guild,
user,
"ban",
reason,
evidence,
undefined,
undefined,
prune
);
} catch (err) {
const id = await errorHandler(bot, "ban command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

143
src/commands/birthday.ts Normal file
View File

@ -0,0 +1,143 @@
import { SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
/**
* Validates that the day provided is a valid day of the month.
*
* @param {string} month The month to validate.
* @param {number} day The day to validate.
* @returns {boolean} True if the day is within the month's range.
*/
const validateDate = (month: string, day: number): boolean => {
switch (month) {
case "Jan":
case "Mar":
case "May":
case "Jul":
case "Aug":
case "Oct":
case "Dec":
return day >= 1 && day <= 31;
case "Feb":
return day >= 1 && day <= 29;
case "Apr":
case "Jun":
case "Sep":
case "Nov":
return day >= 1 && day <= 30;
default:
return false;
}
};
export const birthday: Command = {
data: new SlashCommandBuilder()
.setName("birthday")
.setDescription("Set your birthday!")
.addStringOption((option) =>
option
.setName("month")
.setDescription("Your Birth Month")
.setRequired(true)
.setChoices(
{
name: "January",
value: "Jan"
},
{
name: "February",
value: "Feb"
},
{
name: "March",
value: "Mar"
},
{
name: "April",
value: "Apr"
},
{
name: "May",
value: "May"
},
{
name: "June",
value: "Jun"
},
{
name: "July",
value: "Jul"
},
{
name: "August",
value: "Aug"
},
{
name: "September",
value: "Sep"
},
{
name: "October",
value: "Oct"
},
{
name: "November",
value: "Nov"
},
{
name: "December",
value: "Dec"
}
)
)
.addIntegerOption((option) =>
option
.setName("day")
.setDescription("Your Birth Day (1-31)")
.setRequired(true)
.setMinValue(1)
.setMaxValue(31)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply();
const month = interaction.options.getString("month", true);
const day = interaction.options.getInteger("day", true);
if (!validateDate(month, day)) {
await interaction.editReply({
content: `${month} ${day} is not a valid date!`
});
return;
}
await bot.db.birthdays.upsert({
where: {
serverId_userId: {
serverId: interaction.guild.id,
userId: interaction.user.id
}
},
update: {
birthday: new Date(`${month}-${day}-2000`)
},
create: {
serverId: interaction.guild.id,
userId: interaction.user.id,
birthday: new Date(`${month}-${day}-2000`)
}
});
await interaction.editReply(
`Your birthday has been set to ${month}-${day}!`
);
} catch (err) {
const id = await errorHandler(bot, "birthday command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

90
src/commands/cases.ts Normal file
View File

@ -0,0 +1,90 @@
import { GuildMember, EmbedBuilder, SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import { customSubstring } from "../utils/customSubstring";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
export const cases: Command = {
data: new SlashCommandBuilder()
.setName("case")
.setDescription("View a specific moderation case.")
.addIntegerOption((option) =>
option
.setName("number")
.setDescription("The case number to view.")
.setRequired(true)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (!isModerator(member as GuildMember)) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const target = interaction.options.getUser("user", true);
const number = interaction.options.getInteger("number", true);
const requestedCase = await bot.db.cases.findFirst({
where: {
userId: target.id,
serverId: guild.id,
number
}
});
if (!requestedCase) {
await interaction.editReply({
content: "That user doesn't seem to have a moderation history yet."
});
return;
}
const viewEmbed = new EmbedBuilder();
viewEmbed.setTitle(
`Case ${requestedCase.number} - ${requestedCase.action}`
);
viewEmbed.setAuthor({
name: target.tag,
iconURL: target.displayAvatarURL()
});
viewEmbed.setDescription(customSubstring(requestedCase.reason, 4000));
viewEmbed.addFields(
{
name: "Evidence",
value:
customSubstring(requestedCase.evidence.join("\n"), 2000) ||
"No evidence provided"
},
{
name: "Date",
value: requestedCase.timestamp
},
{
name: "Moderator",
value: requestedCase.moderator
}
);
await interaction.editReply({
embeds: [viewEmbed]
});
} catch (err) {
const id = await errorHandler(bot, "cases command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

161
src/commands/config.ts Normal file
View File

@ -0,0 +1,161 @@
import {
SlashCommandBuilder,
SlashCommandSubcommandBuilder,
Guild,
GuildMember,
PermissionFlagsBits
} from "discord.js";
import { logChannelChoices } from "../config/LogChannelChoices";
import { Command } from "../interfaces/Command";
import { CommandHandler } from "../interfaces/CommandHandler";
import { getConfig } from "../modules/data/getConfig";
import { handleAppealLink } from "../modules/subcommands/config/handleAppealLink";
import { handleBirthdayChannel } from "../modules/subcommands/config/handleBirthdayChannel";
import { handleInviteLink } from "../modules/subcommands/config/handleInviteLink";
import { handleJoinRole } from "../modules/subcommands/config/handleJoinRole";
import { handleList } from "../modules/subcommands/config/handleList";
import { handleLogging } from "../modules/subcommands/config/handleLogging";
import { handleRole } from "../modules/subcommands/config/handleRole";
import { errorHandler } from "../utils/errorHandler";
const handlers: { [key: string]: CommandHandler } = {
list: handleList,
"invite-link": handleInviteLink,
"appeal-link": handleAppealLink,
logging: handleLogging,
role: handleRole,
"join-role": handleJoinRole,
"birthday-channel": handleBirthdayChannel
};
export const config: Command = {
data: new SlashCommandBuilder()
.setName("config")
.setDescription("Modify the config settings.")
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("list")
.setDescription("List your server's current config settings")
)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("invite-link")
.setDescription(
"Set the link to be sent to someone to rejoin the server after they are kicked."
)
.addStringOption((option) =>
option
.setRequired(true)
.setName("link")
.setDescription("The invite link to send.")
)
)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("appeal-link")
.setDescription(
"Set the link to be sent to someone when they are banned to appeal the decision."
)
.addStringOption((option) =>
option
.setRequired(true)
.setName("link")
.setDescription("The appeal link to send.")
)
)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("logging")
.setDescription("Configure a logging channel.")
.addStringOption((option) =>
option
.setName("log-type")
.setDescription("The type of log to configure.")
.addChoices(...logChannelChoices)
.setRequired(true)
)
.addChannelOption((option) =>
option
.setName("channel")
.setDescription("The channel to log to.")
.setRequired(true)
)
)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("roles")
.setDescription("Toggle roles to be self-assignable by users.")
.addRoleOption((o) =>
o
.setName("role")
.setDescription("The role to toggle.")
.setRequired(true)
)
)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("join-role")
.setDescription("Configure a role to be assigned when a member joins.")
.addRoleOption((o) =>
o
.setName("role")
.setDescription("The role to assign.")
.setRequired(true)
)
)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("birthday-channel")
.setDescription(
"Configure a channel where members can be wished a happy birthday."
)
.addChannelOption((o) =>
o
.setName("channel")
.setDescription("The channel to send birthday messages in.")
.setRequired(true)
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const member = interaction.member as GuildMember;
const guild = interaction.guild as Guild;
if (!member || !guild) {
await interaction.editReply({
content: "You must be in a server to use this command."
});
return;
}
const config = await getConfig(bot, guild.id);
if (!member.permissions.has(PermissionFlagsBits.ManageGuild)) {
await interaction.editReply({
content: "You do not have permission to use this command."
});
return;
}
const subcommand = interaction.options.getSubcommand();
const handler = handlers[subcommand];
if (handler) {
await handler(bot, interaction, config);
return;
}
await interaction.editReply({
content: "This is an invalid subcommand. Please contact Naomi."
});
} catch (err) {
const id = await errorHandler(bot, "config command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

88
src/commands/help.ts Normal file
View File

@ -0,0 +1,88 @@
import { execSync } from "child_process";
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
EmbedBuilder,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { checkEntitledGuild } from "../utils/checkEntitledGuild";
import { errorHandler } from "../utils/errorHandler";
export const help: Command = {
data: new SlashCommandBuilder()
.setName("help")
.setDMPermission(false)
.setDescription("Get help with the bot."),
run: async (bot, interaction) => {
try {
await interaction.deferReply();
const version = process.env.npm_package_version;
const commit = execSync("git rev-parse HEAD").toString().trim();
const subscribed = await checkEntitledGuild(bot, interaction.guild);
const servers = bot.guilds.cache.size;
const members = bot.guilds.cache.reduce(
(sum, guild) => sum + guild.memberCount,
0
);
const embed = new EmbedBuilder();
embed.setTitle("Naomi's Moderation Bot");
embed.setDescription(
"This is a highly focused moderation bot designed to deliver the best experience when it comes to keeping your community safe and welcoming. To ensure we are able to deliver the features our users require, this bot is only available through a $5/month subscription."
);
embed.addFields(
{
name: "Version",
value: version ? `v${version}` : "unable to parse version",
inline: true
},
{
name: "Current Commit",
value: `[${commit.slice(
0,
7
)}](https://github.com/nhcarrigan/mod-bot/commit/${commit})`,
inline: true
},
{
name: "Is this server subscribed?",
value: subscribed ? "Yes!" : "No :c",
inline: true
},
{
name: "Details",
value: `Currently protecting ${servers} servers and watching over ${members} users.`
}
);
const supportButton = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setURL("https://chat.naomi.lgbt")
.setLabel("Join our Support Server");
const subscribeButton = new ButtonBuilder()
.setStyle(ButtonStyle.Link)
.setURL("https://docs.nhcarrigan.com/#/donate")
.setLabel("Subscribe for Access");
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
supportButton,
subscribeButton
);
await interaction.editReply({
embeds: [embed],
components: [row]
});
} catch (err) {
const id = await errorHandler(bot, "help command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

198
src/commands/history.ts Normal file
View File

@ -0,0 +1,198 @@
import {
GuildMember,
EmbedBuilder,
SlashCommandBuilder,
ButtonBuilder,
ActionRowBuilder,
ComponentType,
ButtonStyle
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { getNextIndex, getPreviousIndex } from "../utils/getArrayIndex";
import { isModerator } from "../utils/isModerator";
export const history: Command = {
data: new SlashCommandBuilder()
.setName("history")
.setDescription("View a user's history.")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to view the history for.")
.setRequired(true)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (!isModerator(member as GuildMember)) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const target = interaction.options.getUser("user", true);
const cases = await bot.db.cases.findMany({
where: {
userId: target.id,
serverId: guild.id
}
});
if (!cases.length) {
await interaction.editReply({
content: "That user is squeaky clean!"
});
return;
}
const caseNumbers = cases
.filter((c) => c.action !== "note")
.map((c) => `**#${c.number} - ${c.action}**`);
const noteNumbers = cases
.filter((c) => c.action === "note")
.map((c) => `**#${c.number} - ${c.action}**`);
``;
const historyEmbed = new EmbedBuilder();
historyEmbed.setTitle(`${target.tag}'s history`);
historyEmbed.addFields(
{
name: "Bans",
value: String(cases.filter((c) => c.action === "ban").length) || "0",
inline: true
},
{
name: "Unbans",
value:
String(cases.filter((c) => c.action === "unban").length) || "0",
inline: true
},
{
name: "Softbans",
value:
String(cases.filter((c) => c.action === "softban").length) || "0",
inline: true
},
{
name: "Kicks",
value: String(cases.filter((c) => c.action === "kick").length) || "0",
inline: true
},
{
name: "Mutes",
value: String(cases.filter((c) => c.action === "mute").length) || "0",
inline: true
},
{
name: "Unmutes",
value:
String(cases.filter((c) => c.action === "unmute").length) || "0",
inline: true
},
{
name: "Warns",
value: String(cases.filter((c) => c.action === "warn").length) || "0",
inline: true
},
{
name: "Notes",
value: String(cases.filter((c) => c.action === "note").length) || "0",
inline: true
}
);
const embeds = [historyEmbed];
if (caseNumbers.length) {
const manualEmbed = new EmbedBuilder()
.setTitle("Manual Cases")
.setDescription(caseNumbers.join(", "));
embeds.push(manualEmbed);
}
if (noteNumbers.length) {
const noteEmbed = new EmbedBuilder()
.setTitle("Notes")
.setDescription(noteNumbers.join(", "));
embeds.push(noteEmbed);
}
let index = 0;
const nextButton = new ButtonBuilder()
.setCustomId("next")
.setStyle(ButtonStyle.Primary)
.setLabel(
embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed."
)
.setEmoji("▶️");
const prevButton = new ButtonBuilder()
.setCustomId("prev")
.setStyle(ButtonStyle.Primary)
.setLabel(
embeds[getPreviousIndex(embeds, index)]?.data.title ||
"Unknown embed."
)
.setEmoji("◀️");
const initialRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
prevButton,
nextButton
);
const response = await interaction.editReply({
embeds: [embeds[index] as EmbedBuilder],
components: [initialRow]
});
const collector =
response.createMessageComponentCollector<ComponentType.Button>({
time: 1000 * 60 * 5
});
collector.on("collect", async (i) => {
await i.deferUpdate();
index =
i.customId === "next"
? getNextIndex(embeds, index)
: getPreviousIndex(embeds, index);
prevButton.setLabel(
embeds[getPreviousIndex(embeds, index)]?.data.title ||
"Unknown embed."
);
nextButton.setLabel(
embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed."
);
const newRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
prevButton,
nextButton
);
await i.editReply({
embeds: [embeds[index] as EmbedBuilder],
components: [newRow]
});
});
collector.on("end", async () => {
await interaction.editReply({
components: []
});
});
} catch (err) {
const id = await errorHandler(bot, "history command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

145
src/commands/kick.ts Normal file
View File

@ -0,0 +1,145 @@
import {
GuildMember,
Message,
ActionRowBuilder,
ButtonBuilder,
PermissionFlagsBits,
ButtonStyle,
ComponentType,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
import { processModAction } from "../utils/processModAction";
export const kick: Command = {
data: new SlashCommandBuilder()
.setName("kick")
.setDescription("Kick a user from the server.")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to kick.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for kicking.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
)
.addStringOption((option) =>
option
.setName("evidence")
.setDescription(
"A link to the evidence for the kick. For multiple links, separate with a space."
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (
!(member as GuildMember).permissions.has(
PermissionFlagsBits.KickMembers
)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const reason = interaction.options.getString("reason", true);
const evidence =
interaction.options.getString("evidence")?.split(/\s+/) || [];
const user = interaction.options.getUser("user", true);
const target =
guild.members.cache.get(user.id) ||
(await guild.members.fetch(user.id).catch(() => null));
if (!target) {
await interaction.editReply({
content: `${user.tag} is not in this server and thus cannot be kicked.`
});
return;
}
if (isModerator(target)) {
await interaction.editReply({
content: "You cannot kick a moderator."
});
return;
}
const yes = new ButtonBuilder()
.setCustomId("confirm")
.setLabel("Confirm")
.setStyle(ButtonStyle.Success);
const no = new ButtonBuilder()
.setCustomId("cancel")
.setLabel("Cancel")
.setStyle(ButtonStyle.Danger);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(yes, no);
const response = (await interaction.editReply({
content: `Are you sure you want to kick <@!${user.id}>?`,
components: [row]
})) as Message;
const collector =
response.createMessageComponentCollector<ComponentType.Button>({
filter: (click) => click.user.id === interaction.user.id,
time: 10000,
max: 1
});
collector.on("end", async (clicks) => {
const choice = clicks.first()?.customId;
if (!clicks || clicks.size <= 0 || !choice) {
await interaction.editReply({
content: "This command has timed out.",
components: []
});
return;
}
if (choice === "confirm") {
await processModAction(
bot,
interaction,
guild,
user,
"kick",
reason,
evidence
);
return;
}
if (choice === "cancel") {
interaction.editReply({
content: "Kick cancelled.",
components: []
});
}
});
} catch (err) {
const id = await errorHandler(bot, "kick command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

156
src/commands/leaderboard.ts Normal file
View File

@ -0,0 +1,156 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { generateLeaderboardImage } from "../modules/commands/generateProfileImage";
import { errorHandler } from "../utils/errorHandler";
export const leaderboard: Command = {
data: new SlashCommandBuilder()
.setName("leaderboard")
.setDescription("See the levels for this community.")
.setDMPermission(false),
run: async (bot, interaction) => {
try {
await interaction.deferReply();
const levels = await bot.db.levels.findMany({
where: {
serverId: interaction.guild.id
},
orderBy: {
points: "desc"
}
});
const mapped = levels.map((user, index) => ({
...user,
index: index + 1
}));
let page = 1;
const lastPage = Math.ceil(mapped.length / 10);
const pageBack = new ButtonBuilder()
.setCustomId("prev")
.setDisabled(true)
.setLabel("◀")
.setStyle(ButtonStyle.Primary);
const pageForward = new ButtonBuilder()
.setCustomId("next")
.setLabel("▶")
.setStyle(ButtonStyle.Primary);
if (page <= 1) {
pageBack.setDisabled(true);
} else {
pageBack.setDisabled(false);
}
if (page >= lastPage) {
pageForward.setDisabled(true);
} else {
pageForward.setDisabled(false);
}
const attachment = await generateLeaderboardImage(
bot,
mapped.slice(page * 10 - 10, page * 10)
);
if (!attachment) {
await interaction.editReply({
content: "Failed to load leaderboard image.",
files: [],
components: []
});
return;
}
const sent = await interaction.editReply({
files: [attachment],
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
pageBack,
pageForward
)
]
});
const clickyClick =
sent.createMessageComponentCollector<ComponentType.Button>({
time: 300000,
filter: (click) => click.user.id === interaction.user.id
});
clickyClick.on("collect", async (click) => {
await click.deferUpdate();
if (click.customId === "prev") {
page--;
}
if (click.customId === "next") {
page++;
}
if (page <= 1) {
pageBack.setDisabled(true);
} else {
pageBack.setDisabled(false);
}
if (page >= lastPage) {
pageForward.setDisabled(true);
} else {
pageForward.setDisabled(false);
}
const attachment = await generateLeaderboardImage(
bot,
mapped.slice(page * 10 - 10, page * 10)
);
if (!attachment) {
await interaction.editReply({
content: "Failed to load leaderboard image.",
files: [],
components: []
});
return;
}
await interaction.editReply({
files: [attachment],
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
pageBack,
pageForward
)
]
});
});
clickyClick.on("end", async () => {
pageBack.setDisabled(true);
pageForward.setDisabled(true);
await interaction.editReply({
components: [
new ActionRowBuilder<ButtonBuilder>().addComponents(
pageBack,
pageForward
)
]
});
});
} catch (err) {
const id = await errorHandler(bot, "leaderboard subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

106
src/commands/levelRoles.ts Normal file
View File

@ -0,0 +1,106 @@
import {
PermissionFlagsBits,
SlashCommandBuilder,
SlashCommandSubcommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
export const levelRoles: Command = {
data: new SlashCommandBuilder()
.setName("level-role")
.setDescription("Manage level roles.")
.setDMPermission(false)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("create")
.setDescription("Create a new level role.")
.addRoleOption((o) =>
o
.setName("role")
.setDescription("The role to assign")
.setRequired(true)
)
.addIntegerOption((o) =>
o
.setName("level")
.setDescription("The level at which to assign the role.")
.setRequired(true)
.setMinValue(1)
.setMaxValue(1000)
)
)
.addSubcommand(
new SlashCommandSubcommandBuilder()
.setName("delete")
.setDescription("Delete a level role.")
.addRoleOption((o) =>
o
.setName("role")
.setDescription("The role to remove")
.setRequired(true)
)
.addIntegerOption((o) =>
o
.setName("level")
.setDescription("The level at which the role was being assigned")
.setRequired(true)
.setMinValue(1)
.setMaxValue(1000)
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member } = interaction;
if (!member.permissions.has(PermissionFlagsBits.ManageRoles)) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const role = interaction.options.getRole("role", true);
const level = interaction.options.getInteger("level", true);
const action = interaction.options.getSubcommand(true);
let success = false;
if (action === "create") {
success = !!(await bot.db.levelRoles
.create({
data: {
serverId: interaction.guild.id,
roleId: role.id,
level
}
})
.catch(() => null));
}
if (action === "delete") {
success = !!(await bot.db.levelRoles
.delete({
where: {
serverId_level_roleId: {
serverId: interaction.guild.id,
roleId: role.id,
level
}
}
})
.catch(() => null));
}
await interaction.editReply({
content: success
? `Successfully ${action}ed your level ${level} ${role} assignment.`
: `Failed to ${action} your level ${level} ${role} assignment.`
});
} catch (err) {
const id = await errorHandler(bot, "level roles command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

98
src/commands/lockdown.ts Normal file
View File

@ -0,0 +1,98 @@
import {
ChannelType,
EmbedBuilder,
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder,
TextChannel
} from "discord.js";
import { Command } from "../interfaces/Command";
import { getConfig } from "../modules/data/getConfig";
import { errorHandler } from "../utils/errorHandler";
export const lockdown: Command = {
data: new SlashCommandBuilder()
.setName("lockdown")
.setDescription("Lock down a channel.")
.addChannelOption((option) =>
option
.setName("channel")
.setDescription("The channel to lock down.")
.setRequired(true)
.addChannelTypes(ChannelType.GuildText)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "You must be in a guild to use this command."
});
return;
}
if (
!(member as GuildMember).permissions.has(
PermissionFlagsBits.ManageChannels
)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const channel = interaction.options.getChannel(
"channel",
true
) as TextChannel;
if (!("send" in channel)) {
await interaction.editReply({
content: "You must use this command to target a text based channel."
});
return;
}
await channel.permissionOverwrites.edit(
guild.id,
{
SendMessages: false
},
{ reason: `Lockdown Performed by ${interaction.user.tag}` }
);
const config = await getConfig(bot, guild.id);
if (!config.modLogChannel) {
return;
}
const logChannel =
guild.channels.cache.get(config.modLogChannel) ||
(await guild.channels.fetch(config.modLogChannel));
if (!logChannel || !("send" in logChannel)) {
return;
}
const embed = new EmbedBuilder();
embed.setTitle("Channel Locked Down");
embed.setDescription(
`The <#${channel.id}> channel has been locked down.`
);
embed.setAuthor({
name: interaction.user.tag,
iconURL: interaction.user.displayAvatarURL()
});
await logChannel.send({ embeds: [embed] });
await interaction.editReply({ content: "Channel has been locked down!" });
} catch (err) {
const id = await errorHandler(bot, "lockdown", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

49
src/commands/massBan.ts Normal file
View File

@ -0,0 +1,49 @@
import {
ActionRowBuilder,
ModalBuilder,
SlashCommandBuilder,
TextInputBuilder,
TextInputStyle
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
export const massBan: Command = {
data: new SlashCommandBuilder()
.setName("massban")
.setDescription("Ban a list of user IDs at once."),
run: async (bot, interaction) => {
try {
const textInput = new TextInputBuilder()
.setCustomId("mass-ban-ids")
.setLabel("Input your list of IDs separated by new lines")
.setMaxLength(4000)
.setStyle(TextInputStyle.Paragraph)
.setRequired(true);
const reasonInput = new TextInputBuilder()
.setCustomId("reason")
.setLabel("Reason for the mass ban?")
.setStyle(TextInputStyle.Short)
.setRequired(true);
const inputRow = new ActionRowBuilder<TextInputBuilder>().addComponents(
textInput
);
const reasonRow = new ActionRowBuilder<TextInputBuilder>().addComponents(
reasonInput
);
const modal = new ModalBuilder()
.setCustomId("mass-ban-modal")
.setTitle("Mass Ban")
.addComponents(inputRow, reasonRow);
await interaction.showModal(modal);
} catch (err) {
const id = await errorHandler(bot, "mass ban", err);
await interaction.reply({
ephemeral: true,
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

122
src/commands/mute.ts Normal file
View File

@ -0,0 +1,122 @@
import {
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
import { processModAction } from "../utils/processModAction";
export const mute: Command = {
data: new SlashCommandBuilder()
.setName("mute")
.setDescription("Mute a member")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to mute.")
.setRequired(true)
)
.addIntegerOption((option) =>
option
.setName("duration")
.setDescription("The duration of the mute.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("duration-unit")
.setDescription("The unit for the duration value")
.setRequired(true)
.addChoices(
{ name: "Minutes", value: "minutes" },
{ name: "Hours", value: "hours" },
{ name: "Days", value: "days" },
{ name: "Weeks", value: "weeks" }
)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for the mute.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
)
.addStringOption((option) =>
option
.setName("evidence")
.setDescription(
"A link to the evidence for the mute. For multiple links, separate with a space."
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (
!(member as GuildMember).permissions.has(
PermissionFlagsBits.ModerateMembers
)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const user = interaction.options.getUser("user", true);
const target =
guild.members.cache.get(user.id) ||
(await guild.members.fetch(user.id).catch(() => null));
if (!target) {
await interaction.editReply({
content: "That member appears to have left the server."
});
return;
}
if (isModerator(target)) {
await interaction.editReply({
content: "You cannot mute a moderator."
});
return;
}
const duration = interaction.options.getInteger("duration", true);
const durationUnit = interaction.options.getString("duration-unit", true);
const reason = interaction.options.getString("reason", true);
const evidence =
interaction.options.getString("evidence")?.split(/\s+/) || [];
await processModAction(
bot,
interaction,
guild,
user,
"mute",
reason,
evidence,
duration,
durationUnit
);
} catch (err) {
const id = await errorHandler(bot, "mute command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

67
src/commands/note.ts Normal file
View File

@ -0,0 +1,67 @@
import { GuildMember, SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
import { processModAction } from "../utils/processModAction";
export const note: Command = {
data: new SlashCommandBuilder()
.setName("note")
.setDescription("Add a note to a member's record.")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to add a note to.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for adding this note.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (!isModerator(member as GuildMember)) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const user = interaction.options.getUser("user", true);
const reason = interaction.options.getString("reason", true);
const target =
guild.members.cache.get(user.id) ||
(await guild.members.fetch(user.id).catch(() => null));
if (target && isModerator(target)) {
await interaction.editReply({
content: "You cannot add a note to a moderator."
});
return;
}
await processModAction(bot, interaction, guild, user, "note", reason, []);
} catch (err) {
const id = await errorHandler(bot, "note command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

50
src/commands/ping.ts Normal file
View File

@ -0,0 +1,50 @@
import { EmbedBuilder, SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
export const ping: Command = {
data: new SlashCommandBuilder()
.setName("ping")
.setDescription("Check the response time of the bot."),
run: async (bot, interaction) => {
try {
await interaction.deferReply();
const receivedInteraction = Date.now();
const { createdTimestamp } = interaction;
const discordLatency = receivedInteraction - createdTimestamp;
const websocketLatency = bot.ws.ping;
await bot.db.$runCommandRaw({ ping: 1 });
const databaseLatency = Date.now() - receivedInteraction;
const pingEmbed = new EmbedBuilder();
pingEmbed.setTitle("Pong!");
pingEmbed.setDescription("Here is my latency information!");
pingEmbed.addFields(
{
name: "Interaction Latency",
value: `${discordLatency}ms`,
inline: true
},
{
name: "Websocket Latency",
value: `${websocketLatency}ms`,
inline: true
},
{
name: "Database Latency",
value: `${databaseLatency}ms`,
inline: true
}
);
await interaction.editReply({ embeds: [pingEmbed] });
} catch (err) {
const id = await errorHandler(bot, "ping command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

129
src/commands/profile.ts Normal file
View File

@ -0,0 +1,129 @@
import { SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import {
validateColour,
validateImage
} from "../modules/commands/profileValidation";
import { errorHandler } from "../utils/errorHandler";
export const profile: Command = {
data: new SlashCommandBuilder()
.setName("profile")
.setDescription("Edit your profile that appears in the leaderboard")
.setDMPermission(false)
.addStringOption((option) =>
option
.setName("avatar")
.setDescription(
"The avatar to appear on your profile card must be a URL that points to an image."
)
)
.addStringOption((option) =>
option
.setName("background-colour")
.setDescription(
"The semi-transparent background color for your profile card must be a 6-digit hex value."
)
)
.addStringOption((option) =>
option
.setName("background-image")
.setDescription(
"The background image for your profile card must be a URL that points to an image."
)
)
.addStringOption((option) =>
option
.setName("colour")
.setDescription(
"The color for the text on your profile card must be a 6-digit hex value."
)
),
run: async (CamperChan, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const responses = ["Your profile settings have been updated!"];
const opts = {
avatar: interaction.options.getString("avatar"),
backgroundColour: interaction.options.getString("background-colour"),
backgroundImage: interaction.options.getString("background-image"),
colour: interaction.options.getString("colour")
};
if (opts.avatar) {
const isValid = await validateImage(opts.avatar);
if (!isValid) {
responses.push(`${opts.avatar} is not a valid image URL.`);
opts.avatar = "";
}
}
if (opts.backgroundImage) {
const isValid = await validateImage(opts.backgroundImage);
if (!isValid) {
responses.push(`${opts.backgroundImage} is not a valid image URL.`);
opts.backgroundImage = "";
}
}
if (opts.colour) {
if (opts.colour.startsWith("#")) {
opts.colour = opts.colour.slice(1);
}
if (!validateColour(opts.colour)) {
opts.colour = null;
responses.push(
`${interaction.options.getString("colour")} is not a valid hex code. Please try again with a 6 character hex code (# is optional).`
);
}
}
if (opts.backgroundColour) {
if (opts.backgroundColour.startsWith("#")) {
opts.backgroundColour = opts.backgroundColour.slice(1);
}
if (!validateColour(opts.backgroundColour)) {
opts.backgroundColour = null;
responses.push(
`${interaction.options.getString("background-colour")} is not a valid hex code. Please try again with a 6 character hex code (# is optional).`
);
}
}
const query = (
Object.entries(opts) as [keyof typeof opts, string][]
).reduce(
(acc, [key, val]) => {
if (val) {
acc[key] = val;
}
return acc;
},
{} as Record<keyof typeof opts, string>
);
await CamperChan.db.levels.upsert({
where: {
serverId_userId: {
serverId: interaction.guild.id,
userId: interaction.user.id
}
},
update: {
...query
},
create: {
userId: interaction.user.id,
serverId: interaction.guild.id,
username: interaction.user.username,
...query
}
});
await interaction.editReply({ content: responses.join("\n") });
} catch (err) {
const id = await errorHandler(CamperChan, "user settings command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

71
src/commands/prune.ts Normal file
View File

@ -0,0 +1,71 @@
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
import { Command } from "../interfaces/Command";
import { getConfig } from "../modules/data/getConfig";
import { errorHandler } from "../utils/errorHandler";
export const prune: Command = {
data: new SlashCommandBuilder()
.setName("prune")
.setDescription("Prune messages from THIS channel.")
.addNumberOption((option) =>
option
.setName("amount")
.setDescription("The amount of messages to remove")
.setMinValue(1)
.setMaxValue(100)
.setRequired(true)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "Could not find the member or guild."
});
return;
}
if (!member.permissions.has(PermissionFlagsBits.ManageMessages)) {
await interaction.editReply({
content: "You do not have permission to prune messages."
});
return;
}
const channel = interaction.channel;
const amount = interaction.options.getNumber("amount", true);
if (!channel) {
await interaction.editReply({
content: "Please provide a text channel or thread."
});
return;
}
const messages = await channel.messages.fetch({ limit: amount });
for (const message of messages.values()) {
await message.delete().catch(() => null);
}
await interaction.editReply({ content: "Complete." });
const config = await getConfig(bot, interaction.guild.id);
if (config.modLogChannel) {
const logChannel =
interaction.guild.channels.cache.get(config.modLogChannel) ||
(await interaction.guild.channels.fetch(config.modLogChannel));
if (logChannel && "send" in logChannel) {
await logChannel.send({
content: `Prune run by <@!${interaction.user.id}>. Deleted Messages: ${amount}`
});
}
}
} catch (err) {
const id = await errorHandler(bot, "prune interaction", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

52
src/commands/rank.ts Normal file
View File

@ -0,0 +1,52 @@
import { SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import { generateProfileImage } from "../modules/commands/generateProfileImage";
import { errorHandler } from "../utils/errorHandler";
export const rank: Command = {
data: new SlashCommandBuilder()
.setDMPermission(false)
.setName("rank")
.setDescription("See your level rank in the community."),
run: async (bot, interaction) => {
try {
await interaction.deferReply();
const { user } = interaction;
const target = user.id;
const record = await bot.db.levels.findUnique({
where: {
serverId_userId: {
userId: target,
serverId: interaction.guild.id
}
}
});
if (!record) {
await interaction.editReply({
content: "Error loading your database record."
});
return;
}
const file = await generateProfileImage(bot, record);
if (!file) {
await interaction.editReply({
content: "There was an error generating your profile. :c"
});
return;
}
await interaction.editReply({ files: [file] });
} catch (err) {
const id = await errorHandler(bot, "rank command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

52
src/commands/role.ts Normal file
View File

@ -0,0 +1,52 @@
import { SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
export const role: Command = {
data: new SlashCommandBuilder()
.setDMPermission(false)
.setName("role")
.setDescription("Give yourself a permitted role, or remove it.")
.addRoleOption((o) =>
o.setName("role").setDescription("The role to toggle.").setRequired(true)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const role = interaction.options.getRole("role", true);
const isPermitted = await bot.db.roles
.findUnique({
where: {
serverId_roleId: {
serverId: interaction.guild.id,
roleId: role.id
}
}
})
.catch(() => null);
if (!isPermitted) {
await interaction.editReply({
content: `The <@&${role.id}> role is not self-assignable.`
});
return;
}
if (interaction.member.roles.cache.has(role.id)) {
await interaction.member.roles.remove(role.id);
await interaction.editReply({
content: `The <@&${role.id}> role has been removed.`
});
return;
}
await interaction.member.roles.add(role.id);
await interaction.editReply({
content: `The <@&${role.id}> role has been added.`
});
} catch (err) {
const id = await errorHandler(bot, "role command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

102
src/commands/secure.ts Normal file
View File

@ -0,0 +1,102 @@
import { PermissionFlagsBits, SlashCommandBuilder } from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
export const secure: Command = {
data: new SlashCommandBuilder()
.setName("secure")
.setDescription(
"Toggles the feature to keep dms/invites locked or unlocked"
)
.setDMPermission(false)
.addBooleanOption((option) =>
option
.setName("invites")
.setDescription("Keep invites locked down?")
.setRequired(true)
)
.addBooleanOption((option) =>
option
.setName("dms")
.setDescription("Keep DMs locked down?")
.setRequired(true)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { guild, member } = interaction;
const lockInvites = interaction.options.getBoolean("invites", true);
const lockDms = interaction.options.getBoolean("dms", true);
if (
![
PermissionFlagsBits.Administrator,
PermissionFlagsBits.KickMembers,
PermissionFlagsBits.BanMembers,
PermissionFlagsBits.ManageGuild
].some((perm) => member.permissions.has(perm))
) {
await interaction.editReply({
content: "You do not have permission to use this command."
});
return;
}
const botMember = await guild.members
.fetch(bot.user?.id || "oopsie")
.catch(() => null);
if (
!botMember ||
![
PermissionFlagsBits.Administrator,
PermissionFlagsBits.KickMembers,
PermissionFlagsBits.BanMembers,
PermissionFlagsBits.ManageGuild
].some((perm) => botMember.permissions.has(perm))
) {
await interaction.editReply({
content: "I do not have the correct permissions to do this."
});
return;
}
const date = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
await bot.db.security.upsert({
where: { serverId: guild.id },
update: { lockDms, lockInvites },
create: { lockDms, lockInvites, serverId: guild.id }
});
await fetch(
`https://discord.com/api/v10/guilds/${guild.id}/incident-actions`,
{
method: "PUT",
headers: {
Authorization: `Bot ${bot.env.token}`,
"content-type": "application/json"
},
body: JSON.stringify({
dms_disabled_until: lockDms ? date : null,
invites_disabled_until: lockInvites ? date : null
})
}
).catch(
async (e) =>
await errorHandler(bot, `incident-actions for ${guild.id}`, e)
);
await interaction.editReply({
content: `Security options have been updated.\nInvites are ${
lockInvites ? "disabled" : "enabled"
}.\nDMs are ${lockDms ? "disabled" : "enabled"}.`
});
} catch (err) {
const id = await errorHandler(bot, "secure command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

102
src/commands/softBan.ts Normal file
View File

@ -0,0 +1,102 @@
import {
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
import { processModAction } from "../utils/processModAction";
export const softBan: Command = {
data: new SlashCommandBuilder()
.setName("softban")
.setDescription(
"Bans a user from the server, cleans up their messages, and removes the ban."
)
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to softban.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for softbanning.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
)
.addIntegerOption((option) =>
option
.setName("prune")
.setDescription("Number of days to prune messages.")
.setMinValue(1)
.setMaxValue(7)
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("evidence")
.setDescription(
"A link to the evidence for the ban. For multiple links, separate with a space."
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (
!(member as GuildMember).permissions.has(PermissionFlagsBits.BanMembers)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const reason = interaction.options.getString("reason", true);
const evidence =
interaction.options.getString("evidence")?.split(/\s+/) || [];
const prune = interaction.options.getInteger("prune", true);
const user = interaction.options.getUser("user", true);
const target =
guild.members.cache.get(user.id) ||
(await guild.members.fetch(user.id).catch(() => null));
if (target && isModerator(target)) {
await interaction.editReply({
content: "You cannot ban a moderator."
});
return;
}
await processModAction(
bot,
interaction,
guild,
user,
"softban",
reason,
evidence,
undefined,
undefined,
prune
);
} catch (err) {
const id = await errorHandler(bot, "softban command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

87
src/commands/unban.ts Normal file
View File

@ -0,0 +1,87 @@
import {
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { processModAction } from "../utils/processModAction";
export const unban: Command = {
data: new SlashCommandBuilder()
.setName("unban")
.setDescription("Unban a user from the server.")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to unban.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for unbanning.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
)
.addStringOption((option) =>
option
.setName("evidence")
.setDescription(
"A link to the evidence for the unban. For multiple links, separate with a space."
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (
!(member as GuildMember).permissions.has(PermissionFlagsBits.BanMembers)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const reason = interaction.options.getString("reason", true);
const evidence =
interaction.options.getString("evidence")?.split(/\s+/) || [];
const user = interaction.options.getUser("user", true);
const isBanned = await guild.bans.fetch(user.id).catch(() => false);
if (!isBanned) {
await interaction.editReply({
content: `ID ${user.id} is not banned.`
});
return;
}
await processModAction(
bot,
interaction,
guild,
user,
"unban",
reason,
evidence
);
} catch (err) {
const id = await errorHandler(bot, "unban command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

96
src/commands/unlock.ts Normal file
View File

@ -0,0 +1,96 @@
import {
ChannelType,
EmbedBuilder,
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder,
TextChannel
} from "discord.js";
import { Command } from "../interfaces/Command";
import { getConfig } from "../modules/data/getConfig";
import { errorHandler } from "../utils/errorHandler";
export const unlock: Command = {
data: new SlashCommandBuilder()
.setName("unlock")
.setDescription("Remove lock down from a channel.")
.addChannelOption((option) =>
option
.setName("channel")
.setDescription("The channel to unlock.")
.setRequired(true)
.addChannelTypes(ChannelType.GuildText)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "You must be in a guild to use this command."
});
return;
}
if (
!(member as GuildMember).permissions.has(
PermissionFlagsBits.ManageChannels
)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const channel = interaction.options.getChannel(
"channel",
true
) as TextChannel;
if (!("send" in channel)) {
await interaction.editReply({
content: "You must use this command to target a text based channel."
});
return;
}
await channel.permissionOverwrites.edit(
guild.id,
{
SendMessages: null
},
{ reason: `Lockdown Removed by ${interaction.user.tag}` }
);
const config = await getConfig(bot, guild.id);
if (!config.modLogChannel) {
return;
}
const logChannel =
guild.channels.cache.get(config.modLogChannel) ||
(await guild.channels.fetch(config.modLogChannel));
if (!logChannel || !("send" in logChannel)) {
return;
}
const embed = new EmbedBuilder();
embed.setTitle("Channel Unlocked");
embed.setDescription(`The <#${channel.id}> channel has been unlocked.`);
embed.setAuthor({
name: interaction.user.tag,
iconURL: interaction.user.displayAvatarURL()
});
await logChannel.send({ embeds: [embed] });
await interaction.editReply({ content: "Channel has been unlocked!" });
} catch (err) {
const id = await errorHandler(bot, "unlock command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

106
src/commands/unmute.ts Normal file
View File

@ -0,0 +1,106 @@
import {
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
import { processModAction } from "../utils/processModAction";
export const unmute: Command = {
data: new SlashCommandBuilder()
.setName("unmute")
.setDescription("Unmute a member")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to unmute.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for the unmute.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
)
.addStringOption((option) =>
option
.setName("evidence")
.setDescription(
"A link to the evidence for the unmute. For multiple links, separate with a space."
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (
!(member as GuildMember).permissions.has(
PermissionFlagsBits.ModerateMembers
)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const user = interaction.options.getUser("user", true);
const target =
guild.members.cache.get(user.id) ||
(await guild.members.fetch(user.id).catch(() => null));
if (!target) {
await interaction.editReply({
content: "That member appears to have left the server."
});
return;
}
if (!target.isCommunicationDisabled()) {
await interaction.editReply({
content: "That member is not muted."
});
return;
}
if (isModerator(target)) {
await interaction.editReply({
content:
"A moderator should never be muted. How on earth did you achieve this???"
});
}
const reason = interaction.options.getString("reason", true);
const evidence =
interaction.options.getString("evidence")?.split(/\s+/) || [];
await processModAction(
bot,
interaction,
guild,
user,
"unmute",
reason,
evidence
);
} catch (err) {
const id = await errorHandler(bot, "unmute command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

99
src/commands/warn.ts Normal file
View File

@ -0,0 +1,99 @@
import {
GuildMember,
PermissionFlagsBits,
SlashCommandBuilder
} from "discord.js";
import { Command } from "../interfaces/Command";
import { errorHandler } from "../utils/errorHandler";
import { isModerator } from "../utils/isModerator";
import { processModAction } from "../utils/processModAction";
export const warn: Command = {
data: new SlashCommandBuilder()
.setName("warn")
.setDescription("Warn a member")
.addUserOption((option) =>
option
.setName("user")
.setDescription("The user to warn.")
.setRequired(true)
)
.addStringOption((option) =>
option
.setName("reason")
.setDescription("The reason for the warning.")
.setRequired(true)
.setMinLength(1)
.setMaxLength(400)
)
.addStringOption((option) =>
option
.setName("evidence")
.setDescription(
"A link to the evidence for the warning. For multiple links, separate with a space."
)
),
run: async (bot, interaction) => {
try {
await interaction.deferReply({ ephemeral: true });
const { member, guild } = interaction;
if (!member || !guild) {
await interaction.editReply({
content: "There was an error loading the guild and member data."
});
return;
}
if (
!(member as GuildMember).permissions.has(
PermissionFlagsBits.KickMembers
)
) {
await interaction.editReply({
content: "You do not have permission to run this command."
});
return;
}
const user = interaction.options.getUser("user", true);
const target =
guild.members.cache.get(user.id) ||
(await guild.members.fetch(user.id).catch(() => null));
if (!target) {
await interaction.editReply({
content: "That member appears to have left the server."
});
return;
}
if (isModerator(target)) {
await interaction.editReply({
content: "You cannot warn a moderator."
});
return;
}
const reason = interaction.options.getString("reason", true);
const evidence =
interaction.options.getString("evidence")?.split(/\s+/) || [];
await processModAction(
bot,
interaction,
guild,
user,
"warn",
reason,
evidence
);
} catch (err) {
const id = await errorHandler(bot, "warn command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

View File

@ -0,0 +1,12 @@
import { configs } from "@prisma/client";
export const defaultConfig: Omit<configs, "id"> = {
serverId: "",
inviteLink: "",
banAppealLink: "",
modLogChannel: "",
eventLogChannel: "",
messageReportChannel: "",
birthdayChannel: "",
joinRole: ""
};

View File

@ -0,0 +1,12 @@
import { Action } from "../interfaces/Action";
export const EmbedColours: { [K in Action]: number } = {
ban: 0xfa000c,
softban: 0xfa9900,
kick: 0xffee00,
warn: 0x2600ff,
mute: 0xd900ff,
unmute: 0x00ff22,
unban: 0x00ff22,
note: 0x000001
};

17
src/config/Github.ts Normal file
View File

@ -0,0 +1,17 @@
export const IgnoredActors = [
"renovate[bot]",
"codeclimate[bot]",
"dependabot[bot]",
"lgtm-com[bot]",
"deepsource-autofix[bot]",
"sonarcloud[bot]",
"melody-iuvo"
];
export const ThankYou = `## Thank You
Thank you for your contribution to our project. We have reviewed your pull request and are happy to accept these changes.
Please continue to watch for issues labelled \`help wanted\`, as these will be additional opportunities to contribute.
You can also see all open issues [through our contributor tool](https://contribute.naomi.lgbt). Also, feel free to join our [Discord server](https://chat.naomi.lgbt) to chat with us and get notified when new issues are available!`;

View File

@ -0,0 +1,10 @@
import { GatewayIntentBits } from "discord.js";
export const IntentOptions = [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildModeration,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageReactions
];

14
src/config/LevelScale.ts Normal file
View File

@ -0,0 +1,14 @@
/**
* This config is an automatically-generated scale for mapping experience
* point values to level values.
*/
const levelScale: number[] = [];
let j = 0;
for (let i = 0; i <= 1000; i++) {
j += i * 1000;
levelScale[i] = j;
}
export default levelScale;

View File

@ -0,0 +1,15 @@
import { configs } from "@prisma/client";
export const logChannelChoices: { name: string; value: keyof configs }[] = [
{ name: "Moderation Action Log Channel", value: "modLogChannel" },
{ name: "Private Event Log Channel", value: "eventLogChannel" },
{ name: "Message Reporting Channel", value: "messageReportChannel" }
];
export const logChannelChoicesMap: {
[key: string]: string;
} = {
modLogChannel: "moderation actions",
eventLogChannel: "gateway events",
messageReportChannel: "message reports"
};

View File

@ -0,0 +1,8 @@
import { GuildPremiumTier } from "discord.js";
export const ServerUploadLimits: { [tier in GuildPremiumTier]: number } = {
0: 8000000,
1: 8000000,
2: 50000000,
3: 100000000
};

126
src/contexts/report.ts Normal file
View File

@ -0,0 +1,126 @@
import {
Message,
ActionRowBuilder,
ButtonBuilder,
EmbedBuilder,
ButtonStyle,
TextInputBuilder,
TextInputStyle,
ModalBuilder
} from "discord.js";
import { EmbedColours } from "../config/EmbedColours";
import { Context } from "../interfaces/Context";
import { getConfig } from "../modules/data/getConfig";
import { errorHandler } from "../utils/errorHandler";
export const report: Context = {
data: {
name: "report",
type: 3
},
run: async (bot, interaction) => {
try {
if (!interaction.isMessageContextMenuCommand()) {
await interaction.reply({
content:
"This command is improperly configured. Please contact Naomi.",
ephemeral: true
});
return;
}
const guild = interaction.guild;
const message = interaction.options.getMessage(
"message",
true
) as Message;
if (!guild || !message) {
await interaction.reply({
content: "Could not find the guild record...",
ephemeral: true
});
return;
}
const config = await getConfig(bot, guild.id);
if (!config.messageReportChannel) {
await interaction.reply({
content: "Reporting has not been set up for this server.",
ephemeral: true
});
return;
}
const channel =
guild.channels.cache.get(config.messageReportChannel) ||
(await guild.channels.fetch(config.messageReportChannel));
if (!channel || !("send" in channel)) {
await interaction.reply({
content: "Reporting channel not found.",
ephemeral: true
});
return;
}
const embed = new EmbedBuilder()
.setTitle("Message Reported")
.setDescription(message.content)
.setColor(EmbedColours.ban)
.addFields(
{
name: "Author",
value: `<@${message.author.id}>`,
inline: true
},
{
name: "Reporter",
value: `<@${interaction.user.id}>`,
inline: true
}
);
const link = new ButtonBuilder()
.setLabel("Message Link")
.setStyle(ButtonStyle.Link)
.setURL(message.url);
const button = new ButtonBuilder()
.setLabel("Acknowledge")
.setStyle(ButtonStyle.Primary)
.setCustomId(`ack-${message.id}`);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents([
link,
button
]);
const reportLog = await channel.send({
embeds: [embed],
components: [row]
});
const reportModal = new ModalBuilder()
.setCustomId(`rep-${reportLog.id}`)
.setTitle("Report Message");
const reasonInput = new TextInputBuilder()
.setCustomId("reason")
.setLabel("Why are you reporting this message?")
.setStyle(TextInputStyle.Paragraph)
.setRequired(true);
const actionRow = new ActionRowBuilder<TextInputBuilder>().addComponents(
reasonInput
);
reportModal.addComponents(actionRow);
await interaction.showModal(reportModal);
} catch (err) {
const id = await errorHandler(bot, "report context command", err);
await interaction.reply({
ephemeral: true,
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

View File

@ -0,0 +1,20 @@
import { PrismaClient } from "@prisma/client";
import { ExtendedClient } from "../interfaces/ExtendedClient";
import { errorHandler } from "../utils/errorHandler";
import { sendDebugMessage } from "../utils/sendDebugMessage";
/**
* Connects to the database.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const connectDatabase = async (bot: ExtendedClient) => {
try {
bot.db = new PrismaClient();
await bot.db.$connect();
await sendDebugMessage(bot, "Connected to database.");
} catch (err) {
await errorHandler(bot, "connect database", err);
}
};

129
src/events/_handleEvents.ts Normal file
View File

@ -0,0 +1,129 @@
import { ExtendedClient } from "../interfaces/ExtendedClient";
import { checkEntitledGuild } from "../utils/checkEntitledGuild";
import { onDisconnect } from "./client/onDisconnect";
import { onReady } from "./client/onReady";
import { onAuditLogEntry } from "./guild/onAuditLogEntry";
import { onGuildCreate } from "./guild/onGuildCreate";
import { onGuildDelete } from "./guild/onGuildDelete";
import { onInteraction } from "./interaction/onInteraction";
import { onMemberAdd } from "./member/onMemberAdd";
import { onMemberRemove } from "./member/onMemberRemove";
import { onMemberUpdate } from "./member/onMemberUpdate";
import { onMessage } from "./message/onMessage";
import { onMessageDelete } from "./message/onMessageDelete";
import { onMessageEdit } from "./message/onMessageEdit";
import { onThreadCreate } from "./thread/onThreadCreate";
import { onThreadDelete } from "./thread/onThreadDelete";
import { onThreadUpdate } from "./thread/onThreadUpdate";
import { onVoiceUpdate } from "./voice/onVoiceUpdate";
/**
* Module to mount the Discord event listeners.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const handleEvents = (bot: ExtendedClient) => {
/* Client Events */
bot.on("ready", async () => await onReady(bot));
bot.on("disconnect", () => onDisconnect(bot));
/* Message Events */
bot.on("messageCreate", async (message) => {
if (!message.guild || !(await checkEntitledGuild(bot, message.guild))) {
return;
}
await onMessage(bot, message);
});
bot.on("messageDelete", async (message) => {
if (!message.guild || !(await checkEntitledGuild(bot, message.guild))) {
return;
}
await onMessageDelete(bot, message);
});
bot.on("messageUpdate", async (oldMessage, newMessage) => {
if (
!newMessage.guild ||
!(await checkEntitledGuild(bot, newMessage.guild))
) {
return;
}
await onMessageEdit(bot, oldMessage, newMessage);
});
/* Interaction Events */
bot.on(
"interactionCreate",
async (interaction) => await onInteraction(bot, interaction)
);
/* Thread Events */
bot.on("threadCreate", async (thread) => {
if (!thread.guild || !(await checkEntitledGuild(bot, thread.guild))) {
return;
}
await onThreadCreate(bot, thread);
});
bot.on("threadDelete", async (thread) => {
if (!thread.guild || !(await checkEntitledGuild(bot, thread.guild))) {
return;
}
await onThreadDelete(bot, thread);
});
bot.on("threadUpdate", async (oldThread, newThread) => {
if (!newThread.guild || !(await checkEntitledGuild(bot, newThread.guild))) {
return;
}
await onThreadUpdate(bot, oldThread, newThread);
});
/* Voice Events */
bot.on("voiceStateUpdate", async (oldVoice, newVoice) => {
if (!newVoice.guild || !(await checkEntitledGuild(bot, newVoice.guild))) {
return;
}
await onVoiceUpdate(bot, oldVoice, newVoice);
});
/* Member Events */
bot.on("guildMemberAdd", async (member) => {
if (!member.guild || !(await checkEntitledGuild(bot, member.guild))) {
return;
}
await onMemberAdd(bot, member);
});
bot.on("guildMemberRemove", async (member) => {
if (!member.guild || !(await checkEntitledGuild(bot, member.guild))) {
return;
}
await onMemberRemove(bot, member);
});
bot.on("guildMemberUpdate", async (oldMember, newMember) => {
if (!newMember.guild || !(await checkEntitledGuild(bot, newMember.guild))) {
return;
}
await onMemberUpdate(bot, oldMember, newMember);
});
/* Guild Events */
bot.on("guildAuditLogEntryCreate", async (log, guild) => {
if (!guild || !(await checkEntitledGuild(bot, guild))) {
return;
}
await onAuditLogEntry(bot, log, guild);
});
bot.on("guildCreate", async (guild) => {
if (!guild || !(await checkEntitledGuild(bot, guild))) {
return;
}
await onGuildCreate(bot, guild);
});
bot.on("guildDelete", async (guild) => {
if (!guild) {
return;
}
await onGuildDelete(bot, guild);
});
};

View File

@ -0,0 +1,16 @@
import { EmbedBuilder } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
/**
* Sends a message to the debug hook when the bot disconnects.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const onDisconnect = async (bot: ExtendedClient) => {
const disconnectEmbed = new EmbedBuilder();
disconnectEmbed.setTitle("Disconnected");
disconnectEmbed.setDescription("I have been disconnected from Discord.");
disconnectEmbed.setTimestamp();
await bot.env.debugHook.send({ embeds: [disconnectEmbed] });
};

View File

@ -0,0 +1,29 @@
import { scheduleJob } from "node-schedule";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { maintainSecurity } from "../../modules/maintainSecurity";
import { postBirthdays } from "../../modules/postBirthdays";
import { Prometheus } from "../../modules/prometheus";
import { registerCommands } from "../../utils/registerCommands";
import { sendDebugMessage } from "../../utils/sendDebugMessage";
/**
* Handles the `ready` from Discord.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const onReady = async (bot: ExtendedClient) => {
await sendDebugMessage(bot, `Logged in as ${bot.user?.tag}`);
await registerCommands(bot);
bot.analytics = new Prometheus(bot);
await bot.analytics.updateEntitlements(bot);
// Daily at 9am PST
scheduleJob("birthdays", "0 9 * * *", async () => await postBirthdays(bot));
// Daily at midnight and noon
scheduleJob(
"birthdays",
"0 0,12 * * *",
async () => await maintainSecurity(bot)
);
};

View File

@ -0,0 +1,78 @@
import { AuditLogEvent, Guild, GuildAuditLogsEntry, User } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getModActionFromAuditLog } from "../../modules/events/getModActionFromAuditLog";
import { addCase } from "../../utils/addCase";
import { errorHandler } from "../../utils/errorHandler";
import { sendLogMessage } from "../../utils/sendLogMessage";
/**
* Handles properly logging a manual mod action based on audit logs.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {GuildAuditLogsEntry} log The audit log payload from Discord.
* @param {Guild} guild The guild payload from Discord.
*/
export const onAuditLogEntry = async (
bot: ExtendedClient,
log: GuildAuditLogsEntry,
guild: Guild
) => {
try {
const { action, changes, executorId, targetId, target, reason } = log;
if (executorId === bot.user?.id) {
return;
}
// if not a mod action we don't care.
if (
![
AuditLogEvent.MemberBanAdd,
AuditLogEvent.MemberBanRemove,
AuditLogEvent.MemberKick,
AuditLogEvent.MemberUpdate
].includes(action) ||
(action === AuditLogEvent.MemberUpdate &&
!changes.find(
(change) => change.key === "communication_disabled_until"
)) ||
!targetId ||
!(target instanceof User) ||
!executorId
) {
return;
}
const modAction = getModActionFromAuditLog(log);
if (!modAction) {
return;
}
const reasonString = `This was a manual action pulled from the audit log. Please use the bot for accurate reporting.\n\nReason: ${
reason || "Unable to parse reason."
}`;
const caseNum = await addCase(
bot,
guild.id,
target.id,
reasonString,
modAction,
executorId,
[]
);
await sendLogMessage(
bot,
guild,
target,
modAction,
reasonString,
executorId,
[],
false,
caseNum
);
} catch (err) {
await errorHandler(bot, "on audit log entry", err);
}
};

View File

@ -0,0 +1,20 @@
import { Guild } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
/**
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {Guild} guild The newly joined Discord guild.
*/
export const onGuildCreate = async function (
bot: ExtendedClient,
guild: Guild
) {
const owner = await guild.fetchOwner();
await bot.env.debugHook.send({
content: `JOINED GUILD: ${guild.name} (${guild.id}) - owned by ${owner?.displayName} (${owner.id})`
});
bot.analytics.updateGuilds(bot);
await bot.analytics.updateEntitlements(bot);
};

View File

@ -0,0 +1,44 @@
import { Guild } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
/**
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {Guild} guild The newly left Discord guild.
*/
export const onGuildDelete = async function (
bot: ExtendedClient,
guild: Guild
) {
try {
await bot.env.debugHook.send({
content: `LEFT GUILD: ${guild.name} (${guild.id}) `
});
await bot.db.cases
.deleteMany({ where: { serverId: guild.id } })
.catch(() => null);
await bot.db.levelRoles
.deleteMany({ where: { serverId: guild.id } })
.catch(() => null);
await bot.db.levels
.deleteMany({ where: { serverId: guild.id } })
.catch(() => null);
await bot.db.configs
.deleteMany({ where: { serverId: guild.id } })
.catch(() => null);
await bot.db.roles
.deleteMany({ where: { serverId: guild.id } })
.catch(() => null);
await bot.db.birthdays
.deleteMany({ where: { serverId: guild.id } })
.catch(() => null);
await bot.db.security
.deleteMany({ where: { serverId: guild.id } })
.catch(() => null);
bot.analytics.updateGuilds(bot);
await bot.analytics.updateEntitlements(bot);
} catch (err) {
await errorHandler(bot, "on guild delete", err);
}
};

View File

@ -0,0 +1,72 @@
import { Interaction } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { handleCopyIdButton } from "../../modules/buttons/handleCopyIdButton";
import { handleReportAcknowledgeButton } from "../../modules/buttons/handleReportAcknowledgeButton";
import { handleChatInputCommand } from "../../modules/interactions/handleChatInputCommand";
import { handleContextMenuCommand } from "../../modules/interactions/handleContextMenuCommand";
import { handleMassBanModal } from "../../modules/modals/handleMassBanModal";
import { handleMessageReportModal } from "../../modules/modals/handleMessageReportModal";
import { checkEntitledGuild } from "../../utils/checkEntitledGuild";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles interactions.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {Interaction} interaction The interaction payload from Discord.
*/
export const onInteraction = async (
bot: ExtendedClient,
interaction: Interaction
) => {
try {
if (interaction.isAutocomplete()) {
return;
}
if (
!interaction.guild ||
(!(await checkEntitledGuild(bot, interaction.guild)) &&
(!interaction.isChatInputCommand() ||
interaction.commandName !== "help"))
) {
await interaction.reply(
"Your guild does not appear to be subscribed to use our bot. Please reach out to us in our [support server](<https://chat.naomi.lgbt>) if you would like to sign up."
);
return;
}
bot.analytics.commandUsed();
if (interaction.isChatInputCommand()) {
handleChatInputCommand(bot, interaction);
}
if (interaction.isContextMenuCommand()) {
handleContextMenuCommand(bot, interaction);
}
if (interaction.isButton()) {
if (interaction.customId.startsWith("ack")) {
await handleReportAcknowledgeButton(bot, interaction);
}
if (interaction.customId.startsWith("copyid")) {
await handleCopyIdButton(bot, interaction);
}
}
if (interaction.isModalSubmit()) {
if (interaction.customId === "mass-ban-modal") {
await handleMassBanModal(bot, interaction);
}
if (interaction.customId.startsWith("rep")) {
await handleMessageReportModal(bot, interaction);
}
}
} catch (err) {
const id = await errorHandler(bot, "on interaction", err);
if (!interaction.isAutocomplete()) {
await interaction.reply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
}
};

View File

@ -0,0 +1,43 @@
import { GuildMember } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { errorHandler } from "../../utils/errorHandler";
/**
* Sends a log message to the configured log channel when a member
* joins the server.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {GuildMember} member The user's Discord instance.
*/
export const onMemberAdd = async (bot: ExtendedClient, member: GuildMember) => {
try {
const { user, guild } = member;
const config = await getConfig(bot, guild.id);
if (config.joinRole) {
await member.roles.add(config.joinRole).catch(() => null);
}
if (!config.eventLogChannel) {
return;
}
const channel =
guild.channels.cache.get(config.eventLogChannel) ||
(await guild.channels.fetch(config.eventLogChannel));
if (!channel || !("send" in channel)) {
return;
}
await channel.send({
content: `${user.tag} (${user.id}) has joined the server. Total Members: ${guild.memberCount}`
});
bot.analytics.updateUsers(bot);
} catch (err) {
await errorHandler(bot, "on member add", err);
}
};

View File

@ -0,0 +1,61 @@
import { GuildMember, PartialGuildMember } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { errorHandler } from "../../utils/errorHandler";
/**
* Sends a log message to the configured log channel when a member
* leaves the server.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {GuildMember} member The user's Discord instance.
*/
export const onMemberRemove = async (
bot: ExtendedClient,
member: GuildMember | PartialGuildMember
) => {
try {
const { user, guild } = member;
const config = await getConfig(bot, guild.id);
await bot.db.birthdays
.delete({
where: {
serverId_userId: { serverId: guild.id, userId: user.id }
}
})
.catch(() => null);
await bot.db.levels
.delete({
where: {
serverId_userId: { serverId: guild.id, userId: user.id }
}
})
.catch(() => null);
if (!config.eventLogChannel) {
return;
}
const channel =
guild.channels.cache.get(config.eventLogChannel) ||
(await guild.channels.fetch(config.eventLogChannel));
if (!channel || !("send" in channel)) {
return;
}
const joinStamp = member.joinedTimestamp
? `<t:${Math.floor(member.joinedTimestamp / 1000)}:F>`
: "unknown";
await channel.send({
content: `${user.tag} (${user.id}) has left the server (joined at ${joinStamp}). Total Members: ${guild.memberCount}`
});
bot.analytics.updateUsers(bot);
} catch (err) {
await errorHandler(bot, "on member remove", err);
}
};

View File

@ -0,0 +1,80 @@
import { GuildMember, PartialGuildMember } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { errorHandler } from "../../utils/errorHandler";
/**
* Sends a log message to the configured log channel when a member's
* data is updated.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {GuildMember} oldMember The user's old Discord instance.
* @param {GuildMember} newMember The user's new Discord instance.
*/
export const onMemberUpdate = async (
bot: ExtendedClient,
oldMember: GuildMember | PartialGuildMember,
newMember: GuildMember
) => {
try {
const { user, guild } = newMember;
const config = await getConfig(bot, guild.id);
if (!config.eventLogChannel) {
return;
}
const channel =
guild.channels.cache.get(config.eventLogChannel) ||
(await guild.channels.fetch(config.eventLogChannel));
if (!channel || !("send" in channel)) {
return;
}
if (oldMember.user.tag !== newMember.user.tag) {
await channel.send({
content: `${user.tag} (${user.id}) has changed their name from ${oldMember.user.tag} to ${newMember.user.tag}`
});
}
if (oldMember.nickname !== newMember.nickname) {
await channel.send({
content: `${user.tag} (${user.id}) has changed their nickname from ${
oldMember.nickname || "**none**"
} to ${newMember.nickname || "**none**"}`
});
}
const removedRoles = oldMember.roles.cache.filter(
(role) => !newMember.roles.cache.has(role.id)
);
const addedRoles = newMember.roles.cache.filter(
(role) => !oldMember.roles.cache.has(role.id)
);
if (removedRoles.size > 0) {
await channel.send({
content: `${user.tag} (${
user.id
}) has removed the following roles: ${removedRoles.map(
(role) => role.name
)}`
});
}
if (addedRoles.size > 0) {
await channel.send({
content: `${user.tag} (${
user.id
}) has added the following roles: ${addedRoles.map(
(role) => role.name
)}`
});
}
} catch (err) {
await errorHandler(bot, "on member update", err);
}
};

View File

@ -0,0 +1,141 @@
import { Message } from "discord.js";
import levelScale from "../../config/LevelScale";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { calculateMuteDuration } from "../../modules/commands/calculateMuteDuration";
import { checkSpamDomain } from "../../modules/events/checkSpamDomain";
import { addCase } from "../../utils/addCase";
import { errorHandler } from "../../utils/errorHandler";
import { sendLogMessage } from "../../utils/sendLogMessage";
import { sendModDm } from "../../utils/sendModDm";
import { triggerModRequest } from "../../utils/triggerModRequest";
const linkRegex = /https?:\/\/([a-zA-Z0-9_.-]{2,256}\.\w{2,24}\b)/g;
/**
* Module to handle the messageCreate event from Discord.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {Message} message The message payload from Discord.
*/
export const onMessage = async (bot: ExtendedClient, message: Message) => {
try {
const { guild, member, author, system } = message;
if (!guild || !member || system || author.bot) {
return;
}
const links = message.content.match(linkRegex);
if (links) {
for (const link of links) {
if (await checkSpamDomain(bot, link.replace(/https?:\/\//, ""))) {
await message.delete().catch(() => null);
const notified = await sendModDm(
bot,
"mute",
author,
guild,
"Your account appears to be compromised."
);
const caseNum = await addCase(
bot,
guild.id,
author.id,
"Your account appears to be compromised",
"mute",
"Automoderator",
[link]
);
await sendLogMessage(
bot,
guild,
author,
"mute",
"Your account appears to be compromised",
"Automoderation",
[link],
notified,
caseNum
);
await triggerModRequest(bot, {
userId: author.id,
serverId: guild.id,
action: "mute",
reason: "Your account appears to be compromised",
moderator: "Automoderator",
duration: calculateMuteDuration(24, "hours"),
pruneDays: 0
});
return;
}
}
}
const bonus = Math.floor(message.content.length / 10);
const pointsEarned = Math.floor(Math.random() * (20 + bonus)) + 5;
const user = await bot.db.levels.upsert({
where: {
serverId_userId: {
serverId: guild.id,
userId: author.id
}
},
update: {},
create: {
serverId: guild.id,
userId: author.id,
username: author.username,
avatar: author.displayAvatarURL(),
points: 0,
level: 0
}
});
if (Date.now() - user.cooldown.getTime() < 60000 || user.level >= 1000) {
return;
}
user.points += pointsEarned;
user.cooldown = new Date();
let levelUp = false;
while (user.points > (levelScale[user.level + 1] ?? Infinity)) {
user.level++;
levelUp = true;
}
await bot.db.levels.update({
where: {
serverId_userId: {
serverId: guild.id,
userId: author.id
}
},
data: {
points: user.points,
level: user.level,
username: author.username,
avatar: author.displayAvatarURL(),
cooldown: user.cooldown
}
});
if (levelUp) {
await message.reply(`Congrats! You're now level ${user.level}!!`);
}
const levelRoles = await bot.db.levelRoles.findMany({
where: {
serverId: guild.id,
level: {
lte: user.level
}
}
});
for (const record of levelRoles) {
await member.roles.add(record.roleId).catch(() => null);
}
} catch (err) {
await errorHandler(bot, "on message", err);
}
};

View File

@ -0,0 +1,73 @@
import { Message, PartialMessage } from "discord.js";
import { ServerUploadLimits } from "../../config/ServerUploadLimits";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { customSubstring } from "../../utils/customSubstring";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles a message delete event.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {Message} message The message that was deleted.
*/
export const onMessageDelete = async (
bot: ExtendedClient,
message: Message | PartialMessage
) => {
try {
const { author, channel, content, guild, embeds, attachments, stickers } =
message;
if (!guild || author?.bot) {
return;
}
const config = await getConfig(bot, guild.id);
if (!config.eventLogChannel) {
return;
}
const logChannel =
guild.channels.cache.get(config.eventLogChannel) ||
(await guild.channels.fetch(config.eventLogChannel));
if (!logChannel || !("send" in logChannel)) {
return;
}
const deletedContent = content || "**No message content found.**";
const mappedAttachements = attachments
.map((el) => el)
.filter((el) => el.size <= ServerUploadLimits[guild.premiumTier]);
const mappedStickers = stickers
.map((el) => el)
.filter((el) => el.available);
let logContent = `${author?.tag} (${author?.id}) had a message (${message.id}) deleted in <#${channel.id}>:\n\n\`${deletedContent}\``;
if (message.reference && message.reference.messageId) {
logContent += `\n\n**This message was in reply to: https://discord.com/channels/${guild.id}/${message.reference.channelId}/${message.reference.messageId}**`;
}
if (attachments.size && mappedAttachements.length < attachments.size) {
logContent += `\n\n**${
attachments.size - mappedAttachements.length
} attachment(s) were too large to log.**`;
}
await logChannel.send({
content: customSubstring(logContent, 2000),
files: mappedAttachements,
embeds,
stickers: mappedStickers,
allowedMentions: {
parse: []
}
});
} catch (err) {
await errorHandler(bot, "on message delete", err);
}
};

View File

@ -0,0 +1,64 @@
import { Message, PartialMessage } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { customSubstring } from "../../utils/customSubstring";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles a message edit event.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {Message} oldMessage The old message payload.
* @param {Message} newMessage The new message payload.
*/
export const onMessageEdit = async (
bot: ExtendedClient,
oldMessage: Message | PartialMessage,
newMessage: Message | PartialMessage
) => {
try {
const { author, channel, guild } = newMessage;
if (!guild || author?.bot) {
return;
}
if (
!oldMessage.content ||
!newMessage.content ||
oldMessage.content === newMessage.content
) {
return;
}
const config = await getConfig(bot, guild.id);
if (!config.eventLogChannel) {
return;
}
const logChannel =
guild.channels.cache.get(config.eventLogChannel) ||
(await guild.channels.fetch(config.eventLogChannel));
if (!logChannel || !("send" in logChannel)) {
return;
}
await logChannel.send({
content: `${author?.tag} (${author?.id}) edited their message in in <#${
channel.id
}>:\n\n**Old Content:**\n\`${customSubstring(
oldMessage.content,
1750
)}\`\n\n**New content:**\n\`${customSubstring(
newMessage.content,
1750
)}\`\n\n${newMessage.url}`,
allowedMentions: { parse: [] }
});
} catch (err) {
await errorHandler(bot, "on message edit", err);
}
};

View File

@ -0,0 +1,42 @@
import { ThreadChannel } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles the creation of a new thread.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ThreadChannel} thread The thread payload from Discord.
*/
export const onThreadCreate = async (
bot: ExtendedClient,
thread: ThreadChannel
) => {
try {
if (thread.joinable) {
await thread.join();
}
const config = await getConfig(bot, thread.guild.id);
if (!config.eventLogChannel) {
return;
}
const channel =
thread.guild.channels.cache.get(config.eventLogChannel) ||
(await thread.guild.channels.fetch(config.eventLogChannel));
if (!channel || !("send" in channel)) {
return;
}
await channel.send({
content: `${thread.name} has been created in <#${thread.parentId}>`
});
} catch (err) {
await errorHandler(bot, "on thread create", err);
}
};

View File

@ -0,0 +1,38 @@
import { ThreadChannel } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles the deletion of a thread.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ThreadChannel} thread The thread payload from Discord.
*/
export const onThreadDelete = async (
bot: ExtendedClient,
thread: ThreadChannel
) => {
try {
const config = await getConfig(bot, thread.guild.id);
if (!config.eventLogChannel) {
return;
}
const channel =
thread.guild.channels.cache.get(config.eventLogChannel) ||
(await thread.guild.channels.fetch(config.eventLogChannel));
if (!channel || !("send" in channel)) {
return;
}
await channel.send({
content: `${thread.name} has been deleted from <#${thread.parentId}>`
});
} catch (err) {
await errorHandler(bot, "on thread create", err);
}
};

View File

@ -0,0 +1,54 @@
import { ThreadChannel } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles a thread update.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ThreadChannel} oldThread The old thread payload.
* @param {ThreadChannel} newThread The new thread payload.
*/
export const onThreadUpdate = async (
bot: ExtendedClient,
oldThread: ThreadChannel,
newThread: ThreadChannel
) => {
try {
const config = await getConfig(bot, newThread.guild.id);
if (!config.eventLogChannel) {
return;
}
const channel =
oldThread.guild.channels.cache.get(config.eventLogChannel) ||
(await oldThread.guild.channels.fetch(config.eventLogChannel));
if (!channel || !("send" in channel)) {
return;
}
if (!oldThread.archived && newThread.archived) {
await channel.send({
content: `${newThread.name} has been archived <#${newThread.parentId}>`
});
}
if (oldThread.archived && !newThread.archived) {
await channel.send({
content: `${newThread.name} has been unarchived <#${newThread.parentId}>`
});
}
if (oldThread.name !== newThread.name) {
await channel.send({
content: `${oldThread.name} has been renamed to ${newThread.name} in <#${newThread.parentId}>`
});
}
} catch (err) {
await errorHandler(bot, "on thread update", err);
}
};

View File

@ -0,0 +1,82 @@
import { VoiceState } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { getConfig } from "../../modules/data/getConfig";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles voice state updates.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {VoiceState} oldVoice The old voice state.
* @param {VoiceState} newVoice The new voice state.
*/
export const onVoiceUpdate = async (
bot: ExtendedClient,
oldVoice: VoiceState,
newVoice: VoiceState
) => {
try {
const config = await getConfig(bot, newVoice.guild.id);
if (!config.eventLogChannel || !newVoice.member) {
return;
}
const channel =
newVoice.guild.channels.cache.get(config.eventLogChannel) ||
(await newVoice.guild.channels.fetch(config.eventLogChannel));
if (!channel || !("send" in channel)) {
return;
}
if (
oldVoice.channelId &&
newVoice.channelId &&
oldVoice.channelId !== newVoice.channelId
) {
await channel.send({
content: `${newVoice.member.user.tag} (${newVoice.member.id}) has moved from <#!${oldVoice.channelId}> to <#!${newVoice.channelId}>.`
});
}
if (oldVoice.channelId && !newVoice.channelId) {
await channel.send({
content: `${newVoice.member.user.tag} (${newVoice.member.id}) has disconnected from <#!${oldVoice.channelId}>.`
});
}
if (!oldVoice.channelId && newVoice.channelId) {
await channel.send({
content: `${newVoice.member.user.tag} (${newVoice.member.id}) has connected to <#!${newVoice.channelId}>.`
});
}
if (oldVoice.mute && !newVoice.mute) {
await channel.send({
content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been unmuted.`
});
}
if (!oldVoice.mute && newVoice.mute) {
await channel.send({
content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been muted.`
});
}
if (oldVoice.deaf && !newVoice.deaf) {
await channel.send({
content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been undeafened.`
});
}
if (!oldVoice.deaf && newVoice.deaf) {
await channel.send({
content: `${newVoice.member.user.tag} (${newVoice.member.id}) has been deafened.`
});
}
} catch (err) {
await errorHandler(bot, "on voice update", err);
}
};

24
src/index.ts Normal file
View File

@ -0,0 +1,24 @@
import { Client } from "discord.js";
import { IntentOptions } from "./config/IntentOptions";
import { connectDatabase } from "./database/connectDatabase";
import { handleEvents } from "./events/_handleEvents";
import { ExtendedClient } from "./interfaces/ExtendedClient";
import { validateEnv } from "./modules/validateEnv";
import { serve } from "./server/serve";
import { loadCommands } from "./utils/loadCommands";
import { loadContexts } from "./utils/loadContexts";
(async () => {
const bot = new Client({ intents: IntentOptions }) as ExtendedClient;
bot.env = validateEnv();
bot.configs = {};
bot.commands = await loadCommands(bot);
bot.contexts = await loadContexts(bot);
await connectDatabase(bot);
handleEvents(bot);
serve(bot);
await bot.login(bot.env.token);
})();

30
src/interfaces/Action.ts Normal file
View File

@ -0,0 +1,30 @@
type PastAction =
| "warned"
| "kicked"
| "banned"
| "muted"
| "unmuted"
| "unbanned"
| "noted"
| "softbanned";
export type Action =
| "warn"
| "kick"
| "ban"
| "mute"
| "unmute"
| "unban"
| "note"
| "softban";
export const ActionToPastTense: { [key in Action]: PastAction } = {
warn: "warned",
kick: "kicked",
ban: "banned",
mute: "muted",
unmute: "unmuted",
unban: "unbanned",
note: "noted",
softban: "softbanned"
};

View File

@ -0,0 +1,11 @@
import { Action } from "./Action";
export interface ActionPayload {
userId: string;
serverId: string;
action: Action;
reason: string;
moderator: string;
duration?: number;
pruneDays?: number | undefined;
}

19
src/interfaces/Command.ts Normal file
View File

@ -0,0 +1,19 @@
import {
SlashCommandBuilder,
SlashCommandOptionsOnlyBuilder,
SlashCommandSubcommandsOnlyBuilder
} from "discord.js";
import { ExtendedClient } from "./ExtendedClient";
import { GuildCommandInteraction } from "./Interactions";
export interface Command {
data:
| SlashCommandOptionsOnlyBuilder
| Omit<SlashCommandBuilder, "addSubcommand" | "addSubcommandGroup">
| SlashCommandSubcommandsOnlyBuilder;
run: (
bot: ExtendedClient,
interaction: GuildCommandInteraction
) => Promise<void>;
}

View File

@ -0,0 +1,8 @@
import { ExtendedClient } from "./ExtendedClient";
import { GuildCommandInteraction } from "./Interactions";
export type CommandHandler = (
bot: ExtendedClient,
interaction: GuildCommandInteraction,
config: ExtendedClient["configs"][""]
) => Promise<void>;

13
src/interfaces/Context.ts Normal file
View File

@ -0,0 +1,13 @@
import { ExtendedClient } from "./ExtendedClient";
import { GuildContextInteraction } from "./Interactions";
export interface Context {
data: {
name: string;
type: 2 | 3;
};
run: (
bot: ExtendedClient,
interaction: GuildContextInteraction
) => Promise<void>;
}

View File

@ -0,0 +1,21 @@
import { PrismaClient, configs } from "@prisma/client";
import { Client, WebhookClient } from "discord.js";
import { Command } from "./Command";
import { Context } from "./Context";
import type { Prometheus } from "../modules/prometheus.js";
export interface ExtendedClient extends Client {
env: {
token: string;
debugHook: WebhookClient;
mongoUri: string;
devMode: boolean;
};
db: PrismaClient;
analytics: Prometheus;
commands: Command[];
contexts: Context[];
configs: { [serverId: string]: Omit<configs, "id"> };
}

View File

@ -0,0 +1,190 @@
/**
* The structure of the NESTED issue data from the GitHub Webhook.
*/
interface GithubIssuePayload {
id: number;
node_id: string;
url: string;
repository_url: string;
html_url: string;
number: number;
state: string;
state_reason: string | null;
title: string;
body: string;
user: GithubUserPayload;
created_at: string;
updated_at: string;
closed_by: GithubUserPayload;
}
interface GithubPullRequestPayload {
html_url: string;
body: string;
number: number;
merged: boolean;
title: string;
user: GithubUserPayload;
}
/**
* Structure of the repo data, sent on pretty much
* every GitHub Webhook payload.
*/
interface GithubRepoPayload {
id: number;
node_id: string;
name: string;
full_name: string;
owner: GithubUserPayload;
private: boolean;
html_url: string;
description: string;
fork: boolean;
url: string;
archive_url: string;
assignees_url: string;
blobs_url: string;
branches_url: string;
collaborators_url: string;
comments_url: string;
commits_url: string;
compare_url: string;
contents_url: string;
contributors_url: string;
deployments_url: string;
downloads_url: string;
events_url: string;
forks_url: string;
git_commits_url: string;
git_refs_url: string;
git_tags_url: string;
git_url: string;
issue_comment_url: string;
issue_events_url: string;
issues_url: string;
keys_url: string;
labels_url: string;
languages_url: string;
merges_url: string;
milestones_url: string;
notifications_url: string;
pulls_url: string;
releases_url: string;
ssh_url: string;
stargazers_url: string;
statuses_url: string;
subscribers_url: string;
subscription_url: string;
tags_url: string;
teams_url: string;
trees_url: string;
clone_url: string;
mirror_url: string;
hooks_url: string;
svn_url: string;
homepage: string;
language: string | null;
forks: number;
forks_count: number;
stargazers_count: number;
watchers_count: number;
watchers: number;
size: number;
default_branch: string;
open_issues_count: number;
open_issues: number;
created_at: string;
}
interface GithubUserPayload {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
name: string;
company: string;
blog: string;
location: string;
email: string;
hireable: boolean;
}
/**
* The structure of the comment data from the Github Webhook.
*/
export interface GithubCommentPayload {
action: string;
issue: GithubIssuePayload;
comment: {
html_url: string;
body: string;
user: GithubUserPayload;
};
repository: GithubRepoPayload;
sender: GithubUserPayload;
}
export interface GithubForkPayload {
forkee: GithubRepoPayload;
repository: GithubRepoPayload;
sender: GithubUserPayload;
}
/**
* The structure of the top level issue data from the GitHub webhook.
*/
export interface GithubIssuesPayload {
action: string;
issue: GithubIssuePayload;
repository: GithubRepoPayload;
sender: GithubUserPayload;
}
/**
* The structure of the ping payload when a new GitHub webhook
* is initialised.
*/
export interface GithubPingPayload {
zen: string;
hook_id: string;
hook: Record<string, unknown>;
repository: GithubRepoPayload;
organization: Record<string, unknown>;
sender: GithubUserPayload;
}
/**
* Structure of the pull request data from the GitHub Webhook.
*/
export interface GithubPullPayload {
action: string;
number: number;
pull_request: GithubPullRequestPayload;
repository: GithubRepoPayload;
sender: GithubUserPayload;
}
/**
* Structure of the star data sent from the GitHub Webhook.
*/
export interface GithubStarPayload {
action: "created" | "deleted";
starred_at: string;
repository: GithubRepoPayload;
sender: GithubUserPayload;
}

View File

@ -0,0 +1,15 @@
import {
ChatInputCommandInteraction,
ContextMenuCommandInteraction,
Guild,
GuildMember
} from "discord.js";
export interface GuildCommandInteraction extends ChatInputCommandInteraction {
guild: Guild;
member: GuildMember;
}
export interface GuildContextInteraction extends ContextMenuCommandInteraction {
guild: Guild;
}

View File

@ -0,0 +1,28 @@
import { ButtonInteraction } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles the logic for the acknowledge button on message reports.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ButtonInteraction} interaction The interaction payload from Discord.
*/
export const handleCopyIdButton = async (
bot: ExtendedClient,
interaction: ButtonInteraction
) => {
try {
await interaction.deferReply({ ephemeral: true });
const id = interaction.customId.split("-")[1];
await interaction.editReply({
content: id || "Unable to parse ID."
});
} catch (err) {
const id = await errorHandler(bot, "handle copy id button", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,43 @@
import { ButtonInteraction } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
/**
* Handles the logic for the acknowledge button on message reports.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ButtonInteraction} interaction The interaction payload from Discord.
*/
export const handleReportAcknowledgeButton = async (
bot: ExtendedClient,
interaction: ButtonInteraction
) => {
try {
await interaction.deferUpdate();
const message = interaction.message;
const embed = message.embeds[0];
await interaction.editReply({
embeds: [
{
title: embed?.title || "wtf",
description: embed?.description || "wtf",
fields: [
...(embed?.fields ?? []),
{
name: "Acknowledged by",
value: `<@${interaction.user.id}>`
}
],
color: 0x00ff00
}
],
components: []
});
} catch (err) {
const id = await errorHandler(bot, "handle report acknowledge button", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,24 @@
/**
* Parses a value/unit pair into a number of milliseconds. For example,
* (1, "seconds") would return one second in milliseconds.
*
* @param {number} value The number of "unit" to convert to milliseconds.
* @param {string} unit The unit of time to convert to milliseconds.
* @returns {number} The number of milliseconds.
*/
export const calculateMuteDuration = (value: number, unit: string) => {
switch (unit) {
case "seconds":
return value * 1000;
case "minutes":
return value * 60000;
case "hours":
return value * 3600000;
case "days":
return value * 86400000;
case "weeks":
return value * 604800000;
default:
return 0;
}
};

View File

@ -0,0 +1,213 @@
import { levels } from "@prisma/client";
import { AttachmentBuilder } from "discord.js";
import nodeHtmlToImage from "node-html-to-image";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
/**
* Creates an image from the user's profile settings, converts it into a Discord
* attachment, and returns it.
*
* @param {ExtendedClient} CamperChan The CamperChan's Discord instance.
* @param {levels} record The user's record from the database.
* @returns {AttachmentBuilder} The attachment, or null on error.
*/
export const generateProfileImage = async (
CamperChan: ExtendedClient,
record: levels
): Promise<AttachmentBuilder | null> => {
try {
const {
avatar,
backgroundColour,
backgroundImage,
colour,
username,
level,
points
} = record;
const html = `
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@font-face {
font-family: "Roboto";
src: url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700");
}
body {
background: url(${backgroundImage}) no-repeat center center fixed;
background-size: cover;
width: 1920px;
height: 1080px;
text-align: center;
font-family: "Roboto", Courier, monospace;
font-size: 75px;
padding: 2.5%;
}
h1 {
font-size: 150px;
}
main {
width: 100%;
height: 100%;
background-color: #${backgroundColour}bf;
color: #${colour};
padding: 2.5%;
border-radius: 100px;
}
.avatar {
width: 250px;
height: 250px;
border-radius: 50%;
}
h2 {
font-size: 125px;
}
.header {
width: 100%;
display: grid;
grid-template-columns: 250px auto;
justify-items: center;
align-items: center;
}
</style>
<body>
<main>
<div class="header">
<img class="avatar" src=${avatar || "https://cdn.freecodecamp.org/platform/universal/fcc_puck_500.jpg"}></img>
<div>
<h1>${username}</h1>
<p>Level ${level} (${points.toLocaleString("en-GB")}xp)</p>
</div>
</div>
</main>
</body>
`;
const alt = `${username} is at level ${level} with ${points.toLocaleString("en-GB")} experience points.`;
const image = await nodeHtmlToImage({
html,
selector: "body",
transparent: true
});
if (!(image instanceof Buffer)) {
return null;
}
const attachment = new AttachmentBuilder(image, {
name: `${username}.png`,
description: alt
});
return attachment;
} catch (err) {
await errorHandler(CamperChan, "generate profile image module", err);
return null;
}
};
/**
* Generates the image for the leaderboard.
*
* @param {ExtendedClient} CamperChan The CamperChan's Discord instance.
* @param {levels} levels The user's record from the database.
* @returns {AttachmentBuilder} The attachment, or null on error.
*/
export const generateLeaderboardImage = async (
CamperChan: ExtendedClient,
levels: (levels & { index: number })[]
): Promise<AttachmentBuilder | null> => {
try {
const html = `
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
@font-face {
font-family: "Roboto";
src: url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700");
}
body {
background: transparent;
height: 4100px;
width: 2510px;
}
img {
width: 250px;
height: 250px;
}
h1 {
font-size: 150px;
}
p {
font-size: 100px;
font-weight: bold;
}
.row {
width: 2500px;
display: grid;
grid-template-columns: 250px 2250px;
height: 400px;
margin: 5px 10px;
justify-items: left;
align-items: center;
}
</style>
<body>
${levels.map(
(l) =>
`<div class="row" style="background-color: #${l.backgroundColour || "0a0a23"}bf;color: #${l.colour || "d0d0d5"};padding: 2.5%;border-radius: 100px;">
<img style="border-radius: 50%;" src=${l.avatar || "https://cdn.freecodecamp.org/platform/universal/fcc_puck_500.jpg"}></img>
<div style="text-align: left;padding-left:100px;">
<h1>#${l.index}. ${l.username}</h1>
<p>Level ${l.level} (${l.points}xp)</p>
</div>
</div>`
)}
</body>
`;
const alt = levels
.map(
(l) =>
`${l.username} is rank ${l.index} at ${l.level} with ${l.points.toLocaleString("en-GB")} experience points.`
)
.join(", ");
const image = await nodeHtmlToImage({
html,
selector: "body",
transparent: true
});
if (!(image instanceof Buffer)) {
return null;
}
const attachment = new AttachmentBuilder(image, {
name: `leaderboard-${levels[0]?.index}.png`,
description: alt
});
return attachment;
} catch (err) {
await errorHandler(CamperChan, "generate leaderboard image module", err);
return null;
}
};

View File

@ -0,0 +1,32 @@
/**
* Checks if a string matches a 6 character hex code.
*
* @param {string} colour The colour code to validate.
* @returns {boolean} If the string is in the correct format.
*/
export const validateColour = (colour: string): boolean => {
return /[\da-f]{6}/gi.test(colour);
};
/**
* Checks if a url points to a valid image.
*
* @param {string} url The URL to validate.
* @returns {boolean} If the URL provides a 2XX response, and if the response content type
* is an image.
*/
export const validateImage = async (url: string): Promise<boolean> => {
const validImage = await fetch(url, {
method: "HEAD"
}).catch(() => null);
if (!validImage) {
return false;
}
if (!validImage.headers.get("content-type")?.startsWith("image/")) {
return false;
}
return true;
};

View File

@ -0,0 +1,39 @@
import { configs } from "@prisma/client";
import { defaultConfig } from "../../config/DefaultConfig";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
/**
* Module to get the server config from the cache, database, or default.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {string} serverId The ID of the server to get the config for.
* @returns {ExtendedClient["config"]} The server config.
*/
export const getConfig = async (
bot: ExtendedClient,
serverId: string
): Promise<Omit<configs, "id">> => {
try {
const exists = bot.configs[serverId];
if (exists) {
return exists;
}
const record = await bot.db.configs.upsert({
where: {
serverId
},
create: {
...defaultConfig,
serverId
},
update: {}
});
bot.configs[serverId] = record;
return record;
} catch (err) {
await errorHandler(bot, "get config", err);
return defaultConfig;
}
};

View File

@ -0,0 +1,40 @@
import { defaultConfig } from "../../config/DefaultConfig";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
/**
* Module to update a config.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {string} serverId The ID of the server to update.
* @param {keyof ExtendedClient["config"]} setting The setting to update.
* @param {number | string} value The value to update the setting to.
* @returns {boolean} True on successful update.
*/
export const setConfig = async (
bot: ExtendedClient,
serverId: string,
setting: keyof ExtendedClient["configs"][""],
value: number | string
): Promise<boolean> => {
try {
const configData = await bot.db.configs.upsert({
where: {
serverId
},
create: {
...defaultConfig,
serverId,
[setting]: value
},
update: {
[setting]: value
}
});
bot.configs[serverId] = configData;
return true;
} catch (err) {
await errorHandler(bot, "set config", err);
return false;
}
};

View File

@ -0,0 +1,46 @@
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
/**
* Checks if a domain is a known source of Discord scams.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {string} domain The domain to validate. DO NOT include the protocol or path.
* @returns {boolean} True if the domain is known as a scam.
*/
export const checkSpamDomain = async (
bot: ExtendedClient,
domain: string
): Promise<boolean> => {
try {
const walshyReq = await fetch("https://bad-domains.walshy.dev/check", {
method: "POST",
headers: {
accept: "application/json",
"X-Identity": "Naomi's mod bot - built by naomi_lgbt"
},
body: JSON.stringify({ domain })
});
const walshyRes = (await walshyReq.json()) as { badDomain: boolean };
if (walshyRes.badDomain) {
return true;
}
const yachtsReq = await fetch(
`https://phish.sinking.yachts/v2/check/${encodeURI(domain)}`,
{
headers: {
accept: "application/json",
"X-Identity": "Naomi's mod bot - built by naomi_lgbt"
}
}
);
const yachtsRes = (await yachtsReq.json()) as boolean;
if (yachtsRes) {
return true;
}
return false;
} catch (err) {
await errorHandler(bot, "load spam domains", err);
return false;
}
};

View File

@ -0,0 +1,35 @@
import { AuditLogEvent, GuildAuditLogsEntry } from "discord.js";
import { Action } from "../../interfaces/Action";
/**
* Module to parse the audit log entry and return the moderation action.
*
* @param {GuildAuditLogsEntry} log The audit log entry payload.
* @returns {Action | null} The mod action string, or null if not found.
*/
export const getModActionFromAuditLog = (
log: GuildAuditLogsEntry
): Action | null => {
const muteChange = log.changes.find(
(change) => change.key === "communication_disabled_until"
);
switch (log.action) {
case AuditLogEvent.MemberBanAdd:
return "ban";
case AuditLogEvent.MemberBanRemove:
return "unban";
case AuditLogEvent.MemberKick:
return "kick";
case AuditLogEvent.MemberUpdate:
if (!muteChange) {
return null;
}
if (muteChange.new) {
return "mute";
}
return "unmute";
default:
return null;
}
};

View File

@ -0,0 +1,55 @@
import { ChatInputCommandInteraction } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
import { isModerator } from "../../utils/isModerator";
import { isGuildCommandInteraction } from "../validateGuildCommands";
/**
* Handles the logic for running slash commands.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ChatInputCommandInteraction} interaction The interaction payload from Discord.
*/
export const handleChatInputCommand = async (
bot: ExtendedClient,
interaction: ChatInputCommandInteraction
) => {
try {
if (!isGuildCommandInteraction(interaction)) {
await interaction.reply({
content: "You can only use commands in a server.",
ephemeral: true
});
return;
}
const command = bot.commands.find(
(c) => c.data.name === interaction.commandName
);
if (!command) {
await interaction.reply({
content: "That's not a valid command. Please contact Naomi.",
ephemeral: true
});
return;
}
if (
(!interaction.member || !isModerator(interaction.member)) &&
!["leaderboard", "rank", "profile", "role", "help", "ping"].includes(
interaction.commandName
)
) {
await interaction.reply({
content: "You must be a moderator to use bot commands.",
ephemeral: true
});
return;
}
await command.run(bot, interaction);
} catch (err) {
const id = await errorHandler(bot, "handle chat input command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,42 @@
import { ContextMenuCommandInteraction } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
import { isGuildContextInteraction } from "../validateGuildCommands";
/**
* Handles the logic for running context commands.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ContextMenuCommandInteraction} interaction The interaction payload from Discord.
*/
export const handleContextMenuCommand = async (
bot: ExtendedClient,
interaction: ContextMenuCommandInteraction
) => {
try {
if (!isGuildContextInteraction(interaction)) {
await interaction.reply({
content: "You can only use this in a server.",
ephemeral: true
});
return;
}
const context = bot.contexts.find(
(c) => c.data.name === interaction.commandName
);
if (!context) {
await interaction.reply({
content: "That's not a valid context. Please contact Naomi.",
ephemeral: true
});
return;
}
await context.run(bot, interaction);
} catch (err) {
const id = await errorHandler(bot, "handle context menu command", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,47 @@
import { ExtendedClient } from "../interfaces/ExtendedClient";
import { checkEntitledGuild } from "../utils/checkEntitledGuild";
import { errorHandler } from "../utils/errorHandler";
/**
* Loops through the list of configured servers to lock security for, and processes the
* API calls.
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const maintainSecurity = async (bot: ExtendedClient) => {
try {
const records = await bot.db.security.findMany();
const date = new Date(new Date().getTime() + 24 * 60 * 60 * 1000);
for (const record of records) {
const guild =
bot.guilds.cache.get(record.serverId) ||
(await bot.guilds.fetch(record.serverId).catch(() => null));
if (!guild) {
continue;
}
const isEntitled = await checkEntitledGuild(bot, guild);
if (!isEntitled) {
continue;
}
await fetch(
`https://discord.com/api/v10/guilds/${record.serverId}/incident-actions`,
{
method: "PUT",
headers: {
Authorization: `Bot ${bot.env.token}`,
"content-type": "application/json"
},
body: JSON.stringify({
dms_disabled_until: record.lockDms ? date : null,
invites_disabled_until: record.lockInvites ? date : null
})
}
).catch(
async (e) =>
await errorHandler(bot, `incident-actions for ${record.serverId}`, e)
);
}
} catch (err) {
await errorHandler(bot, "maintain security", err);
}
};

View File

@ -0,0 +1,127 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
EmbedBuilder,
Message,
ModalSubmitInteraction
} from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
import { getConfig } from "../data/getConfig";
/**
* Handles the submission of the mass ban form.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ModalSubmitInteraction} interaction The interaction payload from Discord.
*/
export const handleMassBanModal = async (
bot: ExtendedClient,
interaction: ModalSubmitInteraction
) => {
try {
await interaction.deferReply({ ephemeral: true });
const { guild } = interaction;
if (!guild) {
await interaction.editReply({
content: "This command can only be used in a guild."
});
return;
}
const rawBanList = interaction.fields.getTextInputValue("mass-ban-ids");
const banList = rawBanList.trim().split(/\b/g);
const reason = interaction.fields.getTextInputValue("reason");
const embed = new EmbedBuilder();
embed.setTitle("Confirm Mass Ban of Following IDs:");
embed.setDescription(banList.join("\n"));
embed.addFields({
name: "Reason",
value: reason
});
const yes = new ButtonBuilder()
.setCustomId("confirm")
.setLabel("Confirm")
.setStyle(ButtonStyle.Success);
const no = new ButtonBuilder()
.setCustomId("cancel")
.setLabel("Cancel")
.setStyle(ButtonStyle.Danger);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(yes, no);
const response = (await interaction.editReply({
embeds: [embed],
components: [row]
})) as Message;
const collector =
response.createMessageComponentCollector<ComponentType.Button>({
filter: (click) => click.user.id === interaction.user.id,
time: 10000,
max: 1
});
collector.on("end", async (clicks) => {
const choice = clicks.first()?.customId;
if (!clicks || clicks.size <= 0 || !choice) {
await interaction.editReply({
content: "This command has timed out.",
embeds: [],
components: []
});
return;
}
if (choice === "confirm") {
for (const id of banList) {
await guild.bans.create(id, {
reason: `Massban by ${interaction.user.tag} for: ${reason}`,
deleteMessageSeconds: 86400
});
}
const config = await getConfig(bot, guild.id);
if (!config.modLogChannel) {
return;
}
const channel =
guild.channels.cache.get(config.modLogChannel) ||
(await guild.channels.fetch(config.modLogChannel));
if (!channel || !("send" in channel)) {
return;
}
embed.setTitle("Mass Ban:");
embed.setAuthor({
name: interaction.user.tag,
iconURL: interaction.user.displayAvatarURL()
});
await channel.send({ embeds: [embed] });
await interaction.editReply({
content: "Mass ban complete.",
embeds: [],
components: []
});
}
if (choice === "cancel") {
interaction.editReply({
content: "Mass ban cancelled.",
embeds: [],
components: []
});
}
});
} catch (err) {
const id = await errorHandler(bot, "handle mass ban modal", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,82 @@
import { ModalSubmitInteraction } from "discord.js";
import { ExtendedClient } from "../../interfaces/ExtendedClient";
import { errorHandler } from "../../utils/errorHandler";
import { getConfig } from "../data/getConfig";
/**
* Handles the submission of the message report form.
*
* @param {ExtendedClient} bot The bot's Discord instance.
* @param {ModalSubmitInteraction} interaction The interaction payload from Discord.
*/
export const handleMessageReportModal = async (
bot: ExtendedClient,
interaction: ModalSubmitInteraction
) => {
try {
await interaction.deferReply({ ephemeral: true });
if (!interaction.guild) {
await interaction.editReply({
content: "This command can only be used in a guild."
});
return;
}
const reportLogId = interaction.customId.split("-")[1] ?? "oops";
const config = await getConfig(bot, interaction.guild.id);
if (!config.messageReportChannel) {
await interaction.editReply({
content: "Reporting has not been set up for this server."
});
return;
}
const channel =
interaction.guild.channels.cache.get(config.messageReportChannel) ||
(await interaction.guild.channels.fetch(config.messageReportChannel));
if (!channel || !("send" in channel)) {
await interaction.editReply({
content: "Reporting channel not found."
});
return;
}
const reportLog = await channel.messages
.fetch(reportLogId)
.catch(() => null);
if (!reportLog) {
await interaction.editReply({
content: "Could not find the report log."
});
return;
}
const embed = reportLog.embeds[0];
await reportLog.edit({
embeds: [
{
title: embed?.title || "wtf",
description: embed?.description || "wtf",
fields: [
...(embed?.fields ?? []),
{
name: "Reason",
value: interaction.fields.getTextInputValue("reason")
}
]
}
]
});
await interaction.editReply({
content: "Your report has been submitted."
});
} catch (err) {
const id = await errorHandler(bot, "handle message report modal", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,53 @@
import { ExtendedClient } from "../interfaces/ExtendedClient";
import { checkEntitledGuild } from "../utils/checkEntitledGuild";
import { errorHandler } from "../utils/errorHandler";
/**
* Fetches the configs from the database, then for each config that
* has a birthday channel set, fetch birthdays. If any are from today,
* post!
*
* @param {ExtendedClient} bot The bot's Discord instance.
*/
export const postBirthdays = async (bot: ExtendedClient) => {
try {
const configs = await bot.db.configs.findMany();
const withChannel = configs.filter((c) => c.birthdayChannel);
for (const record of withChannel) {
const guild =
bot.guilds.cache.get(record.serverId) ||
(await bot.guilds.fetch(record.serverId).catch(() => null));
if (!guild) {
continue;
}
const isEntitled = await checkEntitledGuild(bot, guild);
if (!isEntitled) {
continue;
}
const channel =
guild.channels.cache.get(record.birthdayChannel) ||
(await bot.guilds.fetch(record.birthdayChannel).catch(() => null));
if (!channel || !("send" in channel)) {
continue;
}
const hasBirthdaySet = await bot.db.birthdays.findMany({
where: { serverId: guild.id }
});
const today = new Date();
const todayIn2000 = new Date(
`2000-${today.getMonth() + 1}-${today.getDate()}`
);
const isBirthdayToday = hasBirthdaySet.filter(
(r) => r.birthday === todayIn2000
);
if (!isBirthdayToday.length) {
return;
}
const names = isBirthdayToday.map((r) => `<@${r.userId}>`).join(", ");
await channel.send(`Happy birthday to these lovely people~!\n${names}`);
}
} catch (err) {
await errorHandler(bot, "post birthdays", err);
}
};

71
src/modules/prometheus.ts Normal file
View File

@ -0,0 +1,71 @@
import client, { Counter, Gauge } from "prom-client";
import type { ExtendedClient } from "../interfaces/ExtendedClient";
import { checkEntitledGuild } from "../utils/checkEntitledGuild";
export class Prometheus {
private client = client;
private guilds: Gauge;
private entitled: Gauge;
private commands: Counter;
private users: Gauge;
private errors: Counter;
constructor(bot: ExtendedClient) {
this.guilds = new Gauge({
name: "guilds",
help: "The number of guilds the bot is in."
});
this.entitled = new Gauge({
name: "entitled",
help: "The number of guilds the bot is in."
});
this.updateGuilds(bot);
this.commands = new Counter({
name: "commands",
help: "The number of commands that have been used since last boot."
});
this.users = new Gauge({
name: "users",
help: "The number of users the bot knows."
});
this.users.set(
bot.guilds.cache.reduce(
(members, guild) => members + guild.memberCount,
0
)
);
this.errors = new Counter({
name: "errors",
help: "The number of errors handled by the process."
});
this.client.collectDefaultMetrics();
}
public commandUsed() {
this.commands.inc();
}
public updateGuilds(bot: ExtendedClient) {
this.guilds.set(bot.guilds.cache.size);
}
public updateUsers(bot: ExtendedClient) {
this.users.set(
bot.guilds.cache.reduce(
(members, guild) => members + guild.memberCount,
0
)
);
}
public async updateEntitlements(bot: ExtendedClient) {
const entitled = bot.guilds.cache.filter(
async (g) => await checkEntitledGuild(bot, g)
);
this.entitled.set(entitled.size);
}
public errorHandled() {
this.errors.inc();
}
}

View File

@ -0,0 +1,34 @@
import { CommandHandler } from "../../../interfaces/CommandHandler";
import { errorHandler } from "../../../utils/errorHandler";
import { setConfig } from "../../data/setConfig";
/**
* Sets the ban appeal link for the server.
*/
export const handleAppealLink: CommandHandler = async (bot, interaction) => {
try {
const link = interaction.options.getString("link", true);
const success = await setConfig(
bot,
interaction.guild.id,
"banAppealLink",
link
);
if (success) {
await interaction.editReply({
content: `Members who are banned can appeal at <${link}>.`
});
return;
}
await interaction.editReply({
content: "Failed to set the settings."
});
} catch (err) {
const id = await errorHandler(bot, "automod logging subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,52 @@
import { PermissionFlagsBits } from "discord.js";
import { CommandHandler } from "../../../interfaces/CommandHandler";
import { errorHandler } from "../../../utils/errorHandler";
import { setConfig } from "../../data/setConfig";
/**
* Sets the birthday channel for the server.
*/
export const handleBirthdayChannel: CommandHandler = async (
bot,
interaction
) => {
try {
const channel = interaction.options.getChannel("channel", true);
if (!("send" in channel)) {
await interaction.editReply({
content: "You must specify a text channel!"
});
return;
}
const me = await interaction.guild.members.fetchMe();
if (!me.permissionsIn(channel).has(PermissionFlagsBits.SendMessages)) {
await interaction.editReply({
content: "I can't send messages there. :c"
});
return;
}
const success = await setConfig(
bot,
interaction.guild.id,
"birthdayChannel",
channel.id
);
if (success) {
await interaction.editReply({
content: `Birthdays will be posted in ${channel.toString()}. Members can set their birthdays with the \`/birthday\` command.`
});
return;
}
await interaction.editReply({
content: "Failed to set the settings."
});
} catch (err) {
const id = await errorHandler(bot, "automod logging subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,34 @@
import { CommandHandler } from "../../../interfaces/CommandHandler";
import { errorHandler } from "../../../utils/errorHandler";
import { setConfig } from "../../data/setConfig";
/**
* Sets the invite link for the server.
*/
export const handleInviteLink: CommandHandler = async (bot, interaction) => {
try {
const link = interaction.options.getString("link", true);
const success = await setConfig(
bot,
interaction.guild.id,
"inviteLink",
link
);
if (success) {
await interaction.editReply({
content: `Members who are kicked will be invited back with <${link}>.`
});
return;
}
await interaction.editReply({
content: "Failed to set the settings."
});
} catch (err) {
const id = await errorHandler(bot, "automod logging subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,34 @@
import { CommandHandler } from "../../../interfaces/CommandHandler";
import { errorHandler } from "../../../utils/errorHandler";
import { setConfig } from "../../data/setConfig";
/**
* Sets the role to be assigned when a member joins.
*/
export const handleJoinRole: CommandHandler = async (bot, interaction) => {
try {
const role = interaction.options.getRole("role", true);
const success = await setConfig(
bot,
interaction.guild.id,
"joinRole",
role.id
);
if (success) {
await interaction.editReply({
content: `Members will be given ${role.toString()} when they join..`
});
return;
}
await interaction.editReply({
content: "Failed to set the settings."
});
} catch (err) {
const id = await errorHandler(bot, "config join-role subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,141 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
EmbedBuilder
} from "discord.js";
import { CommandHandler } from "../../../interfaces/CommandHandler";
import { errorHandler } from "../../../utils/errorHandler";
import { getNextIndex, getPreviousIndex } from "../../../utils/getArrayIndex";
/**
* Fetches the automod settings for the given guild.
*/
export const handleList: CommandHandler = async (bot, interaction, config) => {
try {
const embed = new EmbedBuilder();
embed.setTitle("Automod Settings");
embed.addFields([
{
name: "Moderation Log Channel",
value: config.modLogChannel ? `<#${config.modLogChannel}>` : "Not set.",
inline: true
},
{
name: "Event Log Channel",
value: config.eventLogChannel
? `<#${config.eventLogChannel}>`
: "Not set.",
inline: true
},
{
name: "Message Report Channel",
value: config.messageReportChannel
? `<#${config.messageReportChannel}>`
: "Not set.",
inline: true
},
{
name: "Invite Link",
value: config.inviteLink || "None",
inline: true
},
{
name: "Ban Appeal Link",
value: config.banAppealLink || "None",
inline: true
}
]);
const roles = await bot.db.levelRoles.findMany({
where: { serverId: interaction.guild.id }
});
const levelRoles = new EmbedBuilder();
levelRoles.setTitle("Level Roles");
levelRoles.setDescription(
roles
.map((r) => `- <@&${r.roleId}> is assigned at level ${r.level}`)
.join("\n") || "No roles are currently set."
);
const assignRoles = await bot.db.roles.findMany({
where: {
serverId: interaction.guild.id
}
});
const assignRolesEmbed = new EmbedBuilder();
assignRolesEmbed.setTitle("Self-Assignable Roles");
assignRolesEmbed.setDescription(
assignRoles.map((r) => `<@&${r.roleId}>`).join(" ") ||
"No roles are currently set."
);
const embeds = [embed, levelRoles, assignRolesEmbed];
let index = 0;
const nextButton = new ButtonBuilder()
.setCustomId("next")
.setStyle(ButtonStyle.Primary)
.setLabel(
embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed."
)
.setEmoji("▶️");
const prevButton = new ButtonBuilder()
.setCustomId("prev")
.setStyle(ButtonStyle.Primary)
.setLabel(
embeds[getPreviousIndex(embeds, index)]?.data.title || "Unknown embed."
)
.setEmoji("◀️");
const initialRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
prevButton,
nextButton
);
const response = await interaction.editReply({
embeds: [embeds[index] as EmbedBuilder],
components: [initialRow]
});
const collector =
response.createMessageComponentCollector<ComponentType.Button>({
time: 1000 * 60 * 5
});
collector.on("collect", async (i) => {
await i.deferUpdate();
index =
i.customId === "next"
? getNextIndex(embeds, index)
: getPreviousIndex(embeds, index);
prevButton.setLabel(
embeds[getPreviousIndex(embeds, index)]?.data.title || "Unknown embed."
);
nextButton.setLabel(
embeds[getNextIndex(embeds, index)]?.data.title || "Unknown embed."
);
const newRow = new ActionRowBuilder<ButtonBuilder>().addComponents(
prevButton,
nextButton
);
await i.editReply({
embeds: [embeds[index] as EmbedBuilder],
components: [newRow]
});
});
collector.on("end", async () => {
await interaction.editReply({
components: []
});
});
} catch (err) {
const id = await errorHandler(bot, "automod list subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,40 @@
import { logChannelChoicesMap } from "../../../config/LogChannelChoices";
import { CommandHandler } from "../../../interfaces/CommandHandler";
import { ExtendedClient } from "../../../interfaces/ExtendedClient";
import { errorHandler } from "../../../utils/errorHandler";
import { setConfig } from "../../data/setConfig";
/**
* Sets the logging channel for the server.
*/
export const handleLogging: CommandHandler = async (bot, interaction) => {
try {
const logType = interaction.options.getString(
"log-type",
true
) as keyof ExtendedClient["configs"][""];
const channel = interaction.options.getChannel("channel", true);
const success = await setConfig(
bot,
interaction.guild.id,
logType,
channel.id
);
if (success) {
await interaction.editReply({
content: `Your server will log ${logChannelChoicesMap[logType]} in <#${channel.id}>.`
});
return;
}
await interaction.editReply({
content: "Failed to set the settings."
});
} catch (err) {
const id = await errorHandler(bot, "automod logging subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

View File

@ -0,0 +1,47 @@
import { CommandHandler } from "../../../interfaces/CommandHandler";
import { errorHandler } from "../../../utils/errorHandler";
/**
* Toggles a role to be self-assignable or not.
*/
export const handleRole: CommandHandler = async (bot, interaction) => {
try {
const role = interaction.options.getRole("role", true);
const exists = await bot.db.roles.findUnique({
where: {
serverId_roleId: {
serverId: interaction.guild.id,
roleId: role.id
}
}
});
if (exists) {
await bot.db.roles.delete({
where: {
serverId_roleId: {
serverId: interaction.guild.id,
roleId: role.id
}
}
});
await interaction.editReply({
content: `Your <@&${role.id}> role is no longer self-assignable.`
});
return;
}
await bot.db.roles.create({
data: {
serverId: interaction.guild.id,
roleId: role.id
}
});
await interaction.editReply({
content: `Your <@&${role.id}> role is now self-assignable.`
});
} catch (err) {
const id = await errorHandler(bot, "automod logging subcommand", err);
await interaction.editReply({
content: `Something went wrong. Please [join our support server](https://chat.naomi.lgbt) and provide this ID: \`${id}\``
});
}
};

Some files were not shown because too many files have changed in this diff Show More