Adds ICS
This commit is contained in:
parent
441715675c
commit
a79898b2e9
48 changed files with 2062 additions and 1503 deletions
3
README.md
Normal file
3
README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# The PnP Scheduler Discord-Bot
|
||||
|
||||
Invite: https://discord.com/oauth2/authorize?client_id=1347546897170173985
|
||||
393
package-lock.json
generated
393
package-lock.json
generated
|
|
@ -18,9 +18,11 @@
|
|||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"esbuild": "^0.25.0",
|
||||
"ics": "^3.8.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"log4js": "^6.9.1",
|
||||
"node-cron": "^4.0.7",
|
||||
"node-ical": "^0.20.1",
|
||||
"object-path-set": "^1.0.2",
|
||||
"svg2img": "^1.0.0-beta.2"
|
||||
},
|
||||
|
|
@ -2048,6 +2050,12 @@
|
|||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atob": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
|
||||
|
|
@ -2060,6 +2068,17 @@
|
|||
"node": ">= 4.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
|
||||
"integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
|
@ -2193,6 +2212,19 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
|
@ -2255,6 +2287,18 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
|
|
@ -2343,6 +2387,15 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
|
||||
|
|
@ -2405,6 +2458,20 @@
|
|||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/end-of-stream": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
|
|
@ -2414,6 +2481,51 @@
|
|||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
|
||||
|
|
@ -2820,6 +2932,22 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
|
|
@ -2840,6 +2968,52 @@
|
|||
"node": ">=6 <7 || >=8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gifwrap": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/gifwrap/-/gifwrap-0.9.4.tgz",
|
||||
|
|
@ -2892,6 +3066,18 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
|
|
@ -2915,6 +3101,56 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ics": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/ics/-/ics-3.8.1.tgz",
|
||||
"integrity": "sha512-UqQlfkajfhrS4pUGQfGIJMYz/Jsl/ob3LqcfEhUmLbwumg+ZNkU0/6S734Vsjq3/FYNpEcZVKodLBoe+zBM69g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.1.23",
|
||||
"runes2": "^1.1.2",
|
||||
"yup": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
|
|
@ -3225,6 +3461,15 @@
|
|||
"integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
@ -3261,6 +3506,27 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
|
|
@ -3321,12 +3587,51 @@
|
|||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.48",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
|
||||
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
|
|
@ -3361,6 +3666,18 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-ical": {
|
||||
"version": "0.20.1",
|
||||
"resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.20.1.tgz",
|
||||
"integrity": "sha512-NrXgzDJd6XcyX9kDMJVA3xYCZmntY7ghA2BOdBeYr3iu8tydHOAb+68jPQhF9V2CRQ0/386X05XhmLzQUN0+Hw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"axios": "^1.7.7",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"rrule": "2.8.1",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-path-set": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/object-path-set/-/object-path-set-1.0.2.tgz",
|
||||
|
|
@ -3598,6 +3915,18 @@
|
|||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pump": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
|
|
@ -3757,6 +4086,15 @@
|
|||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rrule": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rrule/-/rrule-2.8.1.tgz",
|
||||
"integrity": "sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||
|
|
@ -3781,6 +4119,12 @@
|
|||
"queue-microtask": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/runes2": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz",
|
||||
"integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
|
|
@ -3998,6 +4342,12 @@
|
|||
"integrity": "sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
|
|
@ -4034,6 +4384,12 @@
|
|||
"url": "https://github.com/sponsors/Borewit"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||
|
|
@ -4084,6 +4440,18 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
|
|
@ -4171,6 +4539,19 @@
|
|||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
|
@ -4285,6 +4666,18 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
|
||||
"integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,11 @@
|
|||
"discord.js": "^14.18.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"esbuild": "^0.25.0",
|
||||
"ics": "^3.8.1",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"log4js": "^6.9.1",
|
||||
"node-cron": "^4.0.7",
|
||||
"node-ical": "^0.20.1",
|
||||
"object-path-set": "^1.0.2",
|
||||
"svg2img": "^1.0.0-beta.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,29 +1,28 @@
|
|||
import {Class} from "../types/Class";
|
||||
|
||||
export class Container {
|
||||
static instance: Container;
|
||||
|
||||
private instances: Map<string, object> = new Map();
|
||||
|
||||
public set<T extends Class>(instance: T, name: string|null = null): void
|
||||
{
|
||||
const settingName = name ?? instance.constructor.name;
|
||||
this.instances.set(settingName.toLowerCase(), instance);
|
||||
}
|
||||
|
||||
public get<T extends Class>(name: string): T
|
||||
{
|
||||
return <T>this.instances.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
static getInstance(): Container {
|
||||
if (!Container.instance) {
|
||||
Container.instance = new Container();
|
||||
}
|
||||
|
||||
return Container.instance;
|
||||
}
|
||||
public static get<T>(name: string): T {
|
||||
return Container.instance.get<T>(name);
|
||||
static instance: Container;
|
||||
|
||||
private instances: Map<string, object> = new Map();
|
||||
|
||||
public set<T extends Class>(instance: T, name: string | null = null): void {
|
||||
const settingName = name ?? instance.constructor.name;
|
||||
this.instances.set(settingName.toLowerCase(), instance);
|
||||
}
|
||||
|
||||
public get<T extends Class>(name: string): T {
|
||||
return <T>this.instances.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
static getInstance(): Container {
|
||||
if (!Container.instance) {
|
||||
Container.instance = new Container();
|
||||
}
|
||||
|
||||
return Container.instance;
|
||||
}
|
||||
|
||||
public static get<T>(name: string): T {
|
||||
return Container.instance.get<T>(name);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,69 +11,77 @@ import {IconCache} from "../Icons/IconCache";
|
|||
import {EventHandler} from "../Events/EventHandler";
|
||||
import {InteractionRouter} from "../Discord/InteractionRouter";
|
||||
import Commands from "../Discord/Commands/Commands";
|
||||
import {CommandDeployer} from "../Discord/CommandDeployer";
|
||||
import {REST} from "discord.js";
|
||||
import {log} from "node:util";
|
||||
|
||||
export enum ServiceHint {
|
||||
App,
|
||||
Deploy
|
||||
App,
|
||||
Deploy
|
||||
}
|
||||
|
||||
export class Services {
|
||||
public static setup(container: Container, hint: ServiceHint) {
|
||||
const env = new Environment();
|
||||
env.setup();
|
||||
container.set<Environment>(env);
|
||||
|
||||
const logger = this.setupLogger(hint);
|
||||
container.set<Logger>(logger, 'logger');
|
||||
public static setup(container: Container, hint: ServiceHint) {
|
||||
const env = new Environment();
|
||||
env.setup();
|
||||
container.set<Environment>(env);
|
||||
|
||||
const logger = this.setupLogger(hint);
|
||||
container.set<Logger>(logger, 'logger');
|
||||
|
||||
const database = new DatabaseConnection(env.database);
|
||||
container.set<DatabaseConnection>(database);
|
||||
|
||||
const restClient = new REST();
|
||||
const commands = new Commands();
|
||||
const discordClient = new DiscordClient(
|
||||
env.discord.clientId,
|
||||
new InteractionRouter(commands, logger),
|
||||
new CommandDeployer(env.discord.clientId, restClient, commands, logger),
|
||||
logger,
|
||||
restClient
|
||||
);
|
||||
container.set<DiscordClient>(discordClient);
|
||||
|
||||
const iconCache = new IconCache(discordClient);
|
||||
container.set<IconCache>(iconCache);
|
||||
|
||||
container.set<EventHandler>(new EventHandler());
|
||||
this.setupRepositories(container);
|
||||
}
|
||||
|
||||
private static setupRepositories(container: Container) {
|
||||
const db = container.get<DatabaseConnection>(DatabaseConnection.name);
|
||||
container.set<GroupRepository>(new GroupRepository(db));
|
||||
container.set<PlaydateRepository>(new PlaydateRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
container.set<GroupConfigurationRepository>(new GroupConfigurationRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
}
|
||||
|
||||
private static setupLogger(hint: ServiceHint): Logger {
|
||||
|
||||
configure({
|
||||
appenders: {
|
||||
out: {type: "stdout"},
|
||||
appLogFile: {type: "file", filename: path.resolve("logs/run.log")},
|
||||
deployLogFile: {type: "file", filename: path.resolve("logs/deploy.log")},
|
||||
},
|
||||
categories: {
|
||||
default: {appenders: ['out'], level: 'debug'},
|
||||
app: {appenders: ["out", "appLogFile"], level: "debug"},
|
||||
deploy: {appenders: ["out", "deployLogFile"], level: "debug"},
|
||||
}
|
||||
})
|
||||
|
||||
let loggername = '';
|
||||
switch (hint) {
|
||||
case ServiceHint.App:
|
||||
loggername = "app";
|
||||
break;
|
||||
case ServiceHint.Deploy:
|
||||
loggername = "deploy";
|
||||
break;
|
||||
|
||||
const database = new DatabaseConnection(env.database);
|
||||
container.set<DatabaseConnection>(database);
|
||||
|
||||
const discordClient = new DiscordClient(
|
||||
env.discord.clientId,
|
||||
new InteractionRouter(new Commands(), logger)
|
||||
);
|
||||
container.set<DiscordClient>(discordClient);
|
||||
|
||||
const iconCache = new IconCache(discordClient);
|
||||
container.set<IconCache>(iconCache);
|
||||
|
||||
container.set<EventHandler>(new EventHandler());
|
||||
this.setupRepositories(container);
|
||||
}
|
||||
|
||||
private static setupRepositories(container: Container) {
|
||||
const db = container.get<DatabaseConnection>(DatabaseConnection.name);
|
||||
container.set<GroupRepository>(new GroupRepository(db));
|
||||
container.set<PlaydateRepository>(new PlaydateRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
container.set<GroupConfigurationRepository>(new GroupConfigurationRepository(db, container.get<GroupRepository>(GroupRepository.name)))
|
||||
}
|
||||
|
||||
private static setupLogger(hint: ServiceHint): Logger {
|
||||
|
||||
configure({
|
||||
appenders: {
|
||||
out: { type: "stdout" },
|
||||
appLogFile: { type: "file", filename: path.resolve("logs/run.log")},
|
||||
deployLogFile: { type: "file", filename: path.resolve("logs/deploy.log")},
|
||||
},
|
||||
categories: {
|
||||
default: { appenders: ['out'], level: 'debug' },
|
||||
app: { appenders: ["out", "appLogFile"], level: "debug" },
|
||||
deploy: { appenders: ["out", "deployLogFile"], level: "debug" },
|
||||
}
|
||||
})
|
||||
|
||||
let loggername = '';
|
||||
switch (hint) {
|
||||
case ServiceHint.App:
|
||||
loggername = "app";
|
||||
break;
|
||||
case ServiceHint.Deploy:
|
||||
loggername = "deploy";
|
||||
break;
|
||||
|
||||
}
|
||||
return getLogger(loggername);
|
||||
}
|
||||
return getLogger(loggername);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,38 +5,39 @@ import {Container} from "../Container/Container";
|
|||
import {Logger} from "log4js";
|
||||
|
||||
export class DatabaseConnection {
|
||||
private database: Sqlite3.Database;
|
||||
|
||||
constructor(env: DatabaseEnvironment) {
|
||||
this.database = new Database(env.path, {
|
||||
nativeBinding: "node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||
})
|
||||
this.database.pragma('journal_mode = WAL');
|
||||
}
|
||||
|
||||
public execute(query: string, ...args: unknown[]): Sqlite3.RunResult {
|
||||
try {
|
||||
const preparedQuery = this.database.prepare(query);
|
||||
return preparedQuery.run(args);
|
||||
} catch (error) {
|
||||
Container.get<Logger>("logger").error("Failed to execute database connection", error, query, args);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
private database: Sqlite3.Database;
|
||||
|
||||
public fetch<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: unknown[]): Result|undefined {
|
||||
const preparedQuery = this.database.prepare<BindParameters, Result>(query);
|
||||
return preparedQuery.get(args);
|
||||
}
|
||||
public fetchAll<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: unknown[]): Result[] {
|
||||
const preparedQuery = this.database.prepare<BindParameters, Result>(query);
|
||||
return preparedQuery.all(args);
|
||||
}
|
||||
|
||||
public hasTable(tableName: string): boolean {
|
||||
const sql = "SELECT COUNT(*) as tableCount FROM sqlite_master WHERE type='table' AND name=? LIMIT 1;";
|
||||
|
||||
const result = this.fetch<string[], {tableCount: number}>(sql, tableName);
|
||||
return result != undefined && result.tableCount > 0;
|
||||
constructor(env: DatabaseEnvironment) {
|
||||
this.database = new Database(env.path, {
|
||||
nativeBinding: "node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||
})
|
||||
this.database.pragma('journal_mode = WAL');
|
||||
}
|
||||
|
||||
public execute(query: string, ...args: unknown[]): Sqlite3.RunResult {
|
||||
try {
|
||||
const preparedQuery = this.database.prepare(query);
|
||||
return preparedQuery.run(args);
|
||||
} catch (error) {
|
||||
Container.get<Logger>("logger").error("Failed to execute database connection", error, query, args);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public fetch<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: unknown[]): Result | undefined {
|
||||
const preparedQuery = this.database.prepare<BindParameters, Result>(query);
|
||||
return preparedQuery.get(args);
|
||||
}
|
||||
|
||||
public fetchAll<BindParameters extends unknown[] | {} = unknown[], Result = unknown>(query: string, ...args: unknown[]): Result[] {
|
||||
const preparedQuery = this.database.prepare<BindParameters, Result>(query);
|
||||
return preparedQuery.all(args);
|
||||
}
|
||||
|
||||
public hasTable(tableName: string): boolean {
|
||||
const sql = "SELECT COUNT(*) as tableCount FROM sqlite_master WHERE type='table' AND name=? LIMIT 1;";
|
||||
|
||||
const result = this.fetch<string[], { tableCount: number }>(sql, tableName);
|
||||
return result != undefined && result.tableCount > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
export type DatabaseColumnDefinition = {
|
||||
name: string;
|
||||
type: string;
|
||||
primaryKey?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
notNull?: boolean;
|
||||
options?: string;
|
||||
name: string;
|
||||
type: string;
|
||||
primaryKey?: boolean;
|
||||
autoIncrement?: boolean;
|
||||
notNull?: boolean;
|
||||
options?: string;
|
||||
}
|
||||
|
||||
export type DatabaseDefinition = {
|
||||
name: string;
|
||||
columns: DatabaseColumnDefinition[];
|
||||
name: string;
|
||||
columns: DatabaseColumnDefinition[];
|
||||
}
|
||||
|
|
@ -4,78 +4,82 @@ import {Container} from "../Container/Container";
|
|||
import {Logger} from "log4js";
|
||||
|
||||
export class DatabaseUpdater {
|
||||
constructor(private readonly database: DatabaseConnection) {}
|
||||
|
||||
public ensureAvaliablity(definitions: Iterable<DatabaseDefinition>) {
|
||||
for (const definition of definitions) {
|
||||
this.ensureDefinition(definition);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureDefinition(definition: DatabaseDefinition) {
|
||||
if (this.database.hasTable(definition.name)) {
|
||||
this.ensureTableColumns(definition);
|
||||
return;
|
||||
}
|
||||
|
||||
this.createTable(definition);
|
||||
}
|
||||
|
||||
private ensureTableColumns(definition: DatabaseDefinition) {
|
||||
const DBSQLColumns = this.database.fetchAll<object, {name: string, type: string}>(
|
||||
`PRAGMA table_info("${definition.name}")`
|
||||
);
|
||||
|
||||
if (!DBSQLColumns) {
|
||||
Container.get<Logger>("logger").log("Request failed...");
|
||||
return;
|
||||
}
|
||||
|
||||
const missingColumns = definition.columns.filter(
|
||||
(column: DatabaseColumnDefinition) => {
|
||||
return !DBSQLColumns.some((dbColumn: DatabaseColumnDefinition) => {
|
||||
return column.name === dbColumn.name
|
||||
});
|
||||
}
|
||||
)
|
||||
|
||||
if (missingColumns.length < 1) {
|
||||
Container.get<Logger>("logger").log(`No new columns found for ${definition.name}`)
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsSQL = missingColumns.map((column: DatabaseColumnDefinition) => {
|
||||
const values = [
|
||||
"ADD",
|
||||
column.name,
|
||||
column.type,
|
||||
column.primaryKey ? `PRIMARY KEY` : '',
|
||||
column.notNull ? 'NOT NULL' : '',
|
||||
column.autoIncrement ? 'AUTOINCREMENT' : '',
|
||||
]
|
||||
constructor(private readonly database: DatabaseConnection) {
|
||||
}
|
||||
|
||||
return values.join(' ');
|
||||
}).join(', ');
|
||||
|
||||
const sql = `ALTER TABLE ${definition.name} ${columnsSQL}`;
|
||||
this.database.execute(sql);
|
||||
public ensureAvaliablity(definitions: Iterable<DatabaseDefinition>) {
|
||||
for (const definition of definitions) {
|
||||
this.ensureDefinition(definition);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureDefinition(definition: DatabaseDefinition) {
|
||||
if (this.database.hasTable(definition.name)) {
|
||||
this.ensureTableColumns(definition);
|
||||
return;
|
||||
}
|
||||
|
||||
this.createTable(definition);
|
||||
}
|
||||
|
||||
private createTable(definition: DatabaseDefinition) {
|
||||
const columnsSQL = definition.columns.map((column: DatabaseColumnDefinition) => {
|
||||
const values = [
|
||||
column.name,
|
||||
column.type,
|
||||
column.primaryKey ? `PRIMARY KEY` : '',
|
||||
column.notNull ? 'NOT NULL' : '',
|
||||
column.autoIncrement ? 'AUTOINCREMENT' : '',
|
||||
]
|
||||
|
||||
return values.join(' ');
|
||||
}).join(', ');
|
||||
|
||||
const sql = `CREATE TABLE IF NOT EXISTS ${definition.name} (${columnsSQL})`;
|
||||
this.database.execute(sql);
|
||||
private ensureTableColumns(definition: DatabaseDefinition) {
|
||||
const DBSQLColumns = this.database.fetchAll<object, { name: string, type: string }>(
|
||||
`PRAGMA table_info("${definition.name}")`
|
||||
);
|
||||
|
||||
if (!DBSQLColumns) {
|
||||
Container.get<Logger>("logger").log("Request failed...");
|
||||
return;
|
||||
}
|
||||
|
||||
const missingColumns = definition.columns.filter(
|
||||
(column: DatabaseColumnDefinition) => {
|
||||
return !DBSQLColumns.some((dbColumn: DatabaseColumnDefinition) => {
|
||||
return column.name === dbColumn.name
|
||||
});
|
||||
}
|
||||
)
|
||||
|
||||
if (missingColumns.length < 1) {
|
||||
Container.get<Logger>("logger").log(`No new columns found for ${definition.name}`)
|
||||
return;
|
||||
}
|
||||
|
||||
const columnsSQL = missingColumns.map((column: DatabaseColumnDefinition) => {
|
||||
const values = [
|
||||
"ADD",
|
||||
column.name,
|
||||
column.type,
|
||||
column.primaryKey ? `PRIMARY KEY` : '',
|
||||
column.notNull ? 'NOT NULL' : '',
|
||||
column.autoIncrement ? 'AUTOINCREMENT' : '',
|
||||
]
|
||||
|
||||
return values.join(' ');
|
||||
}).join(', ');
|
||||
|
||||
const sql = `ALTER TABLE ${definition.name} ${columnsSQL}`;
|
||||
this.database.execute(sql);
|
||||
}
|
||||
|
||||
|
||||
private createTable(definition: DatabaseDefinition) {
|
||||
const columnsSQL = definition.columns.map((column: DatabaseColumnDefinition) => {
|
||||
const values = [
|
||||
column.name,
|
||||
column.type,
|
||||
column.primaryKey ? `PRIMARY KEY` : '',
|
||||
column.notNull ? 'NOT NULL' : '',
|
||||
column.autoIncrement ? 'AUTOINCREMENT' : '',
|
||||
]
|
||||
|
||||
return values.join(' ');
|
||||
}).join(', ');
|
||||
|
||||
const sql = `CREATE TABLE IF NOT EXISTS ${definition.name}
|
||||
(
|
||||
${columnsSQL}
|
||||
)`;
|
||||
this.database.execute(sql);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,9 @@ import Playdate from "./tables/Playdate";
|
|||
import GroupConfiguration from "./tables/GroupConfiguration";
|
||||
|
||||
const definitions = new Set<DatabaseDefinition>([
|
||||
Groups,
|
||||
Playdate,
|
||||
GroupConfiguration
|
||||
Groups,
|
||||
Playdate,
|
||||
GroupConfiguration
|
||||
]);
|
||||
|
||||
export default definitions;
|
||||
|
|
@ -1,34 +1,34 @@
|
|||
import {DatabaseDefinition} from "../DatabaseDefinition";
|
||||
|
||||
export type DBGroupConfiguration = {
|
||||
id: number;
|
||||
groupid: number;
|
||||
key: string;
|
||||
value: string;
|
||||
id: number;
|
||||
groupid: number;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const dbDefinition: DatabaseDefinition = {
|
||||
name: "groupConfiguration",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: "groupid",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "VARCHAR(128)",
|
||||
}
|
||||
]
|
||||
name: "groupConfiguration",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: "groupid",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "key",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "VARCHAR(128)",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default dbDefinition;
|
||||
|
|
@ -1,39 +1,39 @@
|
|||
import {DatabaseDefinition} from "../DatabaseDefinition";
|
||||
|
||||
export type DBGroup = {
|
||||
id: number;
|
||||
name: string;
|
||||
server: string;
|
||||
leader: string;
|
||||
role: string;
|
||||
id: number;
|
||||
name: string;
|
||||
server: string;
|
||||
leader: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
const dbDefinition: DatabaseDefinition = {
|
||||
name: "groups",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: "server",
|
||||
type: "VARCHAR(32)"
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "leader",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "VARCHAR(32)",
|
||||
}
|
||||
]
|
||||
name: "groups",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: "server",
|
||||
type: "VARCHAR(32)"
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "leader",
|
||||
type: "VARCHAR(32)",
|
||||
},
|
||||
{
|
||||
name: "role",
|
||||
type: "VARCHAR(32)",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default dbDefinition;
|
||||
|
|
@ -1,35 +1,35 @@
|
|||
import {DatabaseDefinition} from "../DatabaseDefinition";
|
||||
|
||||
export type DBPlaydate = {
|
||||
id: number;
|
||||
groupid: number;
|
||||
id: number;
|
||||
groupid: number;
|
||||
|
||||
time_from: number;
|
||||
time_to: number;
|
||||
time_from: number;
|
||||
time_to: number;
|
||||
}
|
||||
|
||||
const dbDefinition: DatabaseDefinition = {
|
||||
name: "playdates",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: "groupid",
|
||||
type: "INTEGER",
|
||||
},
|
||||
{
|
||||
name: "time_from",
|
||||
type: "TIMESTAMP",
|
||||
},
|
||||
{
|
||||
name: "time_to",
|
||||
type: "TIMESTAMP",
|
||||
}
|
||||
]
|
||||
name: "playdates",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "INTEGER",
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
name: "groupid",
|
||||
type: "INTEGER",
|
||||
},
|
||||
{
|
||||
name: "time_from",
|
||||
type: "TIMESTAMP",
|
||||
},
|
||||
{
|
||||
name: "time_to",
|
||||
type: "TIMESTAMP",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export default dbDefinition;
|
||||
|
|
@ -1,36 +1,28 @@
|
|||
import {DiscordClient} from "./DiscordClient";
|
||||
import {Logger} from "log4js";
|
||||
import {Routes, Snowflake} from "discord.js";
|
||||
import {REST, Routes, Snowflake} from "discord.js";
|
||||
import Commands from "./Commands/Commands";
|
||||
|
||||
export class CommandDeployer {
|
||||
constructor(
|
||||
private readonly client: DiscordClient,
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
}
|
||||
|
||||
public async deployAvailableServers() {
|
||||
const commandInfos: object[] = [];
|
||||
this.client.Commands.allCommands.forEach((command) => {
|
||||
commandInfos.push(command.definition().toJSON())
|
||||
})
|
||||
|
||||
const guilds = await this.client.RESTClient.get(Routes.userGuilds());
|
||||
const deployments = guilds.map(guild => {
|
||||
return this.deployServer(commandInfos, guild.id)
|
||||
})
|
||||
|
||||
await Promise.all(deployments);
|
||||
}
|
||||
|
||||
private async deployServer(commandInfos: object[], serverId: Snowflake) {
|
||||
this.logger.log(`Started refreshing ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
constructor(
|
||||
private readonly applicationId: string,
|
||||
private readonly restClient: REST,
|
||||
private readonly commands: Commands,
|
||||
private readonly logger: Logger
|
||||
) {
|
||||
}
|
||||
|
||||
// The put method is used to fully refresh all commands in the guild with the current set
|
||||
await this.client.RESTClient.put(
|
||||
Routes.applicationGuildCommands(this.client.ApplicationId, serverId),
|
||||
{ body: commandInfos },
|
||||
);
|
||||
this.logger.log(`Successfully reloaded ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
}
|
||||
public async deployServer(serverId: Snowflake) {
|
||||
const commandInfos = this.commands.getJsonDefinitions();
|
||||
|
||||
this.logger.debug(`Started refreshing ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
|
||||
// The put method is used to fully refresh all commands in the guild with the current set
|
||||
await this.restClient.put(
|
||||
Routes.applicationGuildCommands(this.applicationId, serverId),
|
||||
{body: commandInfos},
|
||||
);
|
||||
|
||||
this.logger.debug(`Successfully reloaded ${commandInfos.length} application (/) commands for ${serverId}.`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import {
|
||||
AutocompleteInteraction,
|
||||
CommandInteraction,
|
||||
GuildMember, SlashCommandIntegerOption,
|
||||
AutocompleteInteraction,
|
||||
CommandInteraction,
|
||||
GuildMember, SlashCommandIntegerOption,
|
||||
} from "discord.js";
|
||||
import {Container} from "../../Container/Container";
|
||||
import {GroupRepository} from "../../Repositories/GroupRepository";
|
||||
|
|
@ -9,36 +9,36 @@ import {GroupModel} from "../../Models/GroupModel";
|
|||
import {UserError} from "../UserError";
|
||||
|
||||
export class GroupSelection {
|
||||
public static createOptionSetup(): SlashCommandIntegerOption {
|
||||
return new SlashCommandIntegerOption()
|
||||
.setName("group")
|
||||
.setDescription("Defines the group this action is for")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
public static createOptionSetup(): SlashCommandIntegerOption {
|
||||
return new SlashCommandIntegerOption()
|
||||
.setName("group")
|
||||
.setDescription("Defines the group this action is for")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
}
|
||||
|
||||
public static async handleAutocomplete(interaction: AutocompleteInteraction, onlyLeaders: boolean = false): Promise<void> {
|
||||
const value = interaction.options.getFocused();
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
|
||||
await interaction.respond(
|
||||
groups
|
||||
.filter((group) => group.name.startsWith(value))
|
||||
.map((group) => ({name: group.name, value: group.id}))
|
||||
)
|
||||
}
|
||||
|
||||
public static getGroup(interaction: CommandInteraction | AutocompleteInteraction): GroupModel {
|
||||
const groupname = interaction.options.get("group", true);
|
||||
if (!groupname) {
|
||||
throw new UserError("No group name provided");
|
||||
}
|
||||
|
||||
public static async handleAutocomplete(interaction: AutocompleteInteraction, onlyLeaders: boolean = false): Promise<void> {
|
||||
const value = interaction.options.getFocused();
|
||||
const repo = Container.get<GroupRepository>(GroupRepository.name);
|
||||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member, onlyLeaders);
|
||||
await interaction.respond(
|
||||
groups
|
||||
.filter((group) => group.name.startsWith(value))
|
||||
.map((group) => ({name: group.name, value: group.id }))
|
||||
)
|
||||
}
|
||||
|
||||
public static getGroup(interaction: CommandInteraction|AutocompleteInteraction): GroupModel {
|
||||
const groupname = interaction.options.get("group", true);
|
||||
if (!groupname) {
|
||||
throw new UserError("No group name provided");
|
||||
}
|
||||
|
||||
const group = Container.get<GroupRepository>(GroupRepository.name).getById(<number>(groupname.value ?? 0));
|
||||
if (!group) {
|
||||
throw new UserError("No group found");
|
||||
}
|
||||
|
||||
return <GroupModel>group;
|
||||
|
||||
const group = Container.get<GroupRepository>(GroupRepository.name).getById(<number>(groupname.value ?? 0));
|
||||
if (!group) {
|
||||
throw new UserError("No group found");
|
||||
}
|
||||
|
||||
return <GroupModel>group;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
import {ChatInputCommandInteraction, Interaction, SlashCommandBuilder} from "discord.js";
|
||||
|
||||
export interface Command {
|
||||
definition(): SlashCommandBuilder;
|
||||
definition(): SlashCommandBuilder;
|
||||
}
|
||||
|
||||
export interface ChatInteractionCommand {
|
||||
execute(interaction: ChatInputCommandInteraction): Promise<void>;
|
||||
execute(interaction: ChatInputCommandInteraction): Promise<void>;
|
||||
}
|
||||
|
||||
export interface AutocompleteCommand {
|
||||
handleAutocomplete(interaction: Interaction): Promise<void>;
|
||||
handleAutocomplete(interaction: Interaction): Promise<void>;
|
||||
}
|
||||
|
||||
export type CommandUnion =
|
||||
Command | Partial<ChatInteractionCommand> | Partial<AutocompleteCommand>;
|
||||
export type CommandUnion =
|
||||
Command | Partial<ChatInteractionCommand> | Partial<AutocompleteCommand>;
|
||||
|
|
@ -2,40 +2,54 @@ import {HelloWorldCommand} from "./HelloWorldCommand";
|
|||
import {Command, CommandUnion} from "./Command";
|
||||
import {GroupCommand} from "./Groups";
|
||||
import {PlaydatesCommand} from "./Playdates";
|
||||
import {RESTPostAPIChatInputApplicationCommandsJSONBody} from "discord.js";
|
||||
import {Nullable} from "../../types/Nullable";
|
||||
|
||||
const commands: Set<Command> = new Set<Command>([
|
||||
new HelloWorldCommand(),
|
||||
new GroupCommand(),
|
||||
new PlaydatesCommand()
|
||||
new HelloWorldCommand(),
|
||||
new GroupCommand(),
|
||||
new PlaydatesCommand()
|
||||
]);
|
||||
|
||||
export default class Commands {
|
||||
private mappedCommands: Map<string, Command> = new Map<string, Command>();
|
||||
|
||||
constructor() {
|
||||
this.mappedCommands = this.getMap();
|
||||
private mappedCommands: Map<string, Command> = new Map<string, Command>();
|
||||
private definitions: Nullable<RESTPostAPIChatInputApplicationCommandsJSONBody[]>;
|
||||
|
||||
constructor() {
|
||||
this.mappedCommands = this.getMap();
|
||||
}
|
||||
|
||||
public getCommand(commandName: string): CommandUnion | undefined {
|
||||
if (!this.mappedCommands.has(commandName)) {
|
||||
throw new Error(`Unknown command: ${commandName}`);
|
||||
}
|
||||
|
||||
public getCommand(commandName: string): CommandUnion|undefined {
|
||||
if (!this.mappedCommands.has(commandName)) {
|
||||
throw new Error(`Unknown command: ${commandName}`);
|
||||
}
|
||||
|
||||
return this.mappedCommands.get(commandName);
|
||||
|
||||
return this.mappedCommands.get(commandName);
|
||||
}
|
||||
|
||||
public get allCommands(): Set<Command> {
|
||||
return commands;
|
||||
}
|
||||
|
||||
public getJsonDefinitions(): RESTPostAPIChatInputApplicationCommandsJSONBody[] {
|
||||
if (this.definitions) {
|
||||
return this.definitions;
|
||||
}
|
||||
|
||||
public get allCommands(): Set<Command> {
|
||||
return commands;
|
||||
}
|
||||
|
||||
private getMap(): Map<string, Command>
|
||||
{
|
||||
const map = new Map<string, Command>();
|
||||
for (const command of commands) {
|
||||
const definition = command.definition()
|
||||
map.set(definition.name, command);
|
||||
}
|
||||
|
||||
return map;
|
||||
|
||||
this.definitions = [];
|
||||
commands.forEach((command) => {
|
||||
this.definitions?.push(command.definition().toJSON())
|
||||
})
|
||||
return this.definitions;
|
||||
}
|
||||
|
||||
private getMap(): Map<string, Command> {
|
||||
const map = new Map<string, Command>();
|
||||
for (const command of commands) {
|
||||
const definition = command.definition()
|
||||
map.set(definition.name, command);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import {
|
||||
SlashCommandBuilder,
|
||||
ChatInputCommandInteraction,
|
||||
MessageFlags,
|
||||
InteractionReplyOptions,
|
||||
GuildMember,
|
||||
EmbedBuilder,
|
||||
AutocompleteInteraction,
|
||||
roleMention, time, userMention, GuildMemberRoleManager
|
||||
MessageFlags,
|
||||
InteractionReplyOptions,
|
||||
GuildMember,
|
||||
EmbedBuilder,
|
||||
AutocompleteInteraction,
|
||||
roleMention, time, userMention, GuildMemberRoleManager
|
||||
} from "discord.js";
|
||||
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
|
||||
import {GroupModel} from "../../Models/GroupModel";
|
||||
|
|
@ -142,10 +142,10 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
|
||||
Container.get<GroupRepository>(GroupRepository.name).create(group);
|
||||
|
||||
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral })
|
||||
interaction.reply({content: `:white_check_mark: Created group \`${name}\``, flags: MessageFlags.Ephemeral})
|
||||
}
|
||||
|
||||
private validateGroupName(name: string): true|string{
|
||||
private validateGroupName(name: string): true | string {
|
||||
const lowercaseName = name.toLowerCase();
|
||||
for (const invalidcharactersequence of GroupCommand.INVALID_CHARACTER_SEQUENCES) {
|
||||
if (!lowercaseName.includes(invalidcharactersequence)) {
|
||||
|
|
@ -162,7 +162,7 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
const groups = repo.findGroupsByMember(<GuildMember>interaction.member);
|
||||
|
||||
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Your groups on this server:")
|
||||
.setFields(
|
||||
|
|
@ -191,7 +191,7 @@ export class GroupCommand implements Command, ChatInteractionCommand, Autocomple
|
|||
embeds: [
|
||||
embed
|
||||
],
|
||||
allowedMentions: { roles: [] },
|
||||
allowedMentions: {roles: []},
|
||||
flags: MessageFlags.Ephemeral
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,24 @@ import {SlashCommandBuilder, CommandInteraction} from "discord.js";
|
|||
import {Command} from "./Command";
|
||||
|
||||
export class HelloWorldCommand implements Command {
|
||||
private static RESPONSES: string[] = [
|
||||
'Hello :)',
|
||||
'zzzZ... ZzzzZ... huh? I am awake. I am awake!',
|
||||
'Roll for initiative!',
|
||||
'I was an adventurer like you...',
|
||||
'Hello :) How are you?',
|
||||
]
|
||||
|
||||
definition(): SlashCommandBuilder
|
||||
{
|
||||
return new SlashCommandBuilder()
|
||||
.setName("hello")
|
||||
.setDescription("Displays a random response. (commonly used to test if the bot is online)")
|
||||
}
|
||||
async execute(interaction: CommandInteraction): Promise<void> {
|
||||
const random = Math.floor(Math.random() * HelloWorldCommand.RESPONSES.length);
|
||||
|
||||
await interaction.reply(HelloWorldCommand.RESPONSES[random]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
private static RESPONSES: string[] = [
|
||||
'Hello :)',
|
||||
'zzzZ... ZzzzZ... huh? I am awake. I am awake!',
|
||||
'Roll for initiative!',
|
||||
'I was an adventurer like you...',
|
||||
'Hello :) How are you?',
|
||||
]
|
||||
|
||||
definition(): SlashCommandBuilder {
|
||||
return new SlashCommandBuilder()
|
||||
.setName("hello")
|
||||
.setDescription("Displays a random response. (commonly used to test if the bot is online)")
|
||||
}
|
||||
|
||||
async execute(interaction: CommandInteraction): Promise<void> {
|
||||
const random = Math.floor(Math.random() * HelloWorldCommand.RESPONSES.length);
|
||||
|
||||
await interaction.reply(HelloWorldCommand.RESPONSES[random]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import {
|
||||
SlashCommandBuilder,
|
||||
CommandInteraction,
|
||||
AutocompleteInteraction,
|
||||
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, time
|
||||
SlashCommandBuilder,
|
||||
CommandInteraction,
|
||||
AutocompleteInteraction,
|
||||
EmbedBuilder, MessageFlags, ChatInputCommandInteraction, time, AttachmentBuilder, ActivityFlagsBitField, Options
|
||||
} from "discord.js";
|
||||
import {AutocompleteCommand, ChatInteractionCommand, Command} from "./Command";
|
||||
import {Container} from "../../Container/Container";
|
||||
|
|
@ -11,195 +11,325 @@ import {UserError} from "../UserError";
|
|||
import {PlaydateModel} from "../../Models/PlaydateModel";
|
||||
import {PlaydateRepository} from "../../Repositories/PlaydateRepository";
|
||||
import {GroupModel} from "../../Models/GroupModel";
|
||||
import * as ics from 'ics';
|
||||
import ical from 'node-ical';
|
||||
|
||||
export class PlaydatesCommand implements Command, AutocompleteCommand, ChatInteractionCommand {
|
||||
static REGEX = [
|
||||
|
||||
]
|
||||
|
||||
definition(): SlashCommandBuilder {
|
||||
// @ts-expect-error Command builder is improperly marked as incomplete.
|
||||
return new SlashCommandBuilder()
|
||||
.setName("playdates")
|
||||
.setDescription("Manage your playdates")
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("create")
|
||||
.setDescription("Creates a new playdate")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
.addStringOption((option) => option
|
||||
.setName("from")
|
||||
.setDescription("Defines the start date & time. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
.addStringOption((option) => option
|
||||
.setName("to")
|
||||
.setDescription("Defines the end date & time. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
)
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("list")
|
||||
.setDescription("Lists all playdates")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
)
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("remove")
|
||||
.setDescription("Removes a playdate")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
.addIntegerOption((option) => option
|
||||
.setName("playdate")
|
||||
.setDescription("Selects a playdate")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "create":
|
||||
await this.create(interaction, group);
|
||||
break;
|
||||
case "remove":
|
||||
await this.delete(interaction, group);
|
||||
break;
|
||||
case "list":
|
||||
await this.list(interaction, group);
|
||||
break;
|
||||
default:
|
||||
throw new UserError("This subcommand is not yet implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
async create(interaction: CommandInteraction, group: GroupModel): Promise<void> {
|
||||
const fromDate = Date.parse(<string>interaction.options.get("from")?.value ?? '');
|
||||
const toDate = Date.parse(<string>interaction.options.get("to")?.value ?? '');
|
||||
|
||||
if (isNaN(fromDate)) {
|
||||
throw new UserError("No date or invalid date format for the from parameter.");
|
||||
}
|
||||
|
||||
if (isNaN(toDate)) {
|
||||
throw new UserError("No date or invalid date format for the to parameter.");
|
||||
}
|
||||
|
||||
if (fromDate > toDate) {
|
||||
throw new UserError("The to-date can't be earlier than the from-date");
|
||||
}
|
||||
|
||||
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
|
||||
const collidingTimes = playdateRepo.findPlaydatesInRange(fromDate, toDate, group);
|
||||
if (collidingTimes.length > 0) {
|
||||
throw new UserError("The playdate collides with another playdate. Please either remove the old one or choose a different time.")
|
||||
}
|
||||
|
||||
const playdate: Partial<PlaydateModel> = {
|
||||
group: group,
|
||||
from_time: new Date(fromDate),
|
||||
to_time: new Date(toDate),
|
||||
}
|
||||
|
||||
playdateRepo.create(playdate);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Created a play-date.")
|
||||
.setDescription(":white_check_mark: Your playdate has been created! You and your group get notified, when its time.")
|
||||
.setFields({
|
||||
name: "Created playdate",
|
||||
value: `${time(new Date(fromDate),'F')} - ${time(new Date(toDate), 'F')}`,
|
||||
})
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
|
||||
const option = interaction.options.getFocused(true);
|
||||
if (option.name == "group") {
|
||||
await GroupSelection.handleAutocomplete(interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.name != 'playdate') {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
||||
await interaction.respond(
|
||||
playdates.map(playdate => {
|
||||
return {
|
||||
name: `${playdate.from_time.toLocaleString()} - ${playdate.to_time.toLocaleString()}`,
|
||||
value: playdate.id
|
||||
}
|
||||
})
|
||||
definition(): SlashCommandBuilder {
|
||||
// @ts-expect-error Command builder is improperly marked as incomplete.
|
||||
return new SlashCommandBuilder()
|
||||
.setName("playdates")
|
||||
.setDescription("Manage your playdates")
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("create")
|
||||
.setDescription("Creates a new playdate")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
.addStringOption((option) => option
|
||||
.setName("from")
|
||||
.setDescription("Defines the start date & time. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
.addStringOption((option) => option
|
||||
.setName("to")
|
||||
.setDescription("Defines the end date & time. Format: YYYY-MM-DD HH:mm")
|
||||
)
|
||||
)
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("list")
|
||||
.setDescription("Lists all playdates")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
)
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("remove")
|
||||
.setDescription("Removes a playdate")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
.addIntegerOption((option) => option
|
||||
.setName("playdate")
|
||||
.setDescription("Selects a playdate")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand(subcommand => subcommand
|
||||
.setName('import')
|
||||
.setDescription("Imports playdates from iCal files")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
.addAttachmentOption(attachment => attachment
|
||||
.setName("file")
|
||||
.setDescription("The iCal File to import")
|
||||
.setRequired(true)
|
||||
)
|
||||
)
|
||||
.addSubcommand((subcommand) => subcommand
|
||||
.setName("export")
|
||||
.setDescription("Exports a playdate as iCal file.")
|
||||
.addIntegerOption(GroupSelection.createOptionSetup())
|
||||
.addIntegerOption((option) => option
|
||||
.setName("playdate")
|
||||
.setDescription("Selects a playdate to export")
|
||||
.setAutocomplete(true)
|
||||
)
|
||||
.addBooleanOption(option => option
|
||||
.setName("future-dates")
|
||||
.setDescription("Exports the next playdates as a ical file")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async execute(interaction: ChatInputCommandInteraction): Promise<void> {
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
switch (interaction.options.getSubcommand()) {
|
||||
case "create":
|
||||
await this.create(interaction, group);
|
||||
break;
|
||||
case "remove":
|
||||
await this.delete(interaction, group);
|
||||
break;
|
||||
case "list":
|
||||
await this.list(interaction, group);
|
||||
break;
|
||||
case "import":
|
||||
await this.import(interaction, group);
|
||||
break;
|
||||
case "export":
|
||||
await this.export(interaction, group);
|
||||
break;
|
||||
default:
|
||||
throw new UserError("This subcommand is not yet implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
async create(interaction: CommandInteraction, group: GroupModel): Promise<void> {
|
||||
const fromDate = Date.parse(<string>interaction.options.get("from")?.value ?? '');
|
||||
const toDate = Date.parse(<string>interaction.options.get("to")?.value ?? '');
|
||||
|
||||
if (isNaN(fromDate)) {
|
||||
throw new UserError("No date or invalid date format for the from parameter.");
|
||||
}
|
||||
|
||||
private async list(interaction: ChatInputCommandInteraction, group: GroupModel) {
|
||||
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
||||
if (isNaN(toDate)) {
|
||||
throw new UserError("No date or invalid date format for the to parameter.");
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("The next playdates:")
|
||||
.setFields(
|
||||
playdates.map((playdate) =>
|
||||
{
|
||||
return {
|
||||
name: `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`,
|
||||
value: `${time(playdate.from_time, 'R')}`
|
||||
}
|
||||
})
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
if (fromDate > toDate) {
|
||||
throw new UserError("The to-date can't be earlier than the from-date");
|
||||
}
|
||||
|
||||
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
|
||||
const collidingTimes = playdateRepo.findPlaydatesInRange(fromDate, toDate, group);
|
||||
if (collidingTimes.length > 0) {
|
||||
throw new UserError("The playdate collides with another playdate. Please either remove the old one or choose a different time.")
|
||||
}
|
||||
|
||||
const playdate: Partial<PlaydateModel> = {
|
||||
group: group,
|
||||
from_time: new Date(fromDate),
|
||||
to_time: new Date(toDate),
|
||||
}
|
||||
|
||||
playdateRepo.create(playdate);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Created a play-date.")
|
||||
.setDescription(":white_check_mark: Your playdate has been created! You and your group get notified, when its time.")
|
||||
.setFields({
|
||||
name: "Created playdate",
|
||||
value: `${time(new Date(fromDate), 'F')} - ${time(new Date(toDate), 'F')}`,
|
||||
})
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
async handleAutocomplete(interaction: AutocompleteInteraction): Promise<void> {
|
||||
const option = interaction.options.getFocused(true);
|
||||
if (option.name == "group") {
|
||||
await GroupSelection.handleAutocomplete(interaction);
|
||||
return;
|
||||
}
|
||||
|
||||
if (option.name != 'playdate') {
|
||||
return;
|
||||
}
|
||||
|
||||
const group = GroupSelection.getGroup(interaction);
|
||||
|
||||
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
||||
await interaction.respond(
|
||||
playdates.map(playdate => {
|
||||
return {
|
||||
name: `${playdate.from_time.toLocaleString()} - ${playdate.to_time.toLocaleString()}`,
|
||||
value: <number>playdate.id
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private async list(interaction: ChatInputCommandInteraction, group: GroupModel) {
|
||||
const playdates = Container.get<PlaydateRepository>(PlaydateRepository.name).findFromGroup(group);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("The next playdates:")
|
||||
.setFields(
|
||||
playdates.map((playdate) => {
|
||||
return {
|
||||
name: `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`,
|
||||
value: `${time(playdate.from_time, 'R')}`
|
||||
}
|
||||
})
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
const playdateId = interaction.options.getInteger("playdate", true)
|
||||
|
||||
const repo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
const selected = repo.getById(playdateId);
|
||||
if (!selected) {
|
||||
throw new UserError("No playdate found");
|
||||
}
|
||||
|
||||
if (selected.group?.id != group.id) {
|
||||
throw new UserError("No playdate found");
|
||||
}
|
||||
|
||||
repo.delete(playdateId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Playdate deleted")
|
||||
.setDescription(
|
||||
`:x: Deleted \`${selected.from_time.toLocaleString()} - ${selected.to_time.toLocaleString()}\``
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
}
|
||||
|
||||
private async import(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
const file = interaction.options.getAttachment('file', true);
|
||||
const mimeType = file.contentType?.split(';')[0];
|
||||
if (mimeType !== "text/calendar") {
|
||||
throw new UserError(`Invalid ical file. Got: ${mimeType}`, "Providing a valid iCal file");
|
||||
}
|
||||
|
||||
private async delete(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
const playdateId = interaction.options.getInteger("playdate", true)
|
||||
|
||||
const repo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
const selected = repo.getById(playdateId);
|
||||
if (!selected) {
|
||||
throw new UserError("No playdate found");
|
||||
}
|
||||
|
||||
if (selected.group?.id != group.id) {
|
||||
throw new UserError("No playdate found");
|
||||
}
|
||||
|
||||
repo.delete(playdateId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Playdate deleted")
|
||||
.setDescription(
|
||||
`:x: Deleted \`${selected.from_time.toLocaleString()} - ${selected.to_time.toLocaleString()}\``
|
||||
)
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
embed
|
||||
],
|
||||
flags: MessageFlags.Ephemeral,
|
||||
})
|
||||
await interaction.deferReply({
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
|
||||
const playdateRepo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
|
||||
const icalFile = await ical.async.fromURL(file.url);
|
||||
const playdates: PlaydateModel[] = [];
|
||||
for (const event of Object.values(icalFile)) {
|
||||
if (event.type !== 'VEVENT') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const playdate: Partial<PlaydateModel> = {
|
||||
group: group,
|
||||
from_time: event.start,
|
||||
to_time: event.end
|
||||
}
|
||||
|
||||
const id = playdateRepo.create(playdate);
|
||||
playdates.push(<PlaydateModel>{
|
||||
id,
|
||||
...playdate
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Imported play-dates.")
|
||||
.setDescription(`:white_check_mark: Your ${playdates.length} playdates has been created! You and your group get notified, when its time.`)
|
||||
.setFields({
|
||||
name: "Created playdates",
|
||||
value: playdates.map((playdate) => `${time(playdate.from_time, 'F')} - ${time(playdate.to_time, 'F')}`).join('\n')
|
||||
})
|
||||
.setFooter({
|
||||
text: `Group: ${group.name}`
|
||||
})
|
||||
|
||||
interaction.followUp({
|
||||
embeds: [embed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
})
|
||||
}
|
||||
|
||||
private async export(interaction: ChatInputCommandInteraction, group: GroupModel): Promise<void> {
|
||||
const playdates = this.getExportTargets(interaction, group);
|
||||
|
||||
const result = ics.createEvents(
|
||||
playdates.map((playdate) => {
|
||||
return {
|
||||
start: ics.convertTimestampToArray(playdate.from_time.getTime(), ''),
|
||||
startInputType: 'utc',
|
||||
startOutputType: 'utc',
|
||||
end: ics.convertTimestampToArray(playdate.to_time.getTime(), ''),
|
||||
endInputType: 'utc',
|
||||
endOutputType: 'utc',
|
||||
title: `PnP with ${group.name}`,
|
||||
status: "CONFIRMED",
|
||||
busyStatus: "FREE",
|
||||
categories: ['PnP']
|
||||
}
|
||||
})
|
||||
);
|
||||
if (!result.value) {
|
||||
throw new Error("Failed creating ics file...")
|
||||
}
|
||||
const attachment = new AttachmentBuilder(Buffer.from(result.value), {
|
||||
name: "ICSExport.ics",
|
||||
description: "Your export :)"
|
||||
});
|
||||
interaction.reply({
|
||||
files: [attachment],
|
||||
flags: MessageFlags.Ephemeral
|
||||
})
|
||||
}
|
||||
|
||||
private getExportTargets(interaction: ChatInputCommandInteraction, group: GroupModel): PlaydateModel[] {
|
||||
|
||||
const repo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
const nextPlaydates = interaction.options.getBoolean("future-dates") ?? false;
|
||||
if (nextPlaydates) {
|
||||
return repo.findPlaydatesInRange(new Date());
|
||||
}
|
||||
|
||||
const playdateId = interaction.options.getInteger('playdate');
|
||||
if (!playdateId) {
|
||||
throw new UserError("Nothing to export", "Please specify what you want to export.");
|
||||
}
|
||||
|
||||
const playdate = repo.getById(playdateId);
|
||||
if (!playdate) {
|
||||
throw new UserError("Specified playdate id is invalid");
|
||||
}
|
||||
|
||||
return [playdate];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +1,77 @@
|
|||
import {
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
Events,
|
||||
ActivityType, REST
|
||||
Client,
|
||||
GatewayIntentBits,
|
||||
Events,
|
||||
ActivityType, REST
|
||||
} from "discord.js";
|
||||
import Commands from "./Commands/Commands";
|
||||
import {Container} from "../Container/Container";
|
||||
import {Logger} from "log4js";
|
||||
import {InteractionRouter} from "./InteractionRouter";
|
||||
import {CommandDeployer} from "./CommandDeployer";
|
||||
|
||||
export class DiscordClient {
|
||||
private readonly client: Client;
|
||||
private readonly client: Client;
|
||||
|
||||
public get Client(): Client {
|
||||
return this.client;
|
||||
}
|
||||
public get Client(): Client {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public get Commands(): Commands {
|
||||
return this.router.commands
|
||||
}
|
||||
public get Commands(): Commands {
|
||||
return this.router.commands
|
||||
}
|
||||
|
||||
public get RESTClient(): REST {
|
||||
return this.restClient;
|
||||
}
|
||||
public get RESTClient(): REST {
|
||||
return this.restClient;
|
||||
}
|
||||
|
||||
public get ApplicationId(): string {
|
||||
return this.applicationId;
|
||||
}
|
||||
public get ApplicationId(): string {
|
||||
return this.applicationId;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly applicationId: string,
|
||||
private readonly router: InteractionRouter,
|
||||
private readonly restClient: REST = new REST()
|
||||
) {
|
||||
this.client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds]
|
||||
})
|
||||
}
|
||||
constructor(
|
||||
private readonly applicationId: string,
|
||||
private readonly router: InteractionRouter,
|
||||
private readonly deployer: CommandDeployer,
|
||||
private readonly logger: Logger,
|
||||
private readonly restClient: REST = new REST()
|
||||
) {
|
||||
this.client = new Client({
|
||||
intents: [GatewayIntentBits.Guilds]
|
||||
})
|
||||
}
|
||||
|
||||
applyEvents() {
|
||||
this.client.once(Events.ClientReady, () => {
|
||||
if (!this.client.user) {
|
||||
return;
|
||||
}
|
||||
applyEvents() {
|
||||
this.client.once(Events.ClientReady, () => {
|
||||
if (!this.client.user) {
|
||||
return;
|
||||
}
|
||||
|
||||
Container.get<Logger>("logger").info(`Ready! Logged in as ${this.client.user.tag}`);
|
||||
this.client.user.setActivity('your PnP playdates', {
|
||||
type: ActivityType.Watching,
|
||||
});
|
||||
})
|
||||
this.logger.info(`Ready! Logged in as ${this.client.user.tag}`);
|
||||
this.client.user.setActivity('your PnP playdates', {
|
||||
type: ActivityType.Watching,
|
||||
});
|
||||
})
|
||||
|
||||
this.client.on(Events.GuildAvailable, () => {
|
||||
Container.get<Logger>("logger").info("Joined Guild?")
|
||||
})
|
||||
this.client.on(Events.GuildCreate, (guild) => {
|
||||
this.logger.info(`Joined ${guild.name}`);
|
||||
this.deployer.deployServer(guild.id);
|
||||
})
|
||||
this.client.on(Events.GuildDelete, (guild) => {
|
||||
this.logger.info(`Left ${guild.name}`);
|
||||
})
|
||||
this.client.on(Events.GuildAvailable, (guild) => {
|
||||
this.deployer.deployServer(guild.id);
|
||||
})
|
||||
|
||||
this.client.on(Events.InteractionCreate, this.router.route.bind(this.router));
|
||||
}
|
||||
this.client.on(Events.InteractionCreate, this.router.route.bind(this.router));
|
||||
}
|
||||
|
||||
connect(token: string) {
|
||||
this.client.login(token);
|
||||
}
|
||||
connect(token: string) {
|
||||
this.client.login(token);
|
||||
}
|
||||
|
||||
connectRESTClient(token: string) {
|
||||
this.restClient.setToken(token);
|
||||
}
|
||||
connectRESTClient(token: string) {
|
||||
this.restClient.setToken(token);
|
||||
}
|
||||
}
|
||||
|
|
@ -50,20 +50,20 @@ export class InteractionRouter {
|
|||
|
||||
return InteractionRoutingType.Unrouted;
|
||||
}
|
||||
|
||||
|
||||
private async handleCommand(interaction: ChatInputCommandInteraction) {
|
||||
try {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
if (!command) {
|
||||
throw new UserError(`Requested command not found.`);
|
||||
}
|
||||
|
||||
|
||||
if (!('execute' in command)) {
|
||||
throw new UserError(`Requested command is not setup for a chat command.`);
|
||||
}
|
||||
|
||||
|
||||
this.logger.debug(`Found chat command ${interaction.commandName}: running...`);
|
||||
|
||||
|
||||
await command.execute?.call(command, interaction);
|
||||
} catch (e: any) {
|
||||
this.logger.error(e)
|
||||
|
|
@ -79,13 +79,13 @@ ${inlineCode(e.tryInstead)}`
|
|||
}
|
||||
}
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.followUp({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
await interaction.followUp({content: userMessage, flags: MessageFlags.Ephemeral});
|
||||
} else {
|
||||
await interaction.reply({ content: userMessage, flags: MessageFlags.Ephemeral });
|
||||
await interaction.reply({content: userMessage, flags: MessageFlags.Ephemeral});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async handleAutocomplete(interaction: AutocompleteInteraction) {
|
||||
const command = this.commands.getCommand(interaction.commandName);
|
||||
|
||||
|
|
@ -104,6 +104,6 @@ ${inlineCode(e.tryInstead)}`
|
|||
} catch (e: unknown) {
|
||||
Container.get<Logger>('logger').error(e);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
export class UserError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly tryInstead: string|null = null
|
||||
) {
|
||||
super(message);
|
||||
|
||||
}
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly tryInstead: string | null = null
|
||||
) {
|
||||
super(message);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -2,33 +2,33 @@ import dotenv from "dotenv";
|
|||
import path from "node:path";
|
||||
|
||||
type DiscordEnvironment = {
|
||||
token: string;
|
||||
guildId: string;
|
||||
clientId: string;
|
||||
token: string;
|
||||
guildId: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export type DatabaseEnvironment = {
|
||||
path: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export class Environment {
|
||||
get discord(): DiscordEnvironment {
|
||||
return {
|
||||
token: process.env.DISCORD_API_KEY ?? '',
|
||||
guildId: process.env.DISCORD_GUILD_ID ?? '',
|
||||
clientId: process.env.DISCORD_CLIENT_ID ?? '',
|
||||
}
|
||||
get discord(): DiscordEnvironment {
|
||||
return {
|
||||
token: process.env.DISCORD_API_KEY ?? '',
|
||||
guildId: process.env.DISCORD_GUILD_ID ?? '',
|
||||
clientId: process.env.DISCORD_CLIENT_ID ?? '',
|
||||
}
|
||||
|
||||
get database(): DatabaseEnvironment {
|
||||
return {
|
||||
path: path.resolve(process.env.DB_PATH ?? ''),
|
||||
}
|
||||
}
|
||||
|
||||
public setup() {
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, "../environment/.env"),
|
||||
});
|
||||
}
|
||||
|
||||
get database(): DatabaseEnvironment {
|
||||
return {
|
||||
path: path.resolve(process.env.DB_PATH ?? ''),
|
||||
}
|
||||
}
|
||||
|
||||
public setup() {
|
||||
dotenv.config({
|
||||
path: path.resolve(__dirname, "../environment/.env"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -10,17 +10,17 @@ export class DefaultEvents {
|
|||
const events: TimedEvent[] = [
|
||||
new ReminderEvent()
|
||||
]
|
||||
|
||||
|
||||
const eventHandler = Container.get<EventHandler>(EventHandler.name);
|
||||
|
||||
|
||||
events.forEach((event) => {
|
||||
eventHandler.addTimed(event);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
public static setupHandlers() {
|
||||
const eventHandler = Container.get<EventHandler>(EventHandler.name);
|
||||
|
||||
|
||||
eventHandler.addHandler<ElementCreatedEvent<PlaydateModel>>(ElementCreatedEvent.name, sendCreatedNotificationEventHandler);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,9 @@ import {Model} from "../Models/Model";
|
|||
|
||||
export class ElementCreatedEvent<T extends Model = Model> {
|
||||
constructor(
|
||||
public readonly tableName: string,
|
||||
public readonly tableName: string,
|
||||
public readonly instanceValues: Partial<T>,
|
||||
public readonly instanceId: number|bigint
|
||||
public readonly instanceId: number | bigint
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import cron from "node-cron";
|
||||
import {Class} from "../types/Class";
|
||||
import {Class} from "../types/Class";
|
||||
|
||||
export type EventConfiguration = {
|
||||
name: string,
|
||||
name: string,
|
||||
maxExecutions?: number,
|
||||
}
|
||||
|
||||
|
|
@ -14,34 +14,34 @@ export interface TimedEvent {
|
|||
|
||||
export class EventHandler {
|
||||
private eventHandlers: Map<string, CallableFunction[]> = new Map();
|
||||
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
|
||||
public addHandler<T extends Class>(eventName: string, handler: (event: T) => void) {
|
||||
if (!this.eventHandlers.has(eventName)) {
|
||||
this.eventHandlers.set(eventName, []);
|
||||
}
|
||||
|
||||
|
||||
this.eventHandlers.get(eventName)?.push(handler);
|
||||
}
|
||||
|
||||
|
||||
public dispatch<T extends Class>(event: T) {
|
||||
const eventName = event.constructor.name;
|
||||
if (!this.eventHandlers.has(eventName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.eventHandlers.get(eventName)?.forEach((handler) => {
|
||||
handler(event);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
public addTimed(event: TimedEvent) {
|
||||
if (!cron.validate(event.cronExpression)) {
|
||||
throw new Error(`Can't create event with name '${event.configuration.name}': Invalid cron expression.`)
|
||||
}
|
||||
|
||||
|
||||
cron.schedule(event.cronExpression, event.execute.bind(event), event.configuration);
|
||||
}
|
||||
}
|
||||
|
|
@ -23,18 +23,18 @@ export async function sendCreatedNotificationEventHandler(event: ElementCreatedE
|
|||
if (!playdate.group || !playdate.from_time || !playdate.to_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const configurationHandler = new GroupConfigurationHandler(
|
||||
Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name),
|
||||
playdate.group
|
||||
);
|
||||
|
||||
|
||||
const targetChannel = configurationHandler.getConfigurationByPath('channels.newPlaydates');
|
||||
if (!targetChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const channel = await Container.get<DiscordClient>(DiscordClient.name).Client.channels.fetch(<string>targetChannel)
|
||||
if (!channel) {
|
||||
return;
|
||||
|
|
@ -67,7 +67,7 @@ export async function sendCreatedNotificationEventHandler(event: ElementCreatedE
|
|||
embed
|
||||
],
|
||||
allowedMentions: {
|
||||
roles: [ playdate.group.role.roleid ]
|
||||
roles: [playdate.group.role.roleid]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -14,24 +14,24 @@ export class ReminderEvent implements TimedEvent {
|
|||
1,
|
||||
7
|
||||
];
|
||||
|
||||
|
||||
private static REMINDER_NOTIFICATIONS = [
|
||||
'The darkness approaches. Get ready!',
|
||||
'Your aid is requested once again.',
|
||||
'Grab your dice and show them evil-doers how its done!',
|
||||
]
|
||||
|
||||
|
||||
|
||||
configuration: EventConfiguration = {
|
||||
name: "Reminders",
|
||||
}
|
||||
|
||||
|
||||
cronExpression: string = "0 9 * * *"
|
||||
|
||||
private readonly groupConfigurationRepository: GroupConfigurationRepository
|
||||
private playdateRepository: PlaydateRepository
|
||||
private discordClient: DiscordClient
|
||||
|
||||
|
||||
constructor() {
|
||||
this.playdateRepository = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
this.groupConfigurationRepository = Container.get<GroupConfigurationRepository>(GroupConfigurationRepository.name);
|
||||
|
|
@ -40,61 +40,61 @@ export class ReminderEvent implements TimedEvent {
|
|||
|
||||
async execute() {
|
||||
const today = new Date();
|
||||
today.setHours(0,0,0,0);
|
||||
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const playdates = ReminderEvent.REMINDER_INTERVALS.flatMap((interval) => {
|
||||
const fromDate = new Date(today.valueOf())
|
||||
fromDate.setDate(fromDate.getDate() + interval);
|
||||
|
||||
|
||||
const toDate = new Date(today.valueOf())
|
||||
toDate.setDate(toDate.getDate() + interval);
|
||||
toDate.setHours(23,59,59,999);
|
||||
|
||||
toDate.setHours(23, 59, 59, 999);
|
||||
|
||||
return this.playdateRepository.findPlaydatesInRange(fromDate, toDate);
|
||||
}, this)
|
||||
|
||||
|
||||
const promises = playdates
|
||||
.map((playdate) => {
|
||||
if (!playdate.group) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
const configurationHandler = new GroupConfigurationHandler(
|
||||
this.groupConfigurationRepository,
|
||||
playdate.group
|
||||
);
|
||||
|
||||
|
||||
const config = configurationHandler.getConfiguration();
|
||||
const targetChannel = config.channels?.playdateReminders;
|
||||
|
||||
|
||||
if (!targetChannel) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
||||
return this.sendReminder(playdate, targetChannel);
|
||||
}, this)
|
||||
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
private async sendReminder(playdate: PlaydateModel, targetChannel: ChannelId) {
|
||||
if (!playdate.group) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const channel = await this.discordClient.Client.channels.fetch(targetChannel)
|
||||
if (!channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!channel.isTextBased()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!channel.isSendable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("Playdate reminder")
|
||||
.setDescription(
|
||||
|
|
@ -114,7 +114,7 @@ export class ReminderEvent implements TimedEvent {
|
|||
embed
|
||||
],
|
||||
allowedMentions: {
|
||||
roles: [ playdate.group.role.roleid ]
|
||||
roles: [playdate.group.role.roleid]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,66 +10,67 @@ import {Nullable} from "../types/Nullable";
|
|||
import {isPlainObject} from "is-plain-object";
|
||||
|
||||
export class GroupConfigurationHandler {
|
||||
static DEFAULT_CONFIGURATION: RuntimeGroupConfiguration = {
|
||||
channels: null,
|
||||
locale: new Intl.Locale('en-GB'),
|
||||
permissions: {
|
||||
allowMemberManagingPlaydates: false
|
||||
}
|
||||
static DEFAULT_CONFIGURATION: RuntimeGroupConfiguration = {
|
||||
channels: null,
|
||||
locale: new Intl.Locale('en-GB'),
|
||||
permissions: {
|
||||
allowMemberManagingPlaydates: false
|
||||
}
|
||||
|
||||
private readonly transformers: GroupConfigurationTransformers = new GroupConfigurationTransformers();
|
||||
|
||||
constructor(
|
||||
private readonly repository: GroupConfigurationRepository,
|
||||
private readonly group: GroupModel
|
||||
) { }
|
||||
}
|
||||
|
||||
private readonly transformers: GroupConfigurationTransformers = new GroupConfigurationTransformers();
|
||||
|
||||
constructor(
|
||||
private readonly repository: GroupConfigurationRepository,
|
||||
private readonly group: GroupModel
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
public saveConfiguration(path: string, value: string): void {
|
||||
const configuration = this.repository.findConfigurationByPath(this.group, path);
|
||||
|
||||
if (configuration) {
|
||||
this.repository.update(
|
||||
{
|
||||
...configuration,
|
||||
value: value
|
||||
}
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
this.repository.create({
|
||||
group: this.group,
|
||||
key: path,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration(): RuntimeGroupConfiguration {
|
||||
return deepmerge(GroupConfigurationHandler.DEFAULT_CONFIGURATION, this.getDatabaseConfiguration(), {
|
||||
isMergeableObject: isPlainObject
|
||||
});
|
||||
}
|
||||
|
||||
public getConfigurationByPath(path: string): Nullable<GroupConfigurationResult> {
|
||||
const configuration = this.repository.findConfigurationByPath(this.group, path);
|
||||
if (!configuration) {
|
||||
return null;
|
||||
}
|
||||
public saveConfiguration(path: string, value: string): void {
|
||||
const configuration = this.repository.findConfigurationByPath(this.group, path);
|
||||
|
||||
return this.transformers.getValue(configuration);
|
||||
if (configuration) {
|
||||
this.repository.update(
|
||||
{
|
||||
...configuration,
|
||||
value: value
|
||||
}
|
||||
)
|
||||
return;
|
||||
}
|
||||
|
||||
private getDatabaseConfiguration(): Partial<RuntimeGroupConfiguration> {
|
||||
const values = this.repository.findGroupConfigurations(this.group);
|
||||
const configuration: Partial<RuntimeGroupConfiguration> = {};
|
||||
|
||||
values.forEach((configValue) => {
|
||||
const value = this.transformers.getValue(configValue);
|
||||
setPath(configuration, configValue.key, value);
|
||||
})
|
||||
|
||||
return configuration;
|
||||
|
||||
this.repository.create({
|
||||
group: this.group,
|
||||
key: path,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration(): RuntimeGroupConfiguration {
|
||||
return deepmerge(GroupConfigurationHandler.DEFAULT_CONFIGURATION, this.getDatabaseConfiguration(), {
|
||||
isMergeableObject: isPlainObject
|
||||
});
|
||||
}
|
||||
|
||||
public getConfigurationByPath(path: string): Nullable<GroupConfigurationResult> {
|
||||
const configuration = this.repository.findConfigurationByPath(this.group, path);
|
||||
if (!configuration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.transformers.getValue(configuration);
|
||||
}
|
||||
|
||||
private getDatabaseConfiguration(): Partial<RuntimeGroupConfiguration> {
|
||||
const values = this.repository.findGroupConfigurations(this.group);
|
||||
const configuration: Partial<RuntimeGroupConfiguration> = {};
|
||||
|
||||
values.forEach((configValue) => {
|
||||
const value = this.transformers.getValue(configValue);
|
||||
setPath(configuration, configValue.key, value);
|
||||
})
|
||||
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,371 +1,372 @@
|
|||
import {GroupConfigurationTransformers, TransformerType} from "./GroupConfigurationTransformers";
|
||||
import {GroupConfigurationHandler} from "./GroupConfigurationHandler";
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
AnySelectMenuInteraction,
|
||||
ButtonBuilder,
|
||||
ButtonStyle, channelMention,
|
||||
ChannelSelectMenuBuilder, ChannelType,
|
||||
ChatInputCommandInteraction, EmbedBuilder, inlineCode, Interaction,
|
||||
InteractionReplyOptions,
|
||||
InteractionUpdateOptions, italic, MessageFlags,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder, UserSelectMenuBuilder
|
||||
ActionRowBuilder,
|
||||
AnySelectMenuInteraction,
|
||||
ButtonBuilder,
|
||||
ButtonStyle, channelMention,
|
||||
ChannelSelectMenuBuilder, ChannelType,
|
||||
ChatInputCommandInteraction, EmbedBuilder, inlineCode, Interaction,
|
||||
InteractionReplyOptions,
|
||||
InteractionUpdateOptions, italic, MessageFlags,
|
||||
StringSelectMenuBuilder,
|
||||
StringSelectMenuOptionBuilder, UserSelectMenuBuilder
|
||||
} from "discord.js";
|
||||
import {Logger} from "log4js";
|
||||
import {Container} from "../Container/Container";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {
|
||||
MentionableSelectMenuBuilder,
|
||||
MessageActionRowComponentBuilder,
|
||||
RoleSelectMenuBuilder
|
||||
MentionableSelectMenuBuilder,
|
||||
MessageActionRowComponentBuilder,
|
||||
RoleSelectMenuBuilder
|
||||
} from "@discordjs/builders";
|
||||
import {ChannelId} from "../types/DiscordTypes";
|
||||
import {IconCache} from "../Icons/IconCache";
|
||||
|
||||
type UIElementCollection = Record<string, UIElement>;
|
||||
type UIElement = {
|
||||
label: string,
|
||||
key: string,
|
||||
description: string,
|
||||
childrenElements?: UIElementCollection,
|
||||
isConfiguration?: true
|
||||
label: string,
|
||||
key: string,
|
||||
description: string,
|
||||
childrenElements?: UIElementCollection,
|
||||
isConfiguration?: true
|
||||
}
|
||||
|
||||
export class GroupConfigurationRenderer {
|
||||
private static MOVETO_COMMAND = 'moveto_';
|
||||
private static SETVALUE_COMMAND = 'setvalue_';
|
||||
private static MOVEBACK_COMMAND = 'back';
|
||||
|
||||
private breadcrumbs: string[] = [];
|
||||
|
||||
private static UI_ELEMENTS : UIElementCollection = {
|
||||
channels: {
|
||||
label: 'Channels',
|
||||
key: 'channels',
|
||||
description: "Provides settings to define in what channels the bot sends messages, when not directly interacting with it.",
|
||||
childrenElements: {
|
||||
newPlaydates: {
|
||||
label: 'New Playdates',
|
||||
key: 'newPlaydates',
|
||||
description: "Sets the channel, where the group get notified when new Playdates are set.",
|
||||
isConfiguration: true
|
||||
},
|
||||
playdateReminders: {
|
||||
label: 'Playdate Reminders',
|
||||
key: 'playdateReminders',
|
||||
description: "Sets the channel, where the group gets reminded of upcoming playdates.",
|
||||
isConfiguration: true
|
||||
}
|
||||
}
|
||||
private static MOVETO_COMMAND = 'moveto_';
|
||||
private static SETVALUE_COMMAND = 'setvalue_';
|
||||
private static MOVEBACK_COMMAND = 'back';
|
||||
|
||||
private breadcrumbs: string[] = [];
|
||||
|
||||
private static UI_ELEMENTS: UIElementCollection = {
|
||||
channels: {
|
||||
label: 'Channels',
|
||||
key: 'channels',
|
||||
description: "Provides settings to define in what channels the bot sends messages, when not directly interacting with it.",
|
||||
childrenElements: {
|
||||
newPlaydates: {
|
||||
label: 'New Playdates',
|
||||
key: 'newPlaydates',
|
||||
description: "Sets the channel, where the group get notified when new Playdates are set.",
|
||||
isConfiguration: true
|
||||
},
|
||||
locale: {
|
||||
label: "Locale",
|
||||
key: 'locale',
|
||||
description: "Provides locale to be used for this group. This mostly sets how the dates are displayed, but this can also be later used for translations.",
|
||||
isConfiguration: true
|
||||
},
|
||||
permissions: {
|
||||
label: "Permissions",
|
||||
key: "permissions",
|
||||
description: "Allows customization, how the members are allowed to interact with the data stored in the group.",
|
||||
childrenElements: {
|
||||
allowMemberManagingPlaydates: {
|
||||
label: "Manage Playdates",
|
||||
key: "allowMemberManagingPlaydates",
|
||||
description: "Defines if the members are allowed to manage playdates like adding or deleting them.",
|
||||
isConfiguration: true
|
||||
}
|
||||
}
|
||||
playdateReminders: {
|
||||
label: 'Playdate Reminders',
|
||||
key: 'playdateReminders',
|
||||
description: "Sets the channel, where the group gets reminded of upcoming playdates.",
|
||||
isConfiguration: true
|
||||
}
|
||||
}
|
||||
},
|
||||
locale: {
|
||||
label: "Locale",
|
||||
key: 'locale',
|
||||
description: "Provides locale to be used for this group. This mostly sets how the dates are displayed, but this can also be later used for translations.",
|
||||
isConfiguration: true
|
||||
},
|
||||
permissions: {
|
||||
label: "Permissions",
|
||||
key: "permissions",
|
||||
description: "Allows customization, how the members are allowed to interact with the data stored in the group.",
|
||||
childrenElements: {
|
||||
allowMemberManagingPlaydates: {
|
||||
label: "Manage Playdates",
|
||||
key: "allowMemberManagingPlaydates",
|
||||
description: "Defines if the members are allowed to manage playdates like adding or deleting them.",
|
||||
isConfiguration: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly configurationHandler: GroupConfigurationHandler,
|
||||
private readonly transformers: GroupConfigurationTransformers,
|
||||
) {}
|
||||
|
||||
public async setup(interaction: ChatInputCommandInteraction) {
|
||||
let response = await interaction.reply(this.getReplyOptions());
|
||||
let exit = false;
|
||||
let eventResponse;
|
||||
const filter = (i: Interaction) => i.user.id === interaction.user.id;
|
||||
do {
|
||||
|
||||
if (eventResponse) {
|
||||
response = await eventResponse.update(this.getReplyOptions());
|
||||
}
|
||||
|
||||
try {
|
||||
eventResponse = await response.resource?.message?.awaitMessageComponent({
|
||||
dispose: true,
|
||||
filter: filter,
|
||||
time: 60_000
|
||||
});
|
||||
} catch (_: unknown) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!eventResponse || eventResponse.customId === 'exit') {
|
||||
exit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId === GroupConfigurationRenderer.MOVEBACK_COMMAND) {
|
||||
this.breadcrumbs.pop()
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId.startsWith(GroupConfigurationRenderer.MOVETO_COMMAND)) {
|
||||
this.breadcrumbs.push(
|
||||
eventResponse.customId.substring(GroupConfigurationRenderer.MOVETO_COMMAND.length)
|
||||
)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId.startsWith(GroupConfigurationRenderer.SETVALUE_COMMAND)) {
|
||||
this.handleSelection(<AnySelectMenuInteraction>eventResponse);
|
||||
continue;
|
||||
}
|
||||
|
||||
} while(!exit);
|
||||
|
||||
if (eventResponse) {
|
||||
try {
|
||||
await eventResponse.update(
|
||||
this.getReplyOptions()
|
||||
);
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
await eventResponse.deleteReply();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const message = response.resource?.message
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.deletable) {
|
||||
await message.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private getReplyOptions(): InteractionUpdateOptions & InteractionReplyOptions & { withResponse: true } {
|
||||
const embed = this.createEmbed();
|
||||
const icons = Container.get<IconCache>(IconCache.name);
|
||||
embed.setAuthor({
|
||||
name: "/ " + this.breadcrumbs.join(" / ")
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly configurationHandler: GroupConfigurationHandler,
|
||||
private readonly transformers: GroupConfigurationTransformers,
|
||||
) {
|
||||
}
|
||||
|
||||
public async setup(interaction: ChatInputCommandInteraction) {
|
||||
let response = await interaction.reply(this.getReplyOptions());
|
||||
let exit = false;
|
||||
let eventResponse;
|
||||
const filter = (i: Interaction) => i.user.id === interaction.user.id;
|
||||
do {
|
||||
|
||||
if (eventResponse) {
|
||||
response = await eventResponse.update(this.getReplyOptions());
|
||||
}
|
||||
|
||||
try {
|
||||
eventResponse = await response.resource?.message?.awaitMessageComponent({
|
||||
dispose: true,
|
||||
filter: filter,
|
||||
time: 60_000
|
||||
});
|
||||
|
||||
const exitButton = new ButtonBuilder()
|
||||
.setLabel("Exit")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setCustomId("exit")
|
||||
.setEmoji(icons.get("door_open_solid_white") ?? '');
|
||||
|
||||
const actionrow = new ActionRowBuilder<ButtonBuilder>()
|
||||
|
||||
if (this.breadcrumbs.length > 0) {
|
||||
const backButton = new ButtonBuilder()
|
||||
.setLabel("Back")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVEBACK_COMMAND)
|
||||
.setEmoji(icons.get("angle_left_solid") ?? '');
|
||||
|
||||
actionrow.addComponents(backButton)
|
||||
}
|
||||
actionrow.addComponents(exitButton)
|
||||
|
||||
return {
|
||||
content: "",
|
||||
embeds: [embed],
|
||||
components: [...this.createActionRowBuildersForMenu(), actionrow],
|
||||
withResponse: true,
|
||||
flags: MessageFlags.Ephemeral
|
||||
};
|
||||
} catch (_: unknown) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!eventResponse || eventResponse.customId === 'exit') {
|
||||
exit = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId === GroupConfigurationRenderer.MOVEBACK_COMMAND) {
|
||||
this.breadcrumbs.pop()
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId.startsWith(GroupConfigurationRenderer.MOVETO_COMMAND)) {
|
||||
this.breadcrumbs.push(
|
||||
eventResponse.customId.substring(GroupConfigurationRenderer.MOVETO_COMMAND.length)
|
||||
)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (eventResponse.customId.startsWith(GroupConfigurationRenderer.SETVALUE_COMMAND)) {
|
||||
this.handleSelection(<AnySelectMenuInteraction>eventResponse);
|
||||
continue;
|
||||
}
|
||||
|
||||
} while (!exit);
|
||||
|
||||
if (eventResponse) {
|
||||
try {
|
||||
await eventResponse.update(
|
||||
this.getReplyOptions()
|
||||
);
|
||||
} catch (_) {
|
||||
|
||||
}
|
||||
await eventResponse.deleteReply();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private createEmbed(): EmbedBuilder {
|
||||
const {currentElement} = this.findCurrentUI();
|
||||
|
||||
if (currentElement === null) {
|
||||
return new EmbedBuilder()
|
||||
.setTitle("Group Configuration")
|
||||
.setDescription("This UI allows you to change settings for your group.")
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(currentElement?.label ?? '')
|
||||
.setDescription(currentElement?.description ?? '');
|
||||
|
||||
if (currentElement?.isConfiguration ?? false) {
|
||||
embed.addFields(
|
||||
{ name: "Current Value", value: this.getCurrentValueAsUI(), inline: false }
|
||||
)
|
||||
}
|
||||
|
||||
return embed;
|
||||
|
||||
const message = response.resource?.message
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.deletable) {
|
||||
await message.delete()
|
||||
}
|
||||
}
|
||||
|
||||
private getReplyOptions(): InteractionUpdateOptions & InteractionReplyOptions & { withResponse: true } {
|
||||
const embed = this.createEmbed();
|
||||
const icons = Container.get<IconCache>(IconCache.name);
|
||||
embed.setAuthor({
|
||||
name: "/ " + this.breadcrumbs.join(" / ")
|
||||
});
|
||||
|
||||
const exitButton = new ButtonBuilder()
|
||||
.setLabel("Exit")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setCustomId("exit")
|
||||
.setEmoji(icons.get("door_open_solid_white") ?? '');
|
||||
|
||||
const actionrow = new ActionRowBuilder<ButtonBuilder>()
|
||||
|
||||
if (this.breadcrumbs.length > 0) {
|
||||
const backButton = new ButtonBuilder()
|
||||
.setLabel("Back")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVEBACK_COMMAND)
|
||||
.setEmoji(icons.get("angle_left_solid") ?? '');
|
||||
|
||||
actionrow.addComponents(backButton)
|
||||
}
|
||||
actionrow.addComponents(exitButton)
|
||||
|
||||
return {
|
||||
content: "",
|
||||
embeds: [embed],
|
||||
components: [...this.createActionRowBuildersForMenu(), actionrow],
|
||||
withResponse: true,
|
||||
flags: MessageFlags.Ephemeral
|
||||
};
|
||||
}
|
||||
|
||||
private createEmbed(): EmbedBuilder {
|
||||
const {currentElement} = this.findCurrentUI();
|
||||
|
||||
if (currentElement === null) {
|
||||
return new EmbedBuilder()
|
||||
.setTitle("Group Configuration")
|
||||
.setDescription("This UI allows you to change settings for your group.")
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(currentElement?.label ?? '')
|
||||
.setDescription(currentElement?.description ?? '');
|
||||
|
||||
if (currentElement?.isConfiguration ?? false) {
|
||||
embed.addFields(
|
||||
{name: "Current Value", value: this.getCurrentValueAsUI(), inline: false}
|
||||
)
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
private getCurrentValueAsUI(): string {
|
||||
const path = this.breadcrumbs.join(".");
|
||||
const value = this.configurationHandler.getConfigurationByPath(path);
|
||||
|
||||
if (value === undefined) return italic("None");
|
||||
|
||||
const type = this.transformers.getTransformerType(path);
|
||||
|
||||
if (type === undefined) {
|
||||
throw new Error("Could not find the type for " + path);
|
||||
}
|
||||
|
||||
private getCurrentValueAsUI(): string {
|
||||
const path = this.breadcrumbs.join(".");
|
||||
const value = this.configurationHandler.getConfigurationByPath(path);
|
||||
|
||||
if (value === undefined) return italic("None");
|
||||
|
||||
const type = this.transformers.getTransformerType(path);
|
||||
|
||||
if (type === undefined) {
|
||||
throw new Error("Could not find the type for " + path);
|
||||
}
|
||||
|
||||
|
||||
const displaynames = new Intl.DisplayNames(["en"], { type: "language" });
|
||||
|
||||
switch (type) {
|
||||
case TransformerType.Locale:
|
||||
if (!value) {
|
||||
return inlineCode("Default");
|
||||
}
|
||||
return displaynames.of((<Intl.Locale>value)?.baseName) ?? "Unknown";
|
||||
case TransformerType.Channel:
|
||||
if (!value) {
|
||||
return inlineCode("None");
|
||||
}
|
||||
return channelMention(<ChannelId>value);
|
||||
case TransformerType.PermissionBoolean:
|
||||
return value ? "Allowed" : "Disallowed"
|
||||
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
}
|
||||
|
||||
private createActionRowBuildersForMenu() : ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
||||
const {currentCollection, currentElement} = this.findCurrentUI();
|
||||
const icons = Container.get<IconCache>(IconCache.name);
|
||||
const displaynames = new Intl.DisplayNames(["en"], {type: "language"});
|
||||
|
||||
if (currentElement?.isConfiguration ?? false) {
|
||||
return [
|
||||
new ActionRowBuilder<ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder>()
|
||||
.addComponents(this.getSelectForBreadcrumbs())
|
||||
]
|
||||
switch (type) {
|
||||
case TransformerType.Locale:
|
||||
if (!value) {
|
||||
return inlineCode("Default");
|
||||
}
|
||||
|
||||
return [
|
||||
new ActionRowBuilder<ButtonBuilder>()
|
||||
.setComponents(
|
||||
...Object.values(currentCollection).map(elem => new ButtonBuilder()
|
||||
.setLabel(` ${elem.label}`)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVETO_COMMAND + elem.key)
|
||||
.setEmoji(icons.get(elem.isConfiguration ? 'pen_solid' : "folder_solid") ?? '')
|
||||
)
|
||||
)
|
||||
return displaynames.of((<Intl.Locale>value)?.baseName) ?? "Unknown";
|
||||
case TransformerType.Channel:
|
||||
if (!value) {
|
||||
return inlineCode("None");
|
||||
}
|
||||
return channelMention(<ChannelId>value);
|
||||
case TransformerType.PermissionBoolean:
|
||||
return value ? "Allowed" : "Disallowed"
|
||||
|
||||
default:
|
||||
return "None";
|
||||
}
|
||||
}
|
||||
|
||||
private createActionRowBuildersForMenu(): ActionRowBuilder<MessageActionRowComponentBuilder>[] {
|
||||
const {currentCollection, currentElement} = this.findCurrentUI();
|
||||
const icons = Container.get<IconCache>(IconCache.name);
|
||||
|
||||
if (currentElement?.isConfiguration ?? false) {
|
||||
return [
|
||||
new ActionRowBuilder<ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder>()
|
||||
.addComponents(this.getSelectForBreadcrumbs())
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
new ActionRowBuilder<ButtonBuilder>()
|
||||
.setComponents(
|
||||
...Object.values(currentCollection).map(elem => new ButtonBuilder()
|
||||
.setLabel(` ${elem.label}`)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setCustomId(GroupConfigurationRenderer.MOVETO_COMMAND + elem.key)
|
||||
.setEmoji(icons.get(elem.isConfiguration ? 'pen_solid' : "folder_solid") ?? '')
|
||||
)
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
private getSelectForBreadcrumbs(): ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder {
|
||||
const breadcrumbPath = this.breadcrumbs.join('.')
|
||||
const transformerType = this.transformers.getTransformerType(breadcrumbPath);
|
||||
if (transformerType === undefined) {
|
||||
throw new Error(`Can not find transformer type for ${breadcrumbPath}`)
|
||||
}
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
const options = [
|
||||
'en-US',
|
||||
'fr-FR',
|
||||
'it-IT',
|
||||
'de-DE'
|
||||
]
|
||||
}
|
||||
|
||||
private getSelectForBreadcrumbs(): ChannelSelectMenuBuilder | MentionableSelectMenuBuilder | RoleSelectMenuBuilder | StringSelectMenuBuilder | UserSelectMenuBuilder {
|
||||
const breadcrumbPath = this.breadcrumbs.join('.')
|
||||
const transformerType = this.transformers.getTransformerType(breadcrumbPath);
|
||||
if (transformerType === undefined) {
|
||||
throw new Error(`Can not find transformer type for ${breadcrumbPath}`)
|
||||
}
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
const options = [
|
||||
'en-US',
|
||||
'fr-FR',
|
||||
'it-IT',
|
||||
'de-DE'
|
||||
]
|
||||
const displaynames = new Intl.DisplayNames(["en"], { type: "language" });
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setOptions(
|
||||
options.map(intl => new StringSelectMenuOptionBuilder()
|
||||
.setLabel(displaynames.of(intl) ?? '')
|
||||
.setValue(intl)
|
||||
)
|
||||
)
|
||||
case TransformerType.Channel:
|
||||
return new ChannelSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setChannelTypes(ChannelType.GuildText)
|
||||
.setPlaceholder("New Value");
|
||||
case TransformerType.PermissionBoolean:
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setOptions(
|
||||
[
|
||||
{
|
||||
label: "Allow",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
label: "Disallow",
|
||||
value: "0"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
default:
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId("...")
|
||||
.setOptions(
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Nothing to see here")
|
||||
.setValue("0")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private handleSelection(interaction: AnySelectMenuInteraction) {
|
||||
const path = interaction.customId.substring(GroupConfigurationRenderer.SETVALUE_COMMAND.length);
|
||||
|
||||
const savingValue = this.getSaveValue(interaction, path);
|
||||
Container.get<Logger>("logger").debug(`Saving '${savingValue}' to '${path}'`);
|
||||
|
||||
this.configurationHandler.saveConfiguration(path, savingValue);
|
||||
}
|
||||
|
||||
private getSaveValue(interaction: AnySelectMenuInteraction, path: string): string {
|
||||
const transformerType = this.transformers.getTransformerType(path);
|
||||
if (transformerType === undefined || transformerType === null) {
|
||||
throw new Error(`Can not find transformer type for ${path}`)
|
||||
}
|
||||
const displaynames = new Intl.DisplayNames(["en"], {type: "language"});
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setOptions(
|
||||
options.map(intl => new StringSelectMenuOptionBuilder()
|
||||
.setLabel(displaynames.of(intl) ?? '')
|
||||
.setValue(intl)
|
||||
)
|
||||
)
|
||||
case TransformerType.Channel:
|
||||
return new ChannelSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setChannelTypes(ChannelType.GuildText)
|
||||
.setPlaceholder("New Value");
|
||||
case TransformerType.PermissionBoolean:
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId(GroupConfigurationRenderer.SETVALUE_COMMAND + breadcrumbPath)
|
||||
.setOptions(
|
||||
[
|
||||
{
|
||||
label: "Allow",
|
||||
value: "1"
|
||||
},
|
||||
{
|
||||
label: "Disallow",
|
||||
value: "0"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
case TransformerType.Channel:
|
||||
case TransformerType.PermissionBoolean:
|
||||
return interaction.values.join('; ');
|
||||
default:
|
||||
throw new Error("Unhandled select menu");
|
||||
}
|
||||
default:
|
||||
return new StringSelectMenuBuilder()
|
||||
.setCustomId("...")
|
||||
.setOptions(
|
||||
new StringSelectMenuOptionBuilder()
|
||||
.setLabel("Nothing to see here")
|
||||
.setValue("0")
|
||||
)
|
||||
}
|
||||
|
||||
private findCurrentUI(): {currentElement: Nullable<UIElement>, currentCollection: UIElementCollection } {
|
||||
let currentCollection: UIElementCollection = GroupConfigurationRenderer.UI_ELEMENTS;
|
||||
let currentElement: Nullable<UIElement> = null;
|
||||
}
|
||||
|
||||
for (const breadcrumb of this.breadcrumbs) {
|
||||
currentElement = currentCollection[breadcrumb];
|
||||
private handleSelection(interaction: AnySelectMenuInteraction) {
|
||||
const path = interaction.customId.substring(GroupConfigurationRenderer.SETVALUE_COMMAND.length);
|
||||
|
||||
if (currentElement.isConfiguration ?? false) {
|
||||
break;
|
||||
}
|
||||
const savingValue = this.getSaveValue(interaction, path);
|
||||
Container.get<Logger>("logger").debug(`Saving '${savingValue}' to '${path}'`);
|
||||
|
||||
currentCollection = currentElement.childrenElements ?? {};
|
||||
}
|
||||
|
||||
return {
|
||||
currentElement,
|
||||
currentCollection,
|
||||
}
|
||||
this.configurationHandler.saveConfiguration(path, savingValue);
|
||||
}
|
||||
|
||||
private getSaveValue(interaction: AnySelectMenuInteraction, path: string): string {
|
||||
const transformerType = this.transformers.getTransformerType(path);
|
||||
if (transformerType === undefined || transformerType === null) {
|
||||
throw new Error(`Can not find transformer type for ${path}`)
|
||||
}
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
case TransformerType.Channel:
|
||||
case TransformerType.PermissionBoolean:
|
||||
return interaction.values.join('; ');
|
||||
default:
|
||||
throw new Error("Unhandled select menu");
|
||||
}
|
||||
}
|
||||
|
||||
private findCurrentUI(): { currentElement: Nullable<UIElement>, currentCollection: UIElementCollection } {
|
||||
let currentCollection: UIElementCollection = GroupConfigurationRenderer.UI_ELEMENTS;
|
||||
let currentElement: Nullable<UIElement> = null;
|
||||
|
||||
for (const breadcrumb of this.breadcrumbs) {
|
||||
currentElement = currentCollection[breadcrumb];
|
||||
|
||||
if (currentElement.isConfiguration ?? false) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentCollection = currentElement.childrenElements ?? {};
|
||||
}
|
||||
|
||||
return {
|
||||
currentElement,
|
||||
currentCollection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,63 +4,63 @@ import {Nullable} from "../types/Nullable";
|
|||
import {ArrayUtils} from "../Utilities/ArrayUtils";
|
||||
|
||||
export enum TransformerType {
|
||||
Locale,
|
||||
Channel,
|
||||
PermissionBoolean,
|
||||
Locale,
|
||||
Channel,
|
||||
PermissionBoolean,
|
||||
}
|
||||
|
||||
type GroupConfigurationTransformer = {
|
||||
path: string[];
|
||||
type: TransformerType,
|
||||
path: string[];
|
||||
type: TransformerType,
|
||||
}
|
||||
|
||||
export type GroupConfigurationResult =
|
||||
ChannelId | Intl.Locale | boolean
|
||||
export type GroupConfigurationResult =
|
||||
ChannelId | Intl.Locale | boolean
|
||||
|
||||
export class GroupConfigurationTransformers {
|
||||
static TRANSFORMERS: GroupConfigurationTransformer[] = [
|
||||
{
|
||||
path: ['channels', 'newPlaydates'],
|
||||
type: TransformerType.Channel,
|
||||
},
|
||||
{
|
||||
path: ['channels', 'playdateReminders'],
|
||||
type: TransformerType.Channel,
|
||||
},
|
||||
{
|
||||
path: ['locale'],
|
||||
type: TransformerType.Locale,
|
||||
},
|
||||
{
|
||||
path: ['permissions', 'allowMemberManagingPlaydates'],
|
||||
type: TransformerType.PermissionBoolean
|
||||
}
|
||||
];
|
||||
|
||||
public getValue(configValue: GroupConfigurationModel): GroupConfigurationResult {
|
||||
const transformerType = this.getTransformerType(configValue.key);
|
||||
if (transformerType === undefined || transformerType === null) {
|
||||
throw new Error(`Can't find transformer for ${configValue.key}`);
|
||||
}
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
return new Intl.Locale(configValue.value)
|
||||
case TransformerType.Channel:
|
||||
return <ChannelId>configValue.value;
|
||||
case TransformerType.PermissionBoolean:
|
||||
return configValue.value === '1';
|
||||
}
|
||||
static TRANSFORMERS: GroupConfigurationTransformer[] = [
|
||||
{
|
||||
path: ['channels', 'newPlaydates'],
|
||||
type: TransformerType.Channel,
|
||||
},
|
||||
{
|
||||
path: ['channels', 'playdateReminders'],
|
||||
type: TransformerType.Channel,
|
||||
},
|
||||
{
|
||||
path: ['locale'],
|
||||
type: TransformerType.Locale,
|
||||
},
|
||||
{
|
||||
path: ['permissions', 'allowMemberManagingPlaydates'],
|
||||
type: TransformerType.PermissionBoolean
|
||||
}
|
||||
|
||||
public getTransformerType(configKey: string): Nullable<TransformerType> {
|
||||
const path = configKey.split('.');
|
||||
return GroupConfigurationTransformers.TRANSFORMERS.find(
|
||||
transformer => {
|
||||
return ArrayUtils.arraysEqual(transformer.path, path);
|
||||
}
|
||||
)?.type;
|
||||
];
|
||||
|
||||
public getValue(configValue: GroupConfigurationModel): GroupConfigurationResult {
|
||||
const transformerType = this.getTransformerType(configValue.key);
|
||||
if (transformerType === undefined || transformerType === null) {
|
||||
throw new Error(`Can't find transformer for ${configValue.key}`);
|
||||
}
|
||||
|
||||
|
||||
|
||||
switch (transformerType) {
|
||||
case TransformerType.Locale:
|
||||
return new Intl.Locale(configValue.value)
|
||||
case TransformerType.Channel:
|
||||
return <ChannelId>configValue.value;
|
||||
case TransformerType.PermissionBoolean:
|
||||
return configValue.value === '1';
|
||||
}
|
||||
}
|
||||
|
||||
public getTransformerType(configKey: string): Nullable<TransformerType> {
|
||||
const path = configKey.split('.');
|
||||
return GroupConfigurationTransformers.TRANSFORMERS.find(
|
||||
transformer => {
|
||||
return ArrayUtils.arraysEqual(transformer.path, path);
|
||||
}
|
||||
)?.type;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
12
source/Groups/RuntimeGroupConfiguration.d.ts
vendored
12
source/Groups/RuntimeGroupConfiguration.d.ts
vendored
|
|
@ -2,16 +2,16 @@ import {ChannelId} from "../types/DiscordTypes";
|
|||
import {Nullable} from "../types/Nullable";
|
||||
|
||||
export type RuntimeGroupConfiguration = {
|
||||
channels: Nullable<ChannelRuntimeGroupConfiguration>,
|
||||
locale: Intl.Locale,
|
||||
permissions: PermissionRuntimeGroupConfiguration
|
||||
channels: Nullable<ChannelRuntimeGroupConfiguration>,
|
||||
locale: Intl.Locale,
|
||||
permissions: PermissionRuntimeGroupConfiguration
|
||||
};
|
||||
|
||||
export type ChannelRuntimeGroupConfiguration = {
|
||||
newPlaydates: ChannelId,
|
||||
playdateReminders: ChannelId
|
||||
newPlaydates: ChannelId,
|
||||
playdateReminders: ChannelId
|
||||
}
|
||||
|
||||
export type PermissionRuntimeGroupConfiguration = {
|
||||
allowMemberManagingPlaydates: boolean
|
||||
allowMemberManagingPlaydates: boolean
|
||||
}
|
||||
|
|
@ -4,30 +4,30 @@ import {Nullable} from "../types/Nullable";
|
|||
|
||||
export class IconCache {
|
||||
private existingIcons: Map<string, string> | undefined;
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly client: DiscordClient
|
||||
) {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
public get(iconName: string): Snowflake | null {
|
||||
if (!this.existingIcons?.has(iconName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return this.existingIcons?.get(iconName) ?? null;
|
||||
}
|
||||
|
||||
|
||||
public getEmoji(iconName: string): string {
|
||||
const id = this.get(iconName);
|
||||
|
||||
|
||||
return formatEmoji({
|
||||
id,
|
||||
name: iconName
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async set(iconName: string, pngBuffer: Buffer) {
|
||||
const pngBase64 = pngBuffer.toString("base64");
|
||||
const iconDataUrl = `data:image/png;base64,${pngBase64}`;
|
||||
|
|
@ -42,23 +42,23 @@ export class IconCache {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
public async populate() {
|
||||
if (this.existingIcons != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const existingEmojis: Nullable<DiscordIconRequest> = await this.client.RESTClient.get(
|
||||
Routes.applicationEmojis(this.client.ApplicationId)
|
||||
)
|
||||
|
||||
|
||||
if (!existingEmojis) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.existingIcons = new Map<string, string>(
|
||||
existingEmojis.items.map((item) => {
|
||||
return [ item.name, item.id ]
|
||||
return [item.name, item.id]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,29 +5,30 @@ import {IconCache} from "./IconCache";
|
|||
|
||||
export class IconDeployer {
|
||||
static ICON_PATH = path.resolve('public/icons')
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly iconCache: IconCache
|
||||
) {}
|
||||
|
||||
) {
|
||||
}
|
||||
|
||||
public async ensureExistance() {
|
||||
const directory = await fs.promises.opendir(IconDeployer.ICON_PATH);
|
||||
const addIconPromises: Promise<void>[] = [];
|
||||
for await (const dirname of directory) {
|
||||
const iconName = path.basename(dirname.name, '.svg').replaceAll('-','_');
|
||||
|
||||
const iconName = path.basename(dirname.name, '.svg').replaceAll('-', '_');
|
||||
|
||||
if (this.iconCache.get(iconName) !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
addIconPromises.push(
|
||||
this.addIcon(path.resolve(dirname.parentPath, dirname.name), iconName)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
await Promise.all(addIconPromises);
|
||||
}
|
||||
|
||||
|
||||
private async addIcon(iconPath: string, iconName: string) {
|
||||
const svgBuffer = await fs.promises.readFile(iconPath, 'utf-8');
|
||||
const pngBuffer = await new Promise<Buffer>(resolve => {
|
||||
|
|
@ -48,8 +49,8 @@ export class IconDeployer {
|
|||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
await this.iconCache.set(iconName, pngBuffer);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
0
source/Menu/MenuRenderer.ts
Normal file
0
source/Menu/MenuRenderer.ts
Normal file
|
|
@ -2,7 +2,7 @@ import {Model} from "./Model";
|
|||
import {GroupModel} from "./GroupModel";
|
||||
|
||||
export interface GroupConfigurationModel extends Model {
|
||||
group: GroupModel;
|
||||
key: string;
|
||||
value: string;
|
||||
group: GroupModel;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import {Model} from "./Model";
|
|||
import {GuildMember, Role} from "../types/DiscordTypes";
|
||||
|
||||
export interface GroupModel extends Model {
|
||||
name: string;
|
||||
leader: GuildMember;
|
||||
role: Role;
|
||||
name: string;
|
||||
leader: GuildMember;
|
||||
role: Role;
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export interface Model {
|
||||
id: number;
|
||||
id: number | bigint;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {GroupModel} from "./GroupModel";
|
|||
import {Nullable} from "../types/Nullable";
|
||||
|
||||
export interface PlaydateModel extends Model {
|
||||
group: Nullable<GroupModel>
|
||||
from_time: Date,
|
||||
to_time: Date,
|
||||
group: Nullable<GroupModel>
|
||||
from_time: Date,
|
||||
to_time: Date,
|
||||
}
|
||||
|
|
@ -1,65 +1,65 @@
|
|||
import {Repository} from "./Repository";
|
||||
import GroupConfiguration, {DBGroupConfiguration} from "../Database/tables/GroupConfiguration";
|
||||
import {GroupConfigurationModel} from "../Models/GroupConfigurationModel";
|
||||
import { GroupModel } from "../Models/GroupModel";
|
||||
import {GroupModel} from "../Models/GroupModel";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {DatabaseConnection} from "../Database/DatabaseConnection";
|
||||
import {GroupRepository} from "./GroupRepository";
|
||||
|
||||
export class GroupConfigurationRepository extends Repository<GroupConfigurationModel, DBGroupConfiguration> {
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
private readonly groupRepository: GroupRepository,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
GroupConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
public findGroupConfigurations(group: GroupModel): GroupConfigurationModel[] {
|
||||
return this.database.fetchAll<number, DBGroupConfiguration>(`
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
private readonly groupRepository: GroupRepository,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
GroupConfiguration
|
||||
);
|
||||
}
|
||||
|
||||
public findGroupConfigurations(group: GroupModel): GroupConfigurationModel[] {
|
||||
return this.database.fetchAll<number, DBGroupConfiguration>(`
|
||||
SELECT * FROM groupConfiguration WHERE groupid = ?`,
|
||||
group.id
|
||||
).map((config) => {
|
||||
return this.convertToModelType(config, group);
|
||||
})
|
||||
}
|
||||
|
||||
public findConfigurationByPath(group: GroupModel, path: string): Nullable<GroupConfigurationModel> {
|
||||
const result = this.database.fetch<number, DBGroupConfiguration>(`
|
||||
group.id
|
||||
).map((config) => {
|
||||
return this.convertToModelType(config, group);
|
||||
})
|
||||
}
|
||||
|
||||
public findConfigurationByPath(group: GroupModel, path: string): Nullable<GroupConfigurationModel> {
|
||||
const result = this.database.fetch<number, DBGroupConfiguration>(`
|
||||
SELECT * FROM groupConfiguration WHERE groupid = ? AND key = ?`,
|
||||
group.id,
|
||||
path
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.convertToModelType(result, group);
|
||||
group.id,
|
||||
path
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected convertToModelType(intermediateModel: DBGroupConfiguration | undefined, group: Nullable<GroupModel> = null): GroupConfigurationModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("No intermediate model provided");
|
||||
}
|
||||
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
group: group ?? this.groupRepository.getById(intermediateModel.id),
|
||||
key: intermediateModel.key,
|
||||
value: intermediateModel.value,
|
||||
}
|
||||
|
||||
return this.convertToModelType(result, group);
|
||||
}
|
||||
|
||||
|
||||
protected convertToModelType(intermediateModel: DBGroupConfiguration | undefined, group: Nullable<GroupModel> = null): GroupConfigurationModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("No intermediate model provided");
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<GroupConfigurationModel>): object {
|
||||
return {
|
||||
groupid: instance.group?.id ?? undefined,
|
||||
key: instance.key ?? undefined,
|
||||
value: instance.value ?? undefined,
|
||||
}
|
||||
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
group: group ?? this.groupRepository.getById(intermediateModel.id),
|
||||
key: intermediateModel.key,
|
||||
value: intermediateModel.value,
|
||||
}
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<GroupConfigurationModel>): object {
|
||||
return {
|
||||
groupid: instance.group?.id ?? undefined,
|
||||
key: instance.key ?? undefined,
|
||||
value: instance.value ?? undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,94 +9,94 @@ import {Container} from "../Container/Container";
|
|||
|
||||
export class GroupRepository extends Repository<GroupModel, DBGroup> {
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
Groups
|
||||
);
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
Groups
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public findGroupByName(name: string): Nullable<GroupModel> {
|
||||
const result = this.database.fetch<string, DBGroup>(
|
||||
`SELECT * FROM groups WHERE name = ? LIMIT 1`,
|
||||
name
|
||||
)
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public findGroupByName(name: string): Nullable<GroupModel> {
|
||||
const result = this.database.fetch<string, DBGroup>(
|
||||
`SELECT * FROM groups WHERE name = ? LIMIT 1`,
|
||||
name
|
||||
)
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.convertToModelType(result);
|
||||
}
|
||||
|
||||
public findGroupsByRoles(server: string, roleIds: string[]): GroupModel[] {
|
||||
const template = roleIds.map(_roleId => '?').join(',');
|
||||
|
||||
const dbResult = this.database.fetchAll<number[], DBGroup>(`
|
||||
|
||||
return this.convertToModelType(result);
|
||||
}
|
||||
|
||||
public findGroupsByRoles(server: string, roleIds: string[]): GroupModel[] {
|
||||
const template = roleIds.map(_roleId => '?').join(',');
|
||||
|
||||
const dbResult = this.database.fetchAll<number[], DBGroup>(`
|
||||
SELECT * FROM groups WHERE server = ? AND role IN (${template})
|
||||
`,
|
||||
server,
|
||||
...roleIds)
|
||||
|
||||
|
||||
return dbResult.map((result) => this.convertToModelType(result));
|
||||
server,
|
||||
...roleIds)
|
||||
|
||||
|
||||
return dbResult.map((result) => this.convertToModelType(result));
|
||||
}
|
||||
|
||||
public findGroupsByMember(member: GuildMember, onlyLeader: boolean = false) {
|
||||
if (!member) {
|
||||
throw new Error("Can't find member for guild: none given");
|
||||
}
|
||||
|
||||
public findGroupsByMember(member: GuildMember, onlyLeader: boolean = false) {
|
||||
if (!member) {
|
||||
throw new Error("Can't find member for guild: none given");
|
||||
}
|
||||
|
||||
const groups = this.findGroupsByRoles(member.guild.id, [...member.roles.cache.keys()])
|
||||
|
||||
if (!onlyLeader) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
return groups.filter((group: GroupModel) => {
|
||||
return group.leader.memberid === member.id;
|
||||
})
|
||||
}
|
||||
|
||||
public deleteGroup(group: GroupModel): void {
|
||||
this.delete(group.id);
|
||||
|
||||
const repo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
const playdates = repo.findFromGroup(group, true)
|
||||
playdates.forEach((playdate) => {
|
||||
repo.delete(playdate.id);
|
||||
})
|
||||
}
|
||||
|
||||
protected convertToModelType(intermediateModel: DBGroup | undefined): GroupModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("No intermediate model provided");
|
||||
}
|
||||
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
name: intermediateModel.name,
|
||||
leader: {
|
||||
server: intermediateModel.server,
|
||||
memberid: intermediateModel.leader
|
||||
},
|
||||
role: {
|
||||
server: intermediateModel.server,
|
||||
roleid: intermediateModel.role
|
||||
}
|
||||
}
|
||||
const groups = this.findGroupsByRoles(member.guild.id, [...member.roles.cache.keys()])
|
||||
|
||||
if (!onlyLeader) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<GroupModel>): object {
|
||||
return {
|
||||
name: instance.name ?? '',
|
||||
server: instance.role?.server ?? null,
|
||||
leader: instance.leader?.memberid ?? null,
|
||||
role: instance.role?.roleid ?? null,
|
||||
}
|
||||
return groups.filter((group: GroupModel) => {
|
||||
return group.leader.memberid === member.id;
|
||||
})
|
||||
}
|
||||
|
||||
public deleteGroup(group: GroupModel): void {
|
||||
this.delete(group.id);
|
||||
|
||||
const repo = Container.get<PlaydateRepository>(PlaydateRepository.name);
|
||||
const playdates = repo.findFromGroup(group, true)
|
||||
playdates.forEach((playdate) => {
|
||||
repo.delete(playdate.id);
|
||||
})
|
||||
}
|
||||
|
||||
protected convertToModelType(intermediateModel: DBGroup | undefined): GroupModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("No intermediate model provided");
|
||||
}
|
||||
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
name: intermediateModel.name,
|
||||
leader: {
|
||||
server: intermediateModel.server,
|
||||
memberid: intermediateModel.leader
|
||||
},
|
||||
role: {
|
||||
server: intermediateModel.server,
|
||||
roleid: intermediateModel.role
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<GroupModel>): object {
|
||||
return {
|
||||
name: instance.name ?? '',
|
||||
server: instance.role?.server ?? null,
|
||||
leader: instance.leader?.memberid ?? null,
|
||||
role: instance.role?.roleid ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,90 +7,96 @@ import {GroupModel} from "../Models/GroupModel";
|
|||
import {Nullable} from "../types/Nullable";
|
||||
|
||||
export class PlaydateRepository extends Repository<PlaydateModel, DBPlaydate> {
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
private readonly groupRepository: GroupRepository,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
Playdate
|
||||
);
|
||||
}
|
||||
|
||||
findFromGroup(group: GroupModel, all = false) {
|
||||
let sql = `SELECT * FROM ${this.schema.name} WHERE groupid = ?`;
|
||||
const params = [group.id];
|
||||
|
||||
if (!all) {
|
||||
sql += " AND time_from > ?"
|
||||
params.push(new Date().getTime())
|
||||
}
|
||||
|
||||
const finds = this.database.fetchAll<number, DBPlaydate>(
|
||||
sql,
|
||||
...params
|
||||
);
|
||||
|
||||
return finds.map((playdate) => this.convertToModelType(playdate, group));
|
||||
}
|
||||
findPlaydatesInRange(fromDate: Date|number, toDate: Date|number, group: GroupModel | undefined = undefined) {
|
||||
if (fromDate instanceof Date) {
|
||||
fromDate = fromDate.getTime();
|
||||
}
|
||||
if (toDate instanceof Date) {
|
||||
toDate = toDate.getTime();
|
||||
}
|
||||
|
||||
let sql = `SELECT * FROM ${this.schema.name} WHERE time_from > ? AND time_from < ?`;
|
||||
const params = [fromDate, toDate];
|
||||
|
||||
if (group) {
|
||||
sql = `${sql} AND groupid = ?`
|
||||
params.push(group.id)
|
||||
}
|
||||
|
||||
const finds = this.database.fetchAll<number, DBPlaydate>(
|
||||
sql,
|
||||
...params
|
||||
);
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
private readonly groupRepository: GroupRepository,
|
||||
) {
|
||||
super(
|
||||
database,
|
||||
Playdate
|
||||
);
|
||||
}
|
||||
|
||||
return finds.map((playdate) => this.convertToModelType(playdate, group));
|
||||
findFromGroup(group: GroupModel, all = false) {
|
||||
let sql = `SELECT * FROM ${this.schema.name} WHERE groupid = ?`;
|
||||
const params = [group.id];
|
||||
|
||||
if (!all) {
|
||||
sql += " AND time_from > ?"
|
||||
params.push(new Date().getTime())
|
||||
}
|
||||
|
||||
const finds = this.database.fetchAll<number, DBPlaydate>(
|
||||
sql,
|
||||
...params
|
||||
);
|
||||
|
||||
return finds.map((playdate) => this.convertToModelType(playdate, group));
|
||||
}
|
||||
|
||||
findPlaydatesInRange(fromDate: Date | number, toDate: Date | number | undefined = undefined, group: GroupModel | undefined = undefined) {
|
||||
if (fromDate instanceof Date) {
|
||||
fromDate = fromDate.getTime();
|
||||
}
|
||||
if (toDate instanceof Date) {
|
||||
toDate = toDate.getTime();
|
||||
}
|
||||
|
||||
let sql = `SELECT * FROM ${this.schema.name} WHERE time_from > ?`;
|
||||
const params = [fromDate];
|
||||
|
||||
if (toDate) {
|
||||
sql = `${sql} AND time_from < ?`
|
||||
params.push(toDate);
|
||||
}
|
||||
|
||||
getNextPlaydateForGroup(group: GroupModel): PlaydateModel | null {
|
||||
const sql = `SELECT * FROM ${this.schema.name} WHERE groupid = ? AND time_from > ? ORDER BY time_from LIMIT 1`;
|
||||
|
||||
const find = this.database.fetch<number, DBPlaydate>(
|
||||
sql,
|
||||
group.id,
|
||||
Date.now()
|
||||
)
|
||||
|
||||
if (!find) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.convertToModelType(find, group)
|
||||
if (group) {
|
||||
sql = `${sql} AND groupid = ?`
|
||||
params.push(<number>group.id)
|
||||
}
|
||||
|
||||
protected convertToModelType(intermediateModel: DBPlaydate | undefined, fixedGroup: Nullable<GroupModel> = null): PlaydateModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("Unable to convert the playdate model");
|
||||
}
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
group: fixedGroup ?? this.groupRepository.getById(intermediateModel.groupid),
|
||||
from_time: new Date(intermediateModel.time_from),
|
||||
to_time: new Date(intermediateModel.time_to),
|
||||
};
|
||||
|
||||
const finds = this.database.fetchAll<number, DBPlaydate>(
|
||||
sql,
|
||||
...params
|
||||
);
|
||||
|
||||
return finds.map((playdate) => this.convertToModelType(playdate, group));
|
||||
}
|
||||
|
||||
getNextPlaydateForGroup(group: GroupModel): PlaydateModel | null {
|
||||
const sql = `SELECT * FROM ${this.schema.name} WHERE groupid = ? AND time_from > ? ORDER BY time_from LIMIT 1`;
|
||||
|
||||
const find = this.database.fetch<number, DBPlaydate>(
|
||||
sql,
|
||||
group.id,
|
||||
Date.now()
|
||||
)
|
||||
|
||||
if (!find) {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<PlaydateModel>): object {
|
||||
return {
|
||||
groupid: instance.group?.id ?? null,
|
||||
time_from: instance.from_time?.getTime() ?? 0,
|
||||
time_to: instance.to_time?.getTime() ?? 0,
|
||||
}
|
||||
|
||||
return this.convertToModelType(find, group)
|
||||
}
|
||||
|
||||
protected convertToModelType(intermediateModel: DBPlaydate | undefined, fixedGroup: Nullable<GroupModel> = null): PlaydateModel {
|
||||
if (!intermediateModel) {
|
||||
throw new Error("Unable to convert the playdate model");
|
||||
}
|
||||
return {
|
||||
id: intermediateModel.id,
|
||||
group: fixedGroup ?? this.groupRepository.getById(intermediateModel.groupid),
|
||||
from_time: new Date(intermediateModel.time_from),
|
||||
to_time: new Date(intermediateModel.time_to),
|
||||
};
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<PlaydateModel>): object {
|
||||
return {
|
||||
groupid: instance.group?.id ?? null,
|
||||
time_from: instance.from_time?.getTime() ?? 0,
|
||||
time_to: instance.to_time?.getTime() ?? 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,83 +1,85 @@
|
|||
import {DatabaseConnection} from "../Database/DatabaseConnection";
|
||||
import {Model} from "../Models/Model";
|
||||
import { Nullable } from "../types/Nullable";
|
||||
import {Nullable} from "../types/Nullable";
|
||||
import {DatabaseDefinition} from "../Database/DatabaseDefinition";
|
||||
import {Container} from "../Container/Container";
|
||||
import {EventHandler} from "../Events/EventHandler";
|
||||
import {ElementCreatedEvent} from "../Events/ElementCreatedEvent";
|
||||
|
||||
export class Repository<ModelType extends Model, IntermediateModelType = unknown> {
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
public readonly schema: DatabaseDefinition,
|
||||
) {}
|
||||
|
||||
public create(instance: Partial<ModelType>): number|bigint {
|
||||
const columnNames = this.schema.columns.filter((column) => {
|
||||
return !column.primaryKey
|
||||
}).map((column) => {
|
||||
return column.name;
|
||||
});
|
||||
|
||||
const createObject = this.convertToCreateObject(instance);
|
||||
const keys = Object.keys(createObject);
|
||||
const missingColumns = columnNames.filter((columnName) => {
|
||||
return !keys.includes(columnName);
|
||||
})
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Error("Can't create instance, due to missing column values: " + missingColumns);
|
||||
}
|
||||
|
||||
const sql = `INSERT INTO ${this.schema.name}(${Object.keys(createObject).join(',')})
|
||||
VALUES (${Object.keys(createObject).map(() => "?").join(',')})`;
|
||||
const result = this.database.execute(sql, ...Object.values(createObject));
|
||||
const id = result.lastInsertRowid;
|
||||
|
||||
Container.get<EventHandler>(EventHandler.name).dispatch(new ElementCreatedEvent<ModelType>(this.schema.name, instance, id));
|
||||
|
||||
return id;
|
||||
|
||||
constructor(
|
||||
protected readonly database: DatabaseConnection,
|
||||
public readonly schema: DatabaseDefinition,
|
||||
) {
|
||||
}
|
||||
|
||||
public create(instance: Partial<ModelType>): number | bigint {
|
||||
const columnNames = this.schema.columns.filter((column) => {
|
||||
return !column.primaryKey
|
||||
}).map((column) => {
|
||||
return column.name;
|
||||
});
|
||||
|
||||
const createObject = this.convertToCreateObject(instance);
|
||||
const keys = Object.keys(createObject);
|
||||
const missingColumns = columnNames.filter((columnName) => {
|
||||
return !keys.includes(columnName);
|
||||
})
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Error("Can't create instance, due to missing column values: " + missingColumns);
|
||||
}
|
||||
|
||||
public update(instance: Partial<ModelType>&{id: number}): boolean {
|
||||
const columnNames = this.schema.columns.filter((column) => {
|
||||
return !column.primaryKey
|
||||
}).map((column) => {
|
||||
return column.name;
|
||||
});
|
||||
|
||||
const createObject = this.convertToCreateObject(instance);
|
||||
const keys = Object.keys(createObject);
|
||||
const missingColumns = columnNames.filter((columnName) => {
|
||||
return !keys.includes(columnName);
|
||||
})
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Error("Can't create instance, due to missing column values: " + missingColumns);
|
||||
}
|
||||
const sql = `INSERT INTO ${this.schema.name}(${Object.keys(createObject).join(',')})
|
||||
VALUES (${Object.keys(createObject).map(() => "?").join(',')})`;
|
||||
const result = this.database.execute(sql, ...Object.values(createObject));
|
||||
const id = result.lastInsertRowid;
|
||||
|
||||
const sql = `UPDATE ${this.schema.name}
|
||||
Container.get<EventHandler>(EventHandler.name).dispatch(new ElementCreatedEvent<ModelType>(this.schema.name, instance, id));
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public update(instance: Partial<ModelType> & { id: number }): boolean {
|
||||
const columnNames = this.schema.columns.filter((column) => {
|
||||
return !column.primaryKey
|
||||
}).map((column) => {
|
||||
return column.name;
|
||||
});
|
||||
|
||||
const createObject = this.convertToCreateObject(instance);
|
||||
const keys = Object.keys(createObject);
|
||||
const missingColumns = columnNames.filter((columnName) => {
|
||||
return !keys.includes(columnName);
|
||||
})
|
||||
|
||||
if (missingColumns.length > 0) {
|
||||
throw new Error("Can't create instance, due to missing column values: " + missingColumns);
|
||||
}
|
||||
|
||||
const sql = `UPDATE ${this.schema.name}
|
||||
SET ${Object.keys(createObject).map((key) => `${key} = ?`).join(',')}
|
||||
WHERE id = ?`;
|
||||
const result = this.database.execute(sql, ...Object.values(createObject), instance.id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
public getById(id: number): Nullable<ModelType> {
|
||||
const sql = `SELECT * FROM ${this.schema.name} WHERE id = ? LIMIT 1`;
|
||||
return this.convertToModelType(this.database.fetch<number, IntermediateModelType>(sql, id));
|
||||
}
|
||||
|
||||
public delete(id: number) {
|
||||
const sql = `DELETE FROM ${this.schema.name} WHERE id = ?`;
|
||||
return this.database.execute(sql, id);
|
||||
}
|
||||
|
||||
protected convertToModelType(intermediateModel: IntermediateModelType | undefined): ModelType {
|
||||
return intermediateModel as unknown as ModelType;
|
||||
}
|
||||
protected convertToCreateObject(instance: Partial<ModelType>): object {
|
||||
return instance;
|
||||
}
|
||||
const result = this.database.execute(sql, ...Object.values(createObject), instance.id);
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
public getById(id: number): Nullable<ModelType> {
|
||||
const sql = `SELECT * FROM ${this.schema.name} WHERE id = ? LIMIT 1`;
|
||||
return this.convertToModelType(this.database.fetch<number, IntermediateModelType>(sql, id));
|
||||
}
|
||||
|
||||
public delete(id: number) {
|
||||
const sql = `DELETE FROM ${this.schema.name} WHERE id = ?`;
|
||||
return this.database.execute(sql, id);
|
||||
}
|
||||
|
||||
protected convertToModelType(intermediateModel: IntermediateModelType | undefined): ModelType {
|
||||
return intermediateModel as unknown as ModelType;
|
||||
}
|
||||
|
||||
protected convertToCreateObject(instance: Partial<ModelType>): object {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
export class ArrayUtils {
|
||||
public static chooseRandom<T>(array: Array<T>):T {
|
||||
const index = Math.floor(Math.random() * array.length);
|
||||
return array[index];
|
||||
}
|
||||
public static chooseRandom<T>(array: Array<T>): T {
|
||||
const index = Math.floor(Math.random() * array.length);
|
||||
return array[index];
|
||||
}
|
||||
|
||||
public static arraysEqual<T>(a: Array<T>, b: Array<T>): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
public static arraysEqual<T>(a: Array<T>, b: Array<T>): boolean {
|
||||
if (a === b) return true;
|
||||
if (a == null || b == null) return false;
|
||||
if (a.length !== b.length) return false;
|
||||
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -21,26 +21,18 @@ const logger = container.get<Logger>("logger");
|
|||
const client = container.get<DiscordClient>(DiscordClient.name);
|
||||
client.connectRESTClient(environment.discord.token)
|
||||
|
||||
logger.log("Deploying Commands...")
|
||||
|
||||
const deployer = new CommandDeployer(client, logger);
|
||||
(async () => {
|
||||
await deployer.deployAvailableServers()
|
||||
})()
|
||||
|
||||
|
||||
logger.log("Ensuring Database...");
|
||||
const updater = new DatabaseUpdater(container.get<DatabaseConnection>(DatabaseConnection.name));
|
||||
updater.ensureAvaliablity(Definitions);
|
||||
|
||||
logger.log("Ensuring icons...");
|
||||
(async () => {
|
||||
const iconCache = container.get<IconCache>(IconCache.name);
|
||||
await iconCache.populate();
|
||||
|
||||
const deployer = new IconDeployer(
|
||||
iconCache
|
||||
);
|
||||
const iconCache = container.get<IconCache>(IconCache.name);
|
||||
await iconCache.populate();
|
||||
|
||||
deployer.ensureExistance()
|
||||
const deployer = new IconDeployer(
|
||||
iconCache
|
||||
);
|
||||
|
||||
deployer.ensureExistance()
|
||||
})()
|
||||
|
|
@ -14,9 +14,9 @@ DefaultEvents.setupHandlers();
|
|||
|
||||
const client = container.get<DiscordClient>(DiscordClient.name);
|
||||
client.connectRESTClient(env.discord.token);
|
||||
|
||||
|
||||
await container.get<IconCache>(IconCache.name).populate()
|
||||
|
||||
|
||||
client.applyEvents()
|
||||
client.connect(env.discord.token)
|
||||
})()
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
export type Class = {constructor: {name: string}}
|
||||
export type ClassNamed = {name: string}
|
||||
export type Class = { constructor: { name: string } }
|
||||
export type ClassNamed = { name: string }
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
export type GuildMember = {
|
||||
server: string;
|
||||
memberid: string;
|
||||
server: string;
|
||||
memberid: string;
|
||||
}
|
||||
|
||||
export type Role = {
|
||||
server: string;
|
||||
roleid: string;
|
||||
server: string;
|
||||
roleid: string;
|
||||
};
|
||||
|
||||
export type ChannelId = string;
|
||||
Loading…
Add table
Add a link
Reference in a new issue