diff --git a/package.json b/package.json index 4a79dad5..2e9e27b7 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "globals": "^15.1.0", "postcss-html": "^1.6.0", "typescript": "^5.4.5", - "typescript-eslint": "^7.8.0" + "typescript-eslint": "^7.8.0", + "@vue/language-server": "^2.1.10" }, "packageManager": "pnpm@9.1.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac7113fe..fb8b5be6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@eslint/js': specifier: ^9.2.0 version: 9.2.0 + '@vue/language-server': + specifier: ^2.1.10 + version: 2.1.10(typescript@5.4.5) eslint: specifier: ^8.57.0 version: 8.57.0 @@ -60,8 +63,8 @@ importers: specifier: ^0.37.79 version: 0.37.83 discord.js: - specifier: ^14.15.2 - version: 14.15.2 + specifier: 14.17.0 + version: 14.17.0 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -207,7 +210,7 @@ importers: version: 9.25.0(eslint@8.57.0) unplugin-vue-components: specifier: ^0.26.0 - version: 0.26.0(@babel/parser@7.24.5)(rollup@4.17.2)(vue@3.4.27(typescript@5.4.5)) + version: 0.26.0(@babel/parser@7.26.3)(rollup@4.17.2)(vue@3.4.27(typescript@5.4.5)) vite: specifier: ^5.2.11 version: 5.2.11(@types/node@22.5.5) @@ -228,10 +231,18 @@ packages: resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.24.5': resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + '@babel/highlight@7.24.5': resolution: {integrity: sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==} engines: {node: '>=6.9.0'} @@ -241,6 +252,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.26.3': + resolution: {integrity: sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/runtime@7.24.5': resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==} engines: {node: '>=6.9.0'} @@ -249,6 +265,10 @@ packages: resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==} engines: {node: '>=6.9.0'} + '@babel/types@7.26.3': + resolution: {integrity: sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==} + engines: {node: '>=6.9.0'} + '@d-fischer/cache-decorators@4.0.1': resolution: {integrity: sha512-HNYLBLWs/t28GFZZeqdIBqq8f37mqDIFO6xNPof94VjpKvuP6ROqCZGafx88dk5zZUlBfViV9jD8iNNlXfc4CA==} @@ -274,20 +294,20 @@ packages: '@d-fischer/typed-event-emitter@3.3.3': resolution: {integrity: sha512-OvSEOa8icfdWDqcRtjSEZtgJTFOFNgTjje7zaL0+nAtu2/kZtRCSK5wUMrI/aXtCH8o0Qz2vA8UqkhWUTARFQQ==} - '@discordjs/builders@1.8.1': - resolution: {integrity: sha512-GkF+HM01FHy+NSoTaUPR8z44otfQgJ1AIsRxclYGUZDyUbdZEFyD/5QVv2Y1Flx6M+B0bQLzg2M9CJv5lGTqpA==} + '@discordjs/builders@1.10.0': + resolution: {integrity: sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==} engines: {node: '>=16.11.0'} '@discordjs/collection@1.5.3': resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} engines: {node: '>=16.11.0'} - '@discordjs/collection@2.1.0': - resolution: {integrity: sha512-mLcTACtXUuVgutoznkh6hS3UFqYirDYAg5Dc1m8xn6OvPjetnUlf/xjtqnnc47OwWdaoCQnHmHh9KofhD6uRqw==} + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} engines: {node: '>=18'} - '@discordjs/formatters@0.4.0': - resolution: {integrity: sha512-fJ06TLC1NiruF35470q3Nr1bi95BdvKFAF+T5bNfZJ4bNdqZ3VZ+Ttg6SThqTxm6qumSG3choxLBHMC69WXNXQ==} + '@discordjs/formatters@0.6.0': + resolution: {integrity: sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==} engines: {node: '>=16.11.0'} '@discordjs/node-pre-gyp@0.4.5': @@ -298,22 +318,43 @@ packages: resolution: {integrity: sha512-NEE76A96FtQ5YuoAVlOlB3ryMPrkXbUCTQICHGKb8ShtjXyubGicjRMouHtP1RpuDdm16cDa+oI3aAMo1zQRUQ==} engines: {node: '>=12.0.0'} - '@discordjs/rest@2.3.0': - resolution: {integrity: sha512-C1kAJK8aSYRv3ZwMG8cvrrW4GN0g5eMdP8AuN8ODH5DyOCbHgJspze1my3xHOAgwLJdKUbWNVyAeJ9cEdduqIg==} - engines: {node: '>=16.11.0'} + '@discordjs/rest@2.4.1': + resolution: {integrity: sha512-a4Vv69usWY4ypwcENzH6QWYNrPwu+es3tRQMLKgaUFs0Mx2Yn7xfDW62++kcgckG8XbHWO7ej1x7raS7LuYgvA==} + engines: {node: '>=18'} - '@discordjs/util@1.1.0': - resolution: {integrity: sha512-IndcI5hzlNZ7GS96RV3Xw1R2kaDuXEp7tRIy/KlhidpN/BQ1qh1NZt3377dMLTa44xDUNKT7hnXkA/oUAzD/lg==} - engines: {node: '>=16.11.0'} + '@discordjs/util@1.1.1': + resolution: {integrity: sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==} + engines: {node: '>=18'} '@discordjs/voice@0.17.0': resolution: {integrity: sha512-hArn9FF5ZYi1IkxdJEVnJi+OxlwLV0NJYWpKXsmNOojtGtAZHxmsELA+MZlu2KW1F/K1/nt7lFOfcMXNYweq9w==} engines: {node: '>=16.11.0'} - '@discordjs/ws@1.1.0': - resolution: {integrity: sha512-O97DIeSvfNTn5wz5vaER6ciyUsr7nOqSEtsLoMhhIgeFkhnxLRqSr00/Fpq2/ppLgjDGLbQCDzIK7ilGoB/M7A==} + '@discordjs/ws@1.2.0': + resolution: {integrity: sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==} engines: {node: '>=16.11.0'} + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.0': + resolution: {integrity: sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + '@emnapi/runtime@1.2.0': resolution: {integrity: sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==} @@ -769,6 +810,9 @@ packages: cpu: [x64] os: [win32] + '@johnsoncodehk/pug-beautify@0.2.2': + resolution: {integrity: sha512-qqNS/YD0Nck5wtQLCPHAfGVgWbbGafxSPjNh0ekYPFSNNqnDH2kamnduzYly8IiADmeVx/MfAE1njMEjVeHTMA==} + '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} @@ -880,12 +924,12 @@ packages: cpu: [x64] os: [win32] - '@sapphire/async-queue@1.5.2': - resolution: {integrity: sha512-7X7FFAA4DngXUl95+hYbUF19bp1LGiffjJtu7ygrZrbdCSsdDDBaSjB7Akw0ZbOu6k0xpXyljnJ6/RZUvLfRdg==} + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - '@sapphire/shapeshift@3.9.7': - resolution: {integrity: sha512-4It2mxPSr4OGn4HSQWGmhFMsNFGfFVhWeRPCRwbH972Ek2pzfGRZtb0pJ4Ze6oIzcyh2jw7nUDa6qGlWofgd9g==} + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} engines: {node: '>=v16'} '@sapphire/snowflake@3.5.3': @@ -1042,31 +1086,64 @@ packages: vite: ^5.0.0 vue: ^3.2.25 - '@vladfrangu/async_event_emitter@2.2.4': - resolution: {integrity: sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==} + '@vladfrangu/async_event_emitter@2.4.6': + resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} engines: {node: '>=v14.0.0', npm: '>=7.0.0'} '@volar/language-core@2.2.1': resolution: {integrity: sha512-iHJAZKcYldZgyS8gx6DfIZApViVBeqbf6iPhqoZpG5A6F4zsZiFldKfwaKaBA3/wnOTWE2i8VUbXywI1WywCPg==} + '@volar/language-core@2.4.11': + resolution: {integrity: sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==} + + '@volar/language-server@2.4.11': + resolution: {integrity: sha512-W9P8glH1M8LGREJ7yHRCANI5vOvTrRO15EMLdmh5WNF9sZYSEbQxiHKckZhvGIkbeR1WAlTl3ORTrJXUghjk7g==} + + '@volar/language-service@2.4.11': + resolution: {integrity: sha512-KIb6g8gjUkS2LzAJ9bJCLIjfsJjeRtmXlu7b2pDFGD3fNqdbC53cCAKzgWDs64xtQVKYBU13DLWbtSNFtGuMLQ==} + '@volar/source-map@2.2.1': resolution: {integrity: sha512-w1Bgpguhbp7YTr7VUFu6gb4iAZjeEPsOX4zpgiuvlldbzvIWDWy4t0jVifsIsxZ99HAu+c3swiME7wt+GeNqhA==} + '@volar/source-map@2.4.11': + resolution: {integrity: sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==} + + '@volar/test-utils@2.4.11': + resolution: {integrity: sha512-ogkLldPqFa/j9302Ns+nWeyTCQv8d4c7iN4t8ziq7j0XeMKWYsTAjLsx/9z0MTNrecBAcocgzEvCricASSq+Hw==} + '@volar/typescript@2.2.1': resolution: {integrity: sha512-Z/tqluR7Hz5/5dCqQp7wo9C/6tSv/IYl+tTzgzUt2NjTq95bKSsuO4E+V06D0c+3aP9x5S9jggLqw451hpnc6Q==} + '@volar/typescript@2.4.11': + resolution: {integrity: sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==} + + '@vscode/emmet-helper@2.11.0': + resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + '@vue/compiler-core@3.4.27': resolution: {integrity: sha512-E+RyqY24KnyDXsCuQrI+mlcdW3ALND6U7Gqa/+bVwbcpcR3BRRIckFoz7Qyd4TTlnugtwuI7YgjbvsLmxb+yvg==} + '@vue/compiler-core@3.5.13': + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + '@vue/compiler-dom@3.4.27': resolution: {integrity: sha512-kUTvochG/oVgE1w5ViSr3KUBh9X7CWirebA3bezTbB5ZKBQZwR2Mwj9uoSKRMFcz4gSMzzLXBPD6KpCLb9nvWw==} + '@vue/compiler-dom@3.5.13': + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + '@vue/compiler-sfc@3.4.27': resolution: {integrity: sha512-nDwntUEADssW8e0rrmE0+OrONwmRlegDA1pD6QhVeXxjIytV03yDqTey9SBDiALsvAd5U4ZrEKbMyVXhX6mCGA==} '@vue/compiler-ssr@3.4.27': resolution: {integrity: sha512-CVRzSJIltzMG5FcidsW0jKNQnNRYC8bT21VegyMMtHmhW3UOI7knmUehzswXLrExDLE6lQCZdrhD4ogI7c+vuw==} + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + '@vue/devtools-api@6.6.1': resolution: {integrity: sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA==} @@ -1078,6 +1155,21 @@ packages: typescript: optional: true + '@vue/language-core@2.1.10': + resolution: {integrity: sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-server@2.1.10': + resolution: {integrity: sha512-Cjxi6nmMVDxOpWWfq1jCe9UqHg/WrTxwZGAXtYXByOhueiRi1kOlHR77vkRQRi40jpZIPWEnjgD/jMMhfLe0ag==} + hasBin: true + + '@vue/language-service@2.1.10': + resolution: {integrity: sha512-voNteQNU+mHUmyUEdHUpWZW5/kfLQxh1zHs98c5hw6XK6pNxJTghG/k4maka6y5WmfmMCKAsddBpD9kZ7lyAlg==} + '@vue/reactivity@3.4.27': resolution: {integrity: sha512-kK0g4NknW6JX2yySLpsm2jlunZJl2/RJGZ0H9ddHdfBVHcNzxmQ0sS0b09ipmBoQpY8JM2KmUw+a6sO8Zo+zIA==} @@ -1095,9 +1187,15 @@ packages: '@vue/shared@3.4.27': resolution: {integrity: sha512-DL3NmY2OFlqmYYrzp39yi3LDkKxa5vZVwxWdQ3rG0ekuWscHraeIbnI8t+aZK7qhYqEqWKTUdijadunb9pnrgA==} + '@vue/shared@3.5.13': + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + '@vue/tsconfig@0.5.1': resolution: {integrity: sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==} + '@vue/typescript-plugin@2.1.10': + resolution: {integrity: sha512-NrS3BB3l5vuZHU4Vp8l9TbT5pC7VjBfwZKqc24dAXF3Z+dJyGs4mcC3zo59gUggLMQSah8mdXj8xqEfMkrps8w==} + '@vueuse/core@10.9.0': resolution: {integrity: sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==} @@ -1134,6 +1232,11 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} engines: {node: '>=0.4.0'} @@ -1154,6 +1257,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + alien-signals@0.2.2: + resolution: {integrity: sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1252,10 +1358,18 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + call-bind-apply-helpers@1.0.1: + resolution: {integrity: sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==} + engines: {node: '>= 0.4'} + call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1283,6 +1397,9 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + character-parser@2.2.0: + resolution: {integrity: sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==} + cheminfo-types@1.7.3: resolution: {integrity: sha512-KIKBULfo+XwkSBwMfwjBmZCmY+RXisN2kRc33WikuWBsCQQy5alHWYVrMCO8//lDvy9h1giOzwsC9kgq0OahUw==} @@ -1464,12 +1581,15 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + discord-api-types@0.37.114: + resolution: {integrity: sha512-9b9oOpktWSmE6ooToc46wfw151SHC/+idmnZvtwpEzW85BijUspQxj4W2uOmo+nZVTdEyb3fku58k+4rHKpdSQ==} + discord-api-types@0.37.83: resolution: {integrity: sha512-urGGYeWtWNYMKnYlZnOnDHm8fVRffQs3U0SpE8RHeiuLKb/u92APS8HoQnPTFbnXmY1vVnXjXO4dOxcAn3J+DA==} - discord.js@14.15.2: - resolution: {integrity: sha512-wGD37YCaTUNprtpqMIRuNiswwsvSWXrHykBSm2SAosoTYut0VUDj9yo9t4iLtMKvuhI49zYkvKc2TNdzdvpJhg==} - engines: {node: '>=16.11.0'} + discord.js@14.17.0: + resolution: {integrity: sha512-bK6oee/RBZF/LCPnR4afECsJV5voUVmfDmOHAMJSJeDeR42EkVyZJN3QVp1Uo0c3/OG0Z6vQVveY4+tFaMO4og==} + engines: {node: '>=18'} doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} @@ -1495,12 +1615,19 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + ee-first@1.1.1: resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=} efrt-unpack@2.2.0: resolution: {integrity: sha512-9xUSSj7qcUxz+0r4X3+bwUNttEfGfK5AH+LVa1aTpqdAfrN5VhROYCfcF+up4hp5OL7IUKcZJJrzAGipQRDoiQ==} + emmet@2.4.11: + resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1530,10 +1657,18 @@ packages: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + esbuild@0.20.2: resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} engines: {node: '>=12'} @@ -1705,6 +1840,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-intrinsic@1.2.6: + resolution: {integrity: sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==} + engines: {node: '>= 0.4'} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -1738,6 +1877,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1773,6 +1916,14 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + has-unicode@2.0.1: resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} @@ -1881,6 +2032,9 @@ packages: is-core-module@2.13.1: resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-expression@4.0.0: + resolution: {integrity: sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1916,6 +2070,10 @@ packages: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -1959,6 +2117,12 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + junk@4.0.1: resolution: {integrity: sha512-Qush0uP+G8ZScpGMZvHUiRfI0YBWuB3gVBYlI0v0vvOJt5FLicco+IkP0a50LqTTQhmts/m6tP5SWE+USyIvcQ==} engines: {node: '>=12.20'} @@ -2038,6 +2202,10 @@ packages: engines: {node: '>= 18'} hasBin: true + math-intrinsics@1.0.0: + resolution: {integrity: sha512-4MqMiKP90ybymYvsut0CH2g4XWbfLtmlCkXmtmdcDCxNB+mQcu1w/1+L/VD7vi/PSv7X2JYV7SCcR+jiPXnQtA==} + engines: {node: '>= 0.4'} + media-typer@0.3.0: resolution: {integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=} engines: {node: '>= 0.6'} @@ -2453,6 +2621,15 @@ packages: pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + pug-error@2.1.0: + resolution: {integrity: sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==} + + pug-lexer@5.0.1: + resolution: {integrity: sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==} + + pug-parser@6.0.0: + resolution: {integrity: sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2502,6 +2679,9 @@ packages: regexp-to-ast@0.4.0: resolution: {integrity: sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==} + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2567,6 +2747,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} engines: {node: '>= 0.8.0'} @@ -2732,6 +2917,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + token-stream@1.0.0: + resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} + touch@3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true @@ -2755,6 +2943,9 @@ packages: tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.19.1: resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} engines: {node: '>=18.0.0'} @@ -2786,6 +2977,9 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + typescript-auto-import-cache@0.3.5: + resolution: {integrity: sha512-fAIveQKsoYj55CozUiBoj4b/7WpN0i4o74wiGY5JVUEoD0XiqDk1tJqTEjgzL2/AizKQrXxyRosSebyDzBZKjw==} + typescript-eslint@7.8.0: resolution: {integrity: sha512-sheFG+/D8N/L7gC3WT0Q8sB97Nm573Yfr+vZFzl/4nBdYcmviBPtwGSX9TJ7wpVg28ocerKVOt+k2eGmHzcgVA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -2807,9 +3001,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - undici@6.13.0: - resolution: {integrity: sha512-Q2rtqmZWrbP8nePMq7mOJIN98M0fYvSgV89vwl/BQRT4mDOeY2GXZngfGpcBBhtky3woM7G24wZV3Q304Bv6cw==} - engines: {node: '>=18.0'} + undici@6.19.8: + resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} + engines: {node: '>=18.17'} unhead@1.9.10: resolution: {integrity: sha512-Y3w+j1x1YFig2YuE+W2sER+SciRR7MQktYRHNqvZJ0iUNCCJTS8Z/SdSMUEeuFV28daXeASlR3fy7Ry3O2indg==} @@ -2883,6 +3077,97 @@ packages: terser: optional: true + volar-service-css@0.0.62: + resolution: {integrity: sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.62: + resolution: {integrity: sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.62: + resolution: {integrity: sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-json@0.0.62: + resolution: {integrity: sha512-Ot+jP+/LzKcaGF7nzrn+gwpzAleb4ej5buO05M8KxfwfODte7o1blARKRoJ3Nv7ls0DBM38Dd5vjzvA9c/9Jtg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-pug-beautify@0.0.62: + resolution: {integrity: sha512-dAFNuNEwTnnVthYoNJhoStwhf/PojzglwCrdhOb2nBegTG3xXMWRFmQzb0JfIlt2wq2wfUq5j+JJswgSD3KluQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-pug@0.0.62: + resolution: {integrity: sha512-C0/O8uGnRfijWKE0zFXxJ/o7BbLebzretsEaiMkvBDIxm5oe7HRDzQr6CgknV/WVgiohZ74v+0CwBPl2YmcPUQ==} + + volar-service-typescript-twoslash-queries@0.0.62: + resolution: {integrity: sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.62: + resolution: {integrity: sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.3.2: + resolution: {integrity: sha512-GEpPxrUTAeXWdZWHev1OJU9lz2Q2/PPBxQ2TIRmLGvQiH3WZbqaNoute0n0ewxlgtjzTW3AKZT+NHySk5Rf4Eg==} + + vscode-html-languageservice@5.3.1: + resolution: {integrity: sha512-ysUh4hFeW/WOWz/TO9gm08xigiSsV/FOAZ+DolgJfeLftna54YdmZ4A+lIn46RbdO3/Qv5QHTn1ZGqmrXQhZyA==} + + vscode-json-languageservice@5.4.2: + resolution: {integrity: sha512-2qujUseKRbLEwLXvEOFAxaz3y1ssdNCXXi95LRdG8AFchJHSnmI2qCg9ixoYxbJtSehIrXOmkhV87Y9lIivOgQ==} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vue-confetti@2.3.0: resolution: {integrity: sha512-zmPniVzBKv0ie/BEXBR6Isi08hYSd6lS18b8VduG5BzZ2tv6bO/rlwISg+IpGY2XsqAFTXFdTC28YR+UPocnAw==} @@ -3027,8 +3312,12 @@ snapshots: '@babel/helper-string-parser@7.24.1': {} + '@babel/helper-string-parser@7.25.9': {} + '@babel/helper-validator-identifier@7.24.5': {} + '@babel/helper-validator-identifier@7.25.9': {} + '@babel/highlight@7.24.5': dependencies: '@babel/helper-validator-identifier': 7.24.5 @@ -3040,6 +3329,10 @@ snapshots: dependencies: '@babel/types': 7.24.5 + '@babel/parser@7.26.3': + dependencies: + '@babel/types': 7.26.3 + '@babel/runtime@7.24.5': dependencies: regenerator-runtime: 0.14.1 @@ -3050,6 +3343,11 @@ snapshots: '@babel/helper-validator-identifier': 7.24.5 to-fast-properties: 2.0.0 + '@babel/types@7.26.3': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@d-fischer/cache-decorators@4.0.1': dependencies: '@d-fischer/shared-utils': 3.6.3 @@ -3085,23 +3383,23 @@ snapshots: dependencies: tslib: 2.6.2 - '@discordjs/builders@1.8.1': + '@discordjs/builders@1.10.0': dependencies: - '@discordjs/formatters': 0.4.0 - '@discordjs/util': 1.1.0 - '@sapphire/shapeshift': 3.9.7 - discord-api-types: 0.37.83 + '@discordjs/formatters': 0.6.0 + '@discordjs/util': 1.1.1 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.37.114 fast-deep-equal: 3.1.3 ts-mixer: 6.0.4 - tslib: 2.6.2 + tslib: 2.8.1 '@discordjs/collection@1.5.3': {} - '@discordjs/collection@2.1.0': {} + '@discordjs/collection@2.1.1': {} - '@discordjs/formatters@0.4.0': + '@discordjs/formatters@0.6.0': dependencies: - discord-api-types: 0.37.83 + discord-api-types: 0.37.114 '@discordjs/node-pre-gyp@0.4.5': dependencies: @@ -3126,19 +3424,19 @@ snapshots: - encoding - supports-color - '@discordjs/rest@2.3.0': + '@discordjs/rest@2.4.1': dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.2 + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.5 '@sapphire/snowflake': 3.5.3 - '@vladfrangu/async_event_emitter': 2.2.4 - discord-api-types: 0.37.83 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.37.114 magic-bytes.js: 1.10.0 - tslib: 2.6.2 - undici: 6.13.0 + tslib: 2.8.1 + undici: 6.19.8 - '@discordjs/util@1.1.0': {} + '@discordjs/util@1.1.1': {} '@discordjs/voice@0.17.0(@discordjs/opus@0.9.0)': dependencies: @@ -3155,21 +3453,44 @@ snapshots: - opusscript - utf-8-validate - '@discordjs/ws@1.1.0': + '@discordjs/ws@1.2.0': dependencies: - '@discordjs/collection': 2.1.0 - '@discordjs/rest': 2.3.0 - '@discordjs/util': 1.1.0 - '@sapphire/async-queue': 1.5.2 + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.4.1 + '@discordjs/util': 1.1.1 + '@sapphire/async-queue': 1.5.5 '@types/ws': 8.5.10 - '@vladfrangu/async_event_emitter': 2.2.4 - discord-api-types: 0.37.83 - tslib: 2.6.2 + '@vladfrangu/async_event_emitter': 2.4.6 + discord-api-types: 0.37.114 + tslib: 2.8.1 ws: 8.17.0 transitivePeerDependencies: - bufferutil - utf-8-validate + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.0': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + '@emnapi/runtime@1.2.0': dependencies: tslib: 2.6.2 @@ -3457,6 +3778,8 @@ snapshots: '@img/sharp-win32-x64@0.33.4': optional: true + '@johnsoncodehk/pug-beautify@0.2.2': {} + '@jridgewell/sourcemap-codec@1.4.15': {} '@msgpack/msgpack@2.8.0': {} @@ -3531,9 +3854,9 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.17.2': optional: true - '@sapphire/async-queue@1.5.2': {} + '@sapphire/async-queue@1.5.5': {} - '@sapphire/shapeshift@3.9.7': + '@sapphire/shapeshift@4.0.0': dependencies: fast-deep-equal: 3.1.3 lodash: 4.17.21 @@ -3745,21 +4068,69 @@ snapshots: vite: 5.2.11(@types/node@22.5.5) vue: 3.4.27(typescript@5.4.5) - '@vladfrangu/async_event_emitter@2.2.4': {} + '@vladfrangu/async_event_emitter@2.4.6': {} '@volar/language-core@2.2.1': dependencies: '@volar/source-map': 2.2.1 + '@volar/language-core@2.4.11': + dependencies: + '@volar/source-map': 2.4.11 + + '@volar/language-server@2.4.11': + dependencies: + '@volar/language-core': 2.4.11 + '@volar/language-service': 2.4.11 + '@volar/typescript': 2.4.11 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + '@volar/language-service@2.4.11': + dependencies: + '@volar/language-core': 2.4.11 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + '@volar/source-map@2.2.1': dependencies: muggle-string: 0.4.1 + '@volar/source-map@2.4.11': {} + + '@volar/test-utils@2.4.11': + dependencies: + '@volar/language-core': 2.4.11 + '@volar/language-server': 2.4.11 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + '@volar/typescript@2.2.1': dependencies: '@volar/language-core': 2.2.1 path-browserify: 1.0.1 + '@volar/typescript@2.4.11': + dependencies: + '@volar/language-core': 2.4.11 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vscode/emmet-helper@2.11.0': + dependencies: + emmet: 2.4.11 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + '@vscode/l10n@0.0.18': {} + '@vue/compiler-core@3.4.27': dependencies: '@babel/parser': 7.24.5 @@ -3768,11 +4139,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.0 + '@vue/compiler-core@3.5.13': + dependencies: + '@babel/parser': 7.26.3 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.0 + '@vue/compiler-dom@3.4.27': dependencies: '@vue/compiler-core': 3.4.27 '@vue/shared': 3.4.27 + '@vue/compiler-dom@3.5.13': + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + '@vue/compiler-sfc@3.4.27': dependencies: '@babel/parser': 7.24.5 @@ -3790,6 +4174,11 @@ snapshots: '@vue/compiler-dom': 3.4.27 '@vue/shared': 3.4.27 + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + '@vue/devtools-api@6.6.1': {} '@vue/language-core@2.0.16(typescript@5.4.5)': @@ -3804,6 +4193,57 @@ snapshots: optionalDependencies: typescript: 5.4.5 + '@vue/language-core@2.1.10(typescript@5.4.5)': + dependencies: + '@volar/language-core': 2.4.11 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.13 + alien-signals: 0.2.2 + minimatch: 9.0.4 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.4.5 + + '@vue/language-server@2.1.10(typescript@5.4.5)': + dependencies: + '@volar/language-core': 2.4.11 + '@volar/language-server': 2.4.11 + '@volar/test-utils': 2.4.11 + '@vue/language-core': 2.1.10(typescript@5.4.5) + '@vue/language-service': 2.1.10(typescript@5.4.5) + '@vue/typescript-plugin': 2.1.10(typescript@5.4.5) + vscode-languageserver-protocol: 3.17.5 + vscode-uri: 3.0.8 + transitivePeerDependencies: + - typescript + + '@vue/language-service@2.1.10(typescript@5.4.5)': + dependencies: + '@volar/language-core': 2.4.11 + '@volar/language-service': 2.4.11 + '@volar/typescript': 2.4.11 + '@vue/compiler-dom': 3.5.13 + '@vue/language-core': 2.1.10(typescript@5.4.5) + '@vue/shared': 3.5.13 + '@vue/typescript-plugin': 2.1.10(typescript@5.4.5) + alien-signals: 0.2.2 + path-browserify: 1.0.1 + volar-service-css: 0.0.62(@volar/language-service@2.4.11) + volar-service-emmet: 0.0.62(@volar/language-service@2.4.11) + volar-service-html: 0.0.62(@volar/language-service@2.4.11) + volar-service-json: 0.0.62(@volar/language-service@2.4.11) + volar-service-pug: 0.0.62 + volar-service-pug-beautify: 0.0.62(@volar/language-service@2.4.11) + volar-service-typescript: 0.0.62(@volar/language-service@2.4.11) + volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.11) + vscode-html-languageservice: 5.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + transitivePeerDependencies: + - typescript + '@vue/reactivity@3.4.27': dependencies: '@vue/shared': 3.4.27 @@ -3827,8 +4267,18 @@ snapshots: '@vue/shared@3.4.27': {} + '@vue/shared@3.5.13': {} + '@vue/tsconfig@0.5.1': {} + '@vue/typescript-plugin@2.1.10(typescript@5.4.5)': + dependencies: + '@volar/typescript': 2.4.11 + '@vue/language-core': 2.1.10(typescript@5.4.5) + '@vue/shared': 3.5.13 + transitivePeerDependencies: + - typescript + '@vueuse/core@10.9.0(vue@3.4.27(typescript@5.4.5))': dependencies: '@types/web-bluetooth': 0.0.20 @@ -3879,6 +4329,8 @@ snapshots: dependencies: acorn: 8.11.3 + acorn@7.4.1: {} + acorn@8.11.3: {} agent-base@6.0.2: @@ -3909,6 +4361,8 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + alien-signals@0.2.2: {} + ansi-regex@5.0.1: {} ansi-styles@3.2.1: @@ -4010,6 +4464,11 @@ snapshots: bytes@3.1.2: {} + call-bind-apply-helpers@1.0.1: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.7: dependencies: es-define-property: 1.0.0 @@ -4018,6 +4477,11 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.1 + get-intrinsic: 1.2.6 + callsites@3.1.0: {} camelcase-keys@7.0.2: @@ -4044,6 +4508,10 @@ snapshots: chalk@5.3.0: {} + character-parser@2.2.0: + dependencies: + is-regex: 1.2.1 + cheminfo-types@1.7.3: {} chevrotain@6.5.0: @@ -4208,22 +4676,24 @@ snapshots: dependencies: path-type: 4.0.0 + discord-api-types@0.37.114: {} + discord-api-types@0.37.83: {} - discord.js@14.15.2: + discord.js@14.17.0: dependencies: - '@discordjs/builders': 1.8.1 + '@discordjs/builders': 1.10.0 '@discordjs/collection': 1.5.3 - '@discordjs/formatters': 0.4.0 - '@discordjs/rest': 2.3.0 - '@discordjs/util': 1.1.0 - '@discordjs/ws': 1.1.0 + '@discordjs/formatters': 0.6.0 + '@discordjs/rest': 2.4.1 + '@discordjs/util': 1.1.1 + '@discordjs/ws': 1.2.0 '@sapphire/snowflake': 3.5.3 - discord-api-types: 0.37.83 + discord-api-types: 0.37.114 fast-deep-equal: 3.1.3 lodash.snakecase: 4.1.1 - tslib: 2.6.2 - undici: 6.13.0 + tslib: 2.8.1 + undici: 6.19.8 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -4254,10 +4724,21 @@ snapshots: dotenv@16.4.5: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + ee-first@1.1.1: {} efrt-unpack@2.2.0: {} + emmet@2.4.11: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + emoji-regex@8.0.0: {} encodeurl@1.0.2: {} @@ -4303,8 +4784,14 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + esbuild@0.20.2: optionalDependencies: '@esbuild/aix-ppc64': 0.20.2 @@ -4599,6 +5086,19 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-intrinsic@1.2.6: + dependencies: + call-bind-apply-helpers: 1.0.1 + dunder-proto: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + function-bind: 1.1.2 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.0.0 + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -4647,6 +5147,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} graphemer@1.4.0: {} @@ -4679,6 +5181,12 @@ snapshots: has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.0.3 + has-unicode@2.0.1: {} hasown@2.0.2: @@ -4805,6 +5313,11 @@ snapshots: dependencies: hasown: 2.0.2 + is-expression@4.0.0: + dependencies: + acorn: 7.4.1 + object-assign: 4.1.1 + is-extglob@2.1.1: {} is-finite@1.1.0: {} @@ -4827,6 +5340,13 @@ snapshots: is-plain-obj@1.1.0: {} + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + is-unicode-supported@0.1.0: {} isexe@2.0.0: {} @@ -4857,6 +5377,10 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonc-parser@2.3.1: {} + + jsonc-parser@3.3.1: {} + junk@4.0.1: {} keyv@4.5.4: @@ -4919,6 +5443,8 @@ snapshots: marked@12.0.2: {} + math-intrinsics@1.0.0: {} + media-typer@0.3.0: {} median-quickselect@1.0.1: {} @@ -5348,6 +5874,19 @@ snapshots: pstree.remy@1.1.8: {} + pug-error@2.1.0: {} + + pug-lexer@5.0.1: + dependencies: + character-parser: 2.2.0 + is-expression: 4.0.0 + pug-error: 2.1.0 + + pug-parser@6.0.0: + dependencies: + pug-error: 2.1.0 + token-stream: 1.0.0 + punycode@2.3.1: {} qs@6.11.0: @@ -5400,6 +5939,8 @@ snapshots: regexp-to-ast@0.4.0: optional: true + request-light@0.7.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -5477,6 +6018,8 @@ snapshots: semver@7.6.1: {} + semver@7.6.3: {} + send@0.18.0: dependencies: debug: 2.6.9 @@ -5710,6 +6253,8 @@ snapshots: toidentifier@1.0.1: {} + token-stream@1.0.0: {} + touch@3.1.0: dependencies: nopt: 1.0.10 @@ -5726,6 +6271,8 @@ snapshots: tslib@2.6.2: {} + tslib@2.8.1: {} + tsx@4.19.1: dependencies: esbuild: 0.23.1 @@ -5752,6 +6299,10 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + typescript-auto-import-cache@0.3.5: + dependencies: + semver: 7.6.3 + typescript-eslint@7.8.0(eslint@8.57.0)(typescript@5.4.5): dependencies: '@typescript-eslint/eslint-plugin': 7.8.0(@typescript-eslint/parser@7.8.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) @@ -5769,7 +6320,7 @@ snapshots: undici-types@6.19.8: {} - undici@6.13.0: {} + undici@6.19.8: {} unhead@1.9.10: dependencies: @@ -5785,7 +6336,7 @@ snapshots: unpipe@1.0.0: {} - unplugin-vue-components@0.26.0(@babel/parser@7.24.5)(rollup@4.17.2)(vue@3.4.27(typescript@5.4.5)): + unplugin-vue-components@0.26.0(@babel/parser@7.26.3)(rollup@4.17.2)(vue@3.4.27(typescript@5.4.5)): dependencies: '@antfu/utils': 0.7.7 '@rollup/pluginutils': 5.1.0(rollup@4.17.2) @@ -5799,7 +6350,7 @@ snapshots: unplugin: 1.10.1 vue: 3.4.27(typescript@5.4.5) optionalDependencies: - '@babel/parser': 7.24.5 + '@babel/parser': 7.26.3 transitivePeerDependencies: - rollup - supports-color @@ -5835,6 +6386,112 @@ snapshots: '@types/node': 22.5.5 fsevents: 2.3.3 + volar-service-css@0.0.62(@volar/language-service@2.4.11): + dependencies: + vscode-css-languageservice: 6.3.2 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.11 + + volar-service-emmet@0.0.62(@volar/language-service@2.4.11): + dependencies: + '@emmetio/css-parser': 0.4.0 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.11.0 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.11 + + volar-service-html@0.0.62(@volar/language-service@2.4.11): + dependencies: + vscode-html-languageservice: 5.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.11 + + volar-service-json@0.0.62(@volar/language-service@2.4.11): + dependencies: + vscode-json-languageservice: 5.4.2 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.11 + + volar-service-pug-beautify@0.0.62(@volar/language-service@2.4.11): + dependencies: + '@johnsoncodehk/pug-beautify': 0.2.2 + optionalDependencies: + '@volar/language-service': 2.4.11 + + volar-service-pug@0.0.62: + dependencies: + '@volar/language-service': 2.4.11 + muggle-string: 0.4.1 + pug-lexer: 5.0.1 + pug-parser: 6.0.0 + volar-service-html: 0.0.62(@volar/language-service@2.4.11) + vscode-html-languageservice: 5.3.1 + vscode-languageserver-textdocument: 1.0.12 + + volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.11): + dependencies: + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.11 + + volar-service-typescript@0.0.62(@volar/language-service@2.4.11): + dependencies: + path-browserify: 1.0.1 + semver: 7.6.3 + typescript-auto-import-cache: 0.3.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.11 + + vscode-css-languageservice@6.3.2: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-html-languageservice@5.3.1: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-json-languageservice@5.4.2: + dependencies: + '@vscode/l10n': 0.0.18 + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.0.8: {} + vue-confetti@2.3.0: {} vue-demi@0.14.7(vue@3.4.27(typescript@5.4.5)): diff --git a/server/package.json b/server/package.json index 8b2643a4..7be4b39c 100644 --- a/server/package.json +++ b/server/package.json @@ -19,7 +19,7 @@ "chalk": "^5.3.0", "cors": "^2.8.5", "discord-api-types": "^0.37.79", - "discord.js": "^14.15.2", + "discord.js": "14.17.0", "dotenv": "^16.4.5", "express": "^4.19.2", "libsodium-wrappers": "^0.7.10", diff --git a/server/src/action-utils/action-manager-models.js b/server/src/action-utils/action-manager-models.js index 04b3ec0a..b80bf025 100644 --- a/server/src/action-utils/action-manager-models.js +++ b/server/src/action-utils/action-manager-models.js @@ -33,6 +33,7 @@ export class Action { * @param {string[]} auth * @param {string[]} requiredParams * @param {string[]} optionalParams + * @param {function} registerFunction */ constructor({ key, @@ -75,7 +76,7 @@ export class Action { get: (...args) => Cache.get(...args), createRecord: (tableName, data) => createRecord(Cache, tableName, [data]), createRecords: (tableName, items) => createRecord(Cache, tableName, items), - updateRecord: (tableName, item, data, source) => updateRecord(Cache, tableName, item, data, source), + updateRecord: (tableName, item, data, source) => updateRecord(Cache, tableName, item, data, source || `actions/${this.key}`), auth: Cache.auth, permissions }; diff --git a/server/src/action-utils/action-utils.js b/server/src/action-utils/action-utils.js index cedc501b..306027d2 100644 --- a/server/src/action-utils/action-utils.js +++ b/server/src/action-utils/action-utils.js @@ -4,6 +4,7 @@ import { StaticAuthProvider } from "@twurple/auth"; import { ApiClient } from "@twurple/api"; import { verboseLog } from "../discord/slmngg-log.js"; import { get } from "./action-cache.js"; +import client from "../discord/client.js"; const airtable = new Airtable({ apiKey: process.env.AIRTABLE_KEY }); const slmngg = airtable.base(process.env.AIRTABLE_APP); @@ -51,14 +52,14 @@ const TimeOffset = 3 * 1000; */ export async function updateRecord(Cache, tableName, item, data, source = undefined) { // see: airtable-interface.js customUpdater - console.log(`[update record] updating table=${tableName} id=${item.id}`, data); + console.log(`[update record] ${source ? `{${source}} ` : ""}updating table=${tableName} id=${item.id}`, data); let slmnggData = { __tableName: tableName, ...deAirtable({ ...item, ...data }), modified: (new Date((new Date()).getTime() + TimeOffset)).toString() }; - verboseLog(`Editing record on **${tableName}** \`${item.id}\``, data); + verboseLog(`Editing record on **${tableName}** \`${item.id}\`${source ? ` {${source}}` : ""}`, data); // Eager update Cache.set(cleanID(item.id), slmnggData, { eager: true, source }); @@ -81,12 +82,13 @@ export async function updateRecord(Cache, tableName, item, data, source = undefi * @param {Cache} Cache * @param {string} tableName * @param {object[]} records + * @param {string | null} source */ -export async function createRecord(Cache, tableName, records) { - console.log(`[create record] creating table=${tableName} records=${records.length}`); +export async function createRecord(Cache, tableName, records, source = null) { + console.log(`[create record] ${source ? `{${source}} ` : ""}creating table=${tableName} records=${records.length}`); try { let newRecords = await slmngg(tableName).create(records.map(recordData => { - verboseLog(`Creating record on **${tableName}** `, recordData); + verboseLog(`Creating record on **${tableName}**${source ? ` {${source}}` : ""}`, recordData); return { fields: recordData }; @@ -193,6 +195,7 @@ export async function getMatchData(broadcast, requireAll) { * @returns {Promise<({report: Report | undefined, match: Match})>} */ export async function getMatchScoreReporting(matchID) { + /** @type {Match} */ const match = await get(matchID); let report; @@ -211,8 +214,45 @@ export async function getMatchScoreReporting(matchID) { if (!eventSettings?.reporting?.score?.use) throw "Score reporting is not enabled on this match"; // check existing report - if (match?.reports?.[0]) { - const firstReport = await get(match?.reports?.[0]); + if ((match?.reports || []).length) { + const reports = await Promise.all((match?.reports || []).map(rID => get(rID))); + const firstReport = reports.find(r => r.type === "Scores"); + if (firstReport?.id) { + report = firstReport; + } + } + + return { match, report }; +} +/** + * + * @param matchID + * @param { {excludeCompleted: Boolean } } settings + * @returns {Promise<({report: Report | undefined, match: Match})>} + */ +export async function getMatchRescheduling(matchID, { excludeCompleted } = {}) { + const match = await get(matchID); + let report; + + if (!match?.id) throw "Couldn't load match data"; + + if (!match?.event?.[0]) throw "Couldn't load event data for this match"; + const event = await get(match?.event?.[0]); + if (!event?.id) throw "Couldn't load event data for this match"; + + // event score reporting must be active + + if (!event?.blocks) throw "Event doesn't have rescheduling set up"; + + /** @type {EventSettings} */ + const eventSettings = JSON.parse(event.blocks); + if (!eventSettings?.reporting?.rescheduling?.use) throw "Rescheduling is not enabled on this event"; + if (!(match?.earliest_start || match?.latest_start)) throw "Rescheduling is not set up on this match"; + + // check existing report + if (match?.reports?.length) { + const reports = await Promise.all((match?.reports || []).map(rID => get(rID))); + const firstReport = reports.find(r => r.type === "Rescheduling" && (excludeCompleted ? !r.approved : true)); if (firstReport?.id) { report = firstReport; } @@ -225,6 +265,7 @@ export async function getTwitchAPIClient(channel) { if (!channel) throw("Internal error connecting to Twitch"); try { const accessToken = await Cache.auth.getTwitchAccessToken(channel); + // this warning is because the twitch auth data is thrown into the general auth map but it shouldn't actually cause an error here const authProvider = new StaticAuthProvider(process.env.TWITCH_CLIENT_ID, accessToken); return new ApiClient({authProvider}); } catch (e) { @@ -319,3 +360,62 @@ export async function findMember(player, team, guild) { } return { member, fixes }; } + +export async function checkDeleteMessage(mapObject, keyPrefix) { + if (mapObject.get(`${keyPrefix}_message_id`) && mapObject.get(`${keyPrefix}_channel_id`)) { + try { + const channel = await client.channels.fetch(mapObject.get(`${keyPrefix}_channel_id`)); + if (channel?.isSendable()) await channel.messages.delete(mapObject.get(`${keyPrefix}_message_id`)); + } catch (e) { + console.error(`Error trying to delete ${keyPrefix} message`, e); + } finally { + mapObject.push(`${keyPrefix}_message_id`, null); + mapObject.push(`${keyPrefix}_channel_id`, null); + } + } +} + +/** + * @param {object} options + * @param {string} options.key + * @param {MapObject} options.mapObject + * @param {(Snowflake | null)=} options.channelID + * @param {(string | null | object)=} options.content + * @param {Function=} options.success + * @returns {Promise} + * @deprecated Use keyed sendRecordedMessage instead + */ +export async function sendMessage({ + key, + mapObject, + channelID, + content, + success, +}) { + if (!channelID) { + console.warn(`Can't send ${key} message without a channel`); + return mapObject; + } + if (!content) { + console.warn(`Can't send ${key} message without content`); + return mapObject; + } + const channel = await client.channels.fetch(channelID); + if (channel?.isSendable()) { + try { + const message = await channel.send(content); + mapObject.push(`${key}_channel_id`, channel.id); + mapObject.push(`${key}_message_id`, message.id); + if (success) await success(mapObject); + } catch (e) { + console.error(`Sending error ${key}`, e); + console.dir(e?.rawError?.errors, { depth: null, colors: true }); + } + } + return mapObject; +} + +export function hammerTime(timeString) { + let start = new Date(timeString).getTime(); + return ``; +} diff --git a/server/src/action-utils/ts-action-utils.ts b/server/src/action-utils/ts-action-utils.ts index 41f4c54a..2f549192 100644 --- a/server/src/action-utils/ts-action-utils.ts +++ b/server/src/action-utils/ts-action-utils.ts @@ -1,9 +1,10 @@ +import type { Snowflake } from "discord-api-types/globals"; import { Match } from "../types.js"; import { get } from "./action-cache.js"; import { MapObject } from "../discord/managers.js"; import client from "../discord/client.js"; -import { Guild } from "discord.js"; -import { cleanID } from "./action-utils.js"; +import { ChannelType, Guild, MessageCreateOptions, MessagePayload } from "discord.js"; +import { cleanID, sendMessage } from "./action-utils.js"; export async function generateMatchReportText(match: Match) { @@ -126,9 +127,19 @@ export async function generateMatchReportText(match: Match) { mapLine.push(map.replay_code); } - lines.push(mapLine.join(" - ")); + if (map.team_1_picks?.length || map.team_2_picks?.length) { + const teamPicks = []; + + for (const teamI of [0, 1]) { + const team = teams[teamI]; + const bannedHeroes = await Promise.all(([map.team_1_picks, map.team_2_picks][teamI] || []).map(id => get(id))); + teamPicks.push(`${team.code || team.name} ban: ${bannedHeroes.map(hero => hero.icon_emoji_text || hero.name).join(" ")}`); + } + + lines.push(`Picks: ${teamPicks.join(" / ")}`); + } if (map.team_1_bans?.length || map.team_2_bans?.length) { const teamBans = []; @@ -138,8 +149,16 @@ export async function generateMatchReportText(match: Match) { teamBans.push(`${team.code || team.name} ban: ${bannedHeroes.map(hero => hero.icon_emoji_text || hero.name).join(" ")}`); } - lines.push(`> ${teamBans.join(" | ")}`); + const banCount = (map.team_1_bans?.length || 0) + (map.team_2_bans?.length || 0); + // if there are only 2 bans and no picks, add to the current line + if (banCount <= 2 && !map.team_1_picks?.length && !map.team_2_picks?.length) { + lines[lines.length - 1] += ` - Bans: ${teamBans.join(" / ")}`; + } else { + lines.push(`Bans: ${teamBans.join(" / ")}`); + } + } + } @@ -150,3 +169,48 @@ export async function generateMatchReportText(match: Match) { return null; } } + +export async function checkDeleteMessage(mapObject: MapObject, keyPrefix: KeyType) { + if (mapObject.get(`${keyPrefix}_message_id`) && mapObject.get(`${keyPrefix}_channel_id`)) { + try { + const channel = await client.channels.fetch(mapObject.get(`${keyPrefix}_channel_id`)); + if (channel) { + console.log(`${keyPrefix} - ${channel.id} ${channel.type !== ChannelType.DM ? channel.name : ""}`); + } else { + console.warn(`${keyPrefix} - No channel`); + } + if (channel?.isSendable()) await channel.messages.delete(mapObject.get(`${keyPrefix}_message_id`)); + } catch (e) { + console.error(`Error trying to delete ${keyPrefix} message`, e); + } finally { + mapObject.push(`${keyPrefix}_message_id`, null); + mapObject.push(`${keyPrefix}_channel_id`, null); + } + } +} + +export async function looseDeleteRecordedMessage(mapObject: MapObject, keyPrefix: KeyType) { + console.log("Loose delete", mapObject.data, keyPrefix); + await checkDeleteMessage(mapObject, keyPrefix); + return mapObject; +} +export async function looseDeleteRecordedMessages(mapObject: MapObject, keyPrefixes: KeyType[]) { + console.log("Loose delete multiple", mapObject.data, keyPrefixes); + if (!mapObject?.data) return mapObject; + console.log(mapObject.data); + await Promise.all(keyPrefixes.map(async keyPrefix => checkDeleteMessage(mapObject, keyPrefix))); + return mapObject; +} + +export async function sendRecordedMessage({ key, mapObject, channelID, content, success } : + { + key: KeyType; + mapObject: MapObject; + channelID?: Snowflake; + content?: null | string | MessagePayload | MessageCreateOptions; + success?: (updatedMapObject: MapObject) => void; + } +): Promise { + console.log("Recorded message", key, mapObject.data); + return sendMessage({ key, mapObject, channelID, content, success }); +} diff --git a/server/src/actions/approve-match-reschedule.ts b/server/src/actions/approve-match-reschedule.ts new file mode 100644 index 00000000..a5313478 --- /dev/null +++ b/server/src/actions/approve-match-reschedule.ts @@ -0,0 +1,141 @@ +import { ActionAuth, Match, MatchResolvableID, Report, ReschedulingReportKeys } from "../types.js"; +import { cleanID, dirtyID, getMatchRescheduling, } from "../action-utils/action-utils.js"; +import { Action } from "../action-utils/action-manager-models.js"; +import { get } from "../action-utils/action-cache.js"; +import { MapObject } from "../discord/managers.js"; +import { looseDeleteRecordedMessage, looseDeleteRecordedMessages } from "../action-utils/ts-action-utils.js"; + +export default { + key: "approve-match-reschedule", + requiredParams: ["matchID", "reaction"], + auth: ["user"], + async handler( + {matchID, reaction}: { matchID: MatchResolvableID, reaction: "approve" | "deny" | "delete" }, + {user}: ActionAuth + ) { + const {match, report}: { match: Match, report: Report | undefined } = await getMatchRescheduling(matchID); + if (!report) { + throw "There is no reschedule request on this match"; + } + + const teams = await Promise.all((match.teams || []).map(t => get(t))); + + const reportableTeams = teams.filter(t => { + // if (reaction === "counter-approve" || reaction === "counter-deny") { + // // original team + // return cleanID(t.id) === cleanID(report.team?.[0]); + // } + return cleanID(t.id) !== cleanID(report.team?.[0]); + }); + + const actingTeam = reportableTeams.find(team => [ + ...(team.players || []), + ...(team.captains || []), + ...(team.staff || []), + ...(team.owners || []), + ].includes(dirtyID(user.airtable.id))); + + const isRelated = teams.some(team => [ + ...(team.players || []), + ...(team.captains || []), + ...(team.staff || []), + ...(team.owners || []), + ].includes(dirtyID(user.airtable.id))); + + if (!isRelated) throw "You don't have permission to use rescheduling for this match"; + + if (reaction === "approve") { + if (!actingTeam) throw "You don't have permission to approve this request"; + if (report.approved) throw "This request has already been approved"; + if (report.denied_by_opponent) throw "This request has already been denied"; + + let messageData = new MapObject(report.message_data); + + messageData = await looseDeleteRecordedMessage(messageData, "reschedule_opponent_approval"); + + const response = await this.helpers.updateRecord("Reports", report, { + "Approved by opponent": true, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + "text=Approved match reschedule", + "key=approved_by_opponent" + ].join("|"), + "Message Data": messageData.textMap + }); + if ("error" in response) { + console.error("Airtable error", response.error); + throw "System error"; + } + return "Match reschedule approved"; + + } else if (reaction === "deny") { + if (!actingTeam) throw "You don't have permission to approve this request"; + if (report.approved) throw "This request has already been approved"; + if (report.denied_by_opponent) throw "This request has already been denied"; + + let messageData = new MapObject(report.message_data); + // approval messages that need to be deleted + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_opponent_approval", "reschedule_staff_approval", "reschedule_staff_preapproval"]); + + const response = await this.helpers.updateRecord("Reports", report, { + "Denied by opponent": true, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + "text=Denied match reschedule", + "key=denied_by_opponent" + ].join("|"), + "Message Data": messageData.textMap + }); + if ("error" in response) { + console.error("Airtable error", response.error); + throw "System error"; + } + + return "Match reschedule denied"; + + } else if (reaction === "delete") { + // any messages from a previous approval that was denied + + if ((report.approved || report.denied_by_opponent) && !report.denied_by_staff) { + // Finished report or opponent denial can start a fresh one. Staff deny locks it + + let messageData = new MapObject(report.message_data); + // any messages from a previous approval that was denied + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_opponent_denial", "reschedule_staff_denial", "reschedule_team_cancel", "reschedule_opponent_cancel"]); + + const response = await this.helpers.updateRecord("Reports", report, { + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + "text=Deleted match reschedule", + "key=deleted" + ].join("|"), + "Message Data": messageData.textMap + }); + if ("error" in response) { + console.error("Airtable error", response.error); + throw "System error"; + } + const matchClearResponse = await this.helpers.updateRecord("Matches", match, { + "Reports": [], + "Report History": [...(match.report_history || []), report.id].map(x => dirtyID(x)) + }); + if ("error" in matchClearResponse) { + console.error("Airtable error", matchClearResponse.error); + throw "System error"; + } + + return "Match reschedule deleted"; + + } else { + throw "You don't have permission to start a new request on this match"; + } + } + } +// @ts-expect-error Needs some action refactoring before it can fully satisfy +} satisfies Action; diff --git a/server/src/actions/approve-score-report.ts b/server/src/actions/approve-score-report.ts index 5547a324..3b8968bc 100644 --- a/server/src/actions/approve-score-report.ts +++ b/server/src/actions/approve-score-report.ts @@ -1,9 +1,9 @@ -import { ActionAuth, Match, MatchResolvableID, Report } from "../types.js"; +import { ActionAuth, Match, MatchResolvableID, Report, ScoreReportingReportKeys } from "../types.js"; import { Action } from "../action-utils/action-manager-models.js"; import { cleanID, dirtyID, getMatchScoreReporting } from "../action-utils/action-utils.js"; import { get } from "../action-utils/action-cache.js"; -import client from "../discord/client.js"; import { MapObject } from "../discord/managers.js"; +import { looseDeleteRecordedMessage } from "../action-utils/ts-action-utils.js"; export default { key: "approve-score-report", @@ -48,25 +48,20 @@ export default { if (reaction === "approve") { // opponent approves original's report - - const messageData = new MapObject(report.message_data); + let messageData = new MapObject(report.message_data); // Remove previous captain notification console.log(messageData.data); - if (messageData.get("opponent_captain_notification_message_id") && messageData.get("opponent_captain_notification_channel_id")) { - try { - const channel = await client.channels.fetch(messageData.get("opponent_captain_notification_channel_id")); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get("opponent_captain_notification_message_id")); - } catch (e) { - console.error("Error trying to delete previous opponent captain notification message", e); - } finally { - messageData.push("opponent_captain_notification_message_id", null); - messageData.push("opponent_captain_notification_channel_id", null); - } - } + messageData = await looseDeleteRecordedMessage(messageData, "report_opponent_notification"); await this.helpers.updateRecord("Reports", report, { "Approved by opponent": true, - "Log": (report.log ? report.log + "\n" : "") + `${(new Date()).toLocaleString()}: ${user.airtable.name} approved score report as ${actingTeam?.name}`, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + "text=Approved score report", + "key=approved_by_opponent" + ].join("|"), "Message Data": messageData.textMap }); @@ -74,21 +69,11 @@ export default { } else if (reaction === "counter-approve") { // original approves opponent's counter report - const messageData = new MapObject(report.message_data); + let messageData = new MapObject(report.message_data); // Remove previous captain notification console.log(messageData.data); - if (messageData.get("original_captain_notification_message_id") && messageData.get("original_captain_notification_channel_id")) { - try { - const channel = await client.channels.fetch(messageData.get("original_captain_notification_channel_id")); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get("original_captain_notification_message_id")); - } catch (e) { - console.error("Error trying to delete previous opponent captain notification message", e); - } finally { - messageData.push("original_captain_notification_message_id", null); - messageData.push("original_captain_notification_channel_id", null); - } - } + messageData = await looseDeleteRecordedMessage(messageData, "report_opponent_notification"); await this.helpers.updateRecord("Reports", report, { "Approved by opponent": true, @@ -96,7 +81,14 @@ export default { "Countered by opponent": false, "Data": report.countered_data, "Countered Data": "", - "Log": (report.log ? report.log + "\n" : "") + `${(new Date()).toLocaleString()}: ${user.airtable.name} approved counter report as ${actingTeam?.name}` + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + "text=Approved counter report", + "key=approved_counter_report" + ].join("|"), + // `${(new Date()).toLocaleString()}: ${user.airtable.name} approved counter report as ${actingTeam?.name}` }); } } diff --git a/server/src/actions/create-event-discord-items.js b/server/src/actions/create-event-discord-items.js index fab80778..35cfceed 100644 --- a/server/src/actions/create-event-discord-items.js +++ b/server/src/actions/create-event-discord-items.js @@ -41,7 +41,7 @@ export default { * @param {boolean?} settings.roles.pingable * @param {boolean?} settings.roles.hoist * @param {string?} settings.teamEmoji.format - * @param {UserData} user + * @param {ActionAuth["user"]} user * @returns {Promise} */ async handler({ eventID, guildID, actions, settings }, { user }) { diff --git a/server/src/actions/create-live-guest.js b/server/src/actions/create-live-guest.js index 2aeffecd..2f23ee93 100644 --- a/server/src/actions/create-live-guest.js +++ b/server/src/actions/create-live-guest.js @@ -1,11 +1,12 @@ import { deAirtable } from "../action-utils/action-utils.js"; + export default { key: "create-live-guest", requiredParams: [], auth: ["user"], /*** * @param {Object?} params - * @param {UserData} user + * @param {ActionAuth["user"]} user * @returns {Promise<{}>} */ async handler(params, { user }) { diff --git a/server/src/actions/manage-prediction.js b/server/src/actions/manage-prediction.js index 4325f1bb..bfa47c7a 100644 --- a/server/src/actions/manage-prediction.js +++ b/server/src/actions/manage-prediction.js @@ -46,7 +46,7 @@ export default { * @param {"create"|"lock"|"resolve"|"cancel"} predictionAction * @param {"match"|"map"} predictionType * @param {number?} autoLockAfter - * @param {ClientData} client + * @param {Client} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/match-discord-slmngg-player.js b/server/src/actions/match-discord-slmngg-player.js index aa0ac77e..48afc58e 100644 --- a/server/src/actions/match-discord-slmngg-player.js +++ b/server/src/actions/match-discord-slmngg-player.js @@ -7,7 +7,7 @@ export default { auth: ["user"], /*** * @param {import("discord.js").User} discordData - * @param {UserData} user + * @param {ActionAuth["user"]} user * @returns {Promise} */ async handler({ discordData }, { user }) { diff --git a/server/src/actions/send-match-discord-message.js b/server/src/actions/send-match-discord-message.js index 65cd9f24..09a76db2 100644 --- a/server/src/actions/send-match-discord-message.js +++ b/server/src/actions/send-match-discord-message.js @@ -7,8 +7,9 @@ export default { auth: ["client", "user"], /*** * @param {Object?} params - * @param {ClientData} client - * @param {CacheGetFunction} get + * @param {ActionAuth["user"]} user + * @param {ActionAuth["client"]} client + * @param {ActionAuth["isAutomation"]} isAutomation * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/set-active-broadcast.ts b/server/src/actions/set-active-broadcast.ts index 99ec01f3..3171d72b 100644 --- a/server/src/actions/set-active-broadcast.ts +++ b/server/src/actions/set-active-broadcast.ts @@ -9,8 +9,7 @@ export default { /*** * @param {DirtyAirtableID} broadcastID * @param {DirtyAirtableID} clientID - * @param {UserData} user - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ async handler( diff --git a/server/src/actions/set-event-guild.js b/server/src/actions/set-event-guild.js index 31e4c836..164971a8 100644 --- a/server/src/actions/set-event-guild.js +++ b/server/src/actions/set-event-guild.js @@ -8,7 +8,7 @@ export default { /*** * @param {Snowflake} guildID * @param {AnyAirtableID} eventID - * @param {UserData} user + * @param {ActionAuth["user"]} user * @returns {Promise} */ async handler({ eventID, guildID }, { user }) { diff --git a/server/src/actions/set-marker.js b/server/src/actions/set-marker.js index 76674a77..3fb8f7fc 100644 --- a/server/src/actions/set-marker.js +++ b/server/src/actions/set-marker.js @@ -7,8 +7,7 @@ export default { optionalParams: ["text"], /*** * @param {Object?} params - * @param {ClientData} client - * @param {CacheGetFunction} get + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/set-match-overlays.js b/server/src/actions/set-match-overlays.js index 5e0461ff..e979156b 100644 --- a/server/src/actions/set-match-overlays.js +++ b/server/src/actions/set-match-overlays.js @@ -4,7 +4,7 @@ export default { requiredParams: ["match", "overlayType", "state"], /*** * @param {Object?} params - * @param {ClientData} client + * @param {ActionAuth["user"]} user * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/set-observer-setting.js b/server/src/actions/set-observer-setting.js index c31b0af6..6d8eff20 100644 --- a/server/src/actions/set-observer-setting.js +++ b/server/src/actions/set-observer-setting.js @@ -4,7 +4,7 @@ export default { auth: ["client"], /*** * @param {Object?} params - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/set-player-cams.js b/server/src/actions/set-player-cams.js index 17a6383e..c49e1245 100644 --- a/server/src/actions/set-player-cams.js +++ b/server/src/actions/set-player-cams.js @@ -6,7 +6,7 @@ export default { requiredParams: ["cams"], /*** * @param {AnyAirtableID[][]} cams - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/set-player-relationships.js b/server/src/actions/set-player-relationships.js index 108cd1c8..a30cea5e 100644 --- a/server/src/actions/set-player-relationships.js +++ b/server/src/actions/set-player-relationships.js @@ -30,7 +30,7 @@ export default { * @typedef {"Team 1" | "Team 2" | "None"} Cams * @typedef {{ clientID: AnyAirtableID, cams: Cams[] }[]} CamsData * @param {Object?} params - * @param {ClientData} client + * @param {ActionAuth["user"]} user * @param {CamsData} clientCams * @returns {Promise} */ diff --git a/server/src/actions/set-player-remote-feed.js b/server/src/actions/set-player-remote-feed.js index cb247beb..f92b3ed0 100644 --- a/server/src/actions/set-player-remote-feed.js +++ b/server/src/actions/set-player-remote-feed.js @@ -5,6 +5,7 @@ export default { /*** * @param { ? } player * @param { string } feed + * @param {boolean} isAutomation */ async handler({ player, feed }, { isAutomation }) { if (!isAutomation) throw "This action can only be run internally."; diff --git a/server/src/actions/set-player-signup-data.js b/server/src/actions/set-player-signup-data.js index 39b3c25e..c14fb99b 100644 --- a/server/src/actions/set-player-signup-data.js +++ b/server/src/actions/set-player-signup-data.js @@ -18,7 +18,7 @@ export default { * @param {object[]} allPlayerData * @param {boolean} useSignupData * @param {boolean} createPlayers - * @param {UserData} user + * @param {ActionAuth["user"]} user * @returns {Promise<*[]>} */ async handler({ eventID, playerData: allPlayerData, useSignupData, createPlayers }, { user }) { @@ -118,6 +118,8 @@ export default { if (playerData?.discord_tag) airtablePlayerData["Discord Tag"] = playerData.discord_tag.replace("@", "").trim(); // if (playerData?.discord_id) airtablePlayerData["Discord ID"] = playerData.discord_id; if (playerData?.battletag) airtablePlayerData["Battletag"] = playerData.battletag; + if (playerData?.pronouns) airtablePlayerData["Pronouns"] = playerData.pronouns.toLowerCase().trim(); + if (playerData?.pronunciation) airtablePlayerData["Pronunciation"] = playerData.pronunciation; if (playerData.team_id) airtablePlayerData["Member Of"] = [dirtyID(playerData.team_id)]; @@ -159,7 +161,8 @@ export default { data: playerData?.eligible_roles?.split(", ")?.filter(Boolean), }, { - signupDataKey: "role", + playerDataKey: "role", + signupDataKey: "main_role", airtableKey: "Main Role", }, { @@ -182,9 +185,8 @@ export default { signupDataKey: "info_for_captains", airtableKey: "Info For Captains", } - ].forEach(({ signupDataKey, airtableKey, data }) => { - data = data || playerData?.[signupDataKey]; - // console.log(airtableKey, data, signupRecord?.[signupDataKey]); + ].forEach(({ signupDataKey, playerDataKey, airtableKey, data }) => { + data = data || playerData?.[playerDataKey || signupDataKey]; // if (!data) return; if (signupRecord) { @@ -302,11 +304,16 @@ export default { { signupDataKey: "discord_tag", airtableKey: "Discord Tag", }, // { signupDataKey: "discord_id", airtableKey: "Discord ID", }, { signupDataKey: "battletag", airtableKey: "Battletag", }, + { signupDataKey: "pronouns", airtableKey: "Pronouns", validation: (str) => str?.toLowerCase()?.trim() }, + { signupDataKey: "pronunciation", airtableKey: "Pronunciation", }, ]; - alwaysAllowedPlayerUpdate.forEach(({ signupDataKey, airtableKey, data }) => { + alwaysAllowedPlayerUpdate.forEach(({ signupDataKey, airtableKey, data, validation }) => { data = data || playerData?.[signupDataKey]; + if (validation) { + data = validation(data); + } // console.log(airtableKey, data, playerData?.[signupDataKey]); if (player[signupDataKey] === data) return; @@ -317,7 +324,6 @@ export default { if (Object.keys(playerUpdateData)?.length) { console.log(playerUpdateData); - console.log(player); await this.helpers.updateRecord("Players", player, playerUpdateData); } } diff --git a/server/src/actions/set-title.js b/server/src/actions/set-title.js index a8e798c2..bdcacbc2 100644 --- a/server/src/actions/set-title.js +++ b/server/src/actions/set-title.js @@ -6,8 +6,8 @@ export default { auth: ["client"], /*** * @param {Object?} params - * @param {ClientData} client - * @param {CacheGetFunction} get + * @param {ActionAuth["client"]} client + * @param {ActionAuth["isAutomation"]} isAutomation * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/staff-approve-match-reschedule.ts b/server/src/actions/staff-approve-match-reschedule.ts new file mode 100644 index 00000000..da740001 --- /dev/null +++ b/server/src/actions/staff-approve-match-reschedule.ts @@ -0,0 +1,122 @@ +import { ActionAuth, Match, MatchResolvableID, Report, ReschedulingReportKeys } from "../types.js"; +import { Action } from "../action-utils/action-manager-models.js"; +import { dirtyID, getMatchRescheduling } from "../action-utils/action-utils.js"; +import { get } from "../action-utils/action-cache.js"; +import { isEventStaffOrHasRole } from "../action-utils/action-permissions.js"; +import { MapObject } from "../discord/managers.js"; +import { looseDeleteRecordedMessages } from "../action-utils/ts-action-utils.js"; + +export default { + key: "staff-approve-match-reschedule", + requiredParams: ["matchID", "reaction"], + auth: ["user"], + async handler( + { matchID, reaction }: { matchID: MatchResolvableID, reaction: "approve" | "pre-approve" | "force-approve" | "deny" | "delete" }, + { user }: ActionAuth + ) { + const { match, report } : { match: Match, report: Report | undefined } = await getMatchRescheduling(matchID); + if (!report) { + throw "There is no match rescheduling request on this match"; + } + + if (!match?.event?.[0]) throw "Event couldn't be loaded for this match"; + const event = await get(match.event[0]); + if (!event?.id) throw "Event couldn't be loaded for this match"; + + if (!(await isEventStaffOrHasRole(user, event, null, ["Can edit any match", "Can edit any event", "Full broadcast permissions"]))) { + throw "You don't have permission to edit this match, including match rescheduling."; + } + + let messageData = new MapObject(report.message_data); + + // Remove previous notifications + // messageData = await looseDeleteRecordedMessage(messageData, "staff_reschedule_notification"); + + if (reaction === "pre-approve") { + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_staff_approval", "reschedule_staff_preapproval"]); + + await this.helpers.updateRecord("Reports", report, { + "Approved by staff": true, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Pre-approved match reschedule", + "key=staff_preapproved" + ].join("|"), + "Message Data": messageData.textMap + }); + } else if (reaction === "approve") { + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_staff_approval", "reschedule_staff_preapproval"]); + + await this.helpers.updateRecord("Reports", report, { + "Approved by staff": true, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Approved match reschedule", + "key=staff_approved" + ].join("|"), + "Message Data": messageData.textMap + }); + } else if (reaction === "force-approve") { + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_staff_approval", "reschedule_staff_preapproval", "reschedule_opponent_approval"]); + await this.helpers.updateRecord("Reports", report, { + "Approved by staff": true, + "Force approved": true, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Force-approved match reschedule", + "key=staff_force_approved" + ].join("|"), + "Message Data": messageData.textMap + }); + } else if (reaction === "deny") { + // approval messages that need to be deleted + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_opponent_approval", "reschedule_staff_approval", "reschedule_staff_preapproval"]); + + await this.helpers.updateRecord("Reports", report, { + "Denied by staff": true, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Denied match reschedule", + "key=staff_denied" + ].join("|"), + "Message Data": messageData.textMap + }); + } else if (reaction === "delete") { + // any messages from a previous approval that was denied + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_staff_denial", "reschedule_opponent_denial", "reschedule_team_cancel", "reschedule_opponent_cancel"]); + // approval messages that need to be deleted + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_opponent_approval", "reschedule_staff_approval", "reschedule_staff_preapproval"]); + + await this.helpers.updateRecord("Reports", report, { + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Deleted match reschedule", + "key=staff_deleted" + ].join("|"), + "Message Data": messageData.textMap + }); + await this.helpers.updateRecord("Matches", match, { + "Reports": [], + "Report History": [...(match.report_history || []), report.id].map(x => dirtyID(x)) + }); + } else { + throw { + errorCode: 501, + errorMessage: "Unknown method for staff approvals" + }; + } + + + } +// @ts-expect-error Needs some action refactoring before it can fully satisfy +} satisfies Action; diff --git a/server/src/actions/staff-approve-score-report.ts b/server/src/actions/staff-approve-score-report.ts index 92fbc8ce..cc38929d 100644 --- a/server/src/actions/staff-approve-score-report.ts +++ b/server/src/actions/staff-approve-score-report.ts @@ -1,10 +1,11 @@ -import { ActionAuth, EventSettings, Match, MatchResolvableID, Report } from "../types.js"; +import { ActionAuth, EventSettings, Match, MatchResolvableID, Report, ScoreReportingReportKeys } from "../types.js"; import { Action } from "../action-utils/action-manager-models.js"; -import { cleanID, getMatchScoreReporting } from "../action-utils/action-utils.js"; +import { cleanID, dirtyID, getMatchScoreReporting } from "../action-utils/action-utils.js"; import { get } from "../action-utils/action-cache.js"; import { isEventStaffOrHasRole } from "../action-utils/action-permissions.js"; import { MapObject } from "../discord/managers.js"; import client from "../discord/client.js"; +import { looseDeleteRecordedMessage, looseDeleteRecordedMessages } from "../action-utils/ts-action-utils.js"; export default { key: "staff-approve-score-report", @@ -24,55 +25,51 @@ export default { if (!event?.id) throw "Event couldn't be loaded for this match"; if (!(await isEventStaffOrHasRole(user, event, null, ["Can edit any match", "Can edit any event", "Full broadcast permissions"]))) { - throw "You don't have permission to edit this match"; + throw "You don't have permission to edit this match, including score reporting."; } - const messageData = new MapObject(report.message_data); + let messageData = new MapObject(report.message_data); - // Remove previous staff notifications console.log(messageData.data); - if (messageData.get("staff_notification_message_id") && messageData.get("staff_notification_channel_id")) { - try { - const channel = await client.channels.fetch(messageData.get("staff_notification_channel_id")); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get("staff_notification_message_id")); - } catch (e) { - console.error("Error trying to delete previous staff message", e); - } finally { - messageData.push("staff_notification_message_id", null); - messageData.push("staff_notification_channel_id", null); - } - } + // Remove previous staff notifications + messageData = await looseDeleteRecordedMessage(messageData, "report_staff_notification"); if (reaction === "pre-approve") { await this.helpers.updateRecord("Reports", report, { "Approved by staff": true, - "Log": (report.log ? report.log + "\n" : "") + `${(new Date()).toLocaleString()}: ${user.airtable.name} pre-approved score report as staff`, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Pre-approved score report", + "key=staff_preapproved" + ].join("|"), "Message Data": messageData.textMap }); } else if (reaction === "approve") { await this.helpers.updateRecord("Reports", report, { "Approved by staff": true, - "Log": (report.log ? report.log + "\n" : "") + `${(new Date()).toLocaleString()}: ${user.airtable.name} approved score report as staff`, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Approved score report", + "key=staff_approved" + ].join("|"), "Message Data": messageData.textMap }); } else if (reaction === "force-approve") { - - if (messageData.get("opponent_captain_notification_message_id") && messageData.get("opponent_captain_notification_channel_id")) { - try { - const channel = await client.channels.fetch(messageData.get("opponent_captain_notification_channel_id")); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get("opponent_captain_notification_message_id")); - } catch (e) { - console.error("Error trying to delete previous staff message", e); - } finally { - messageData.push("opponent_captain_notification_message_id", null); - messageData.push("opponent_captain_notification_channel_id", null); - } - } - + messageData = await looseDeleteRecordedMessage(messageData, "report_opponent_notification"); await this.helpers.updateRecord("Reports", report, { "Approved by staff": true, "Force approved": true, - "Log": (report.log ? report.log + "\n" : "") + `${(new Date()).toLocaleString()}: ${user.airtable.name} force-approved score report as staff`, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Force-approved score report", + "key=staff_force_approved" + ].join("|"), "Message Data": messageData.textMap }); } else if (reaction === "force-counter-approve") { @@ -84,25 +81,21 @@ export default { "Data": report.countered_data, "Countered Data": "", - "Log": (report.log ? report.log + "\n" : "") + `${(new Date()).toLocaleString()}: ${user.airtable.name} force-approved counter score report as staff`, + "Log": (report.log ? report.log + "\n" : "") + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + "staff=true", + "text=Force-approved counter report", + "key=staff_force_approved_counter" + ].join("|"), "Message Data": messageData.textMap }); } else if (reaction === "delete") { - - await Promise.all(["opponent_captain_notification", "staff_notification", "staff_confirmation"].map(async key => { - try { - const channel = await client.channels.fetch(messageData.get(`${key}_channel_id`)); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get(`${key}_channel_id`)); - } catch (e) { - console.error("Error trying to delete previous opponent captain notification message", e); - } finally { - messageData.push(`${key}_message_id`, null); - messageData.push(`${key}_channel_id`, null); - } - })); + messageData = await looseDeleteRecordedMessages(messageData, ["report_opponent_notification", "report_staff_notification", "report_completed_staff", "report_completed_public"]); await this.helpers.updateRecord("Matches", match, { - "Reports": [] + "Reports": [], + "Report History": [...(match.report_history || []), report.id].map(x => dirtyID(x)) }); const eventSettings = JSON.parse(event.blocks || "") as EventSettings; @@ -114,7 +107,7 @@ export default { if (client && eventSettings?.logging?.staffScoreReport) { const channel = await client.channels.fetch(eventSettings.logging.staffScoreReport); - if (channel?.isTextBased()) { + if (channel?.isSendable()) { try { await channel.send(`🗑️ **Denied & deleted**: Score report removed by ${user.airtable.name}.\n${matchLink}`); } catch (e) { diff --git a/server/src/actions/start-commercial.js b/server/src/actions/start-commercial.js index 4bec9cb8..4c2a7274 100644 --- a/server/src/actions/start-commercial.js +++ b/server/src/actions/start-commercial.js @@ -6,8 +6,8 @@ export default { requiredParams: ["commercialDuration"], /*** * @param {30 | 60 | 90 | 120 | 150 | 180} commercialDuration - * @param {UserData} user - * @param {ClientData} client + * @param {ActionAuth["user"]} user + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/submit-match-reschedule.ts b/server/src/actions/submit-match-reschedule.ts new file mode 100644 index 00000000..3649a7dd --- /dev/null +++ b/server/src/actions/submit-match-reschedule.ts @@ -0,0 +1,70 @@ +import { ActionAuth, Match, MatchResolvableID, Report } from "../types.js"; +import { deAirtableRecord, dirtyID, getMatchRescheduling } from "../action-utils/action-utils.js"; +import { get } from "../action-utils/action-cache.js"; +import { Action } from "../action-utils/action-manager-models.js"; + +export default { + key: "submit-match-reschedule", + requiredParams: ["matchID", "startTime"], + auth: ["user"], + async handler( + {matchID, startTime}: { + matchID: MatchResolvableID, + startTime: string + }, + {user}: ActionAuth + ) { + const { match, report: request } : { match: Match, report: Report | undefined } = await getMatchRescheduling(matchID, { excludeCompleted: true }); + if (request) { + throw "A reschedule request is already active on this match."; + } + + const teams = await Promise.all((match.teams || []).map(t => get(t))); + const actingTeam = teams.find(team => [ + ...(team.players || []), + ...(team.captains || []), + ...(team.staff || []), + ...(team.owners || []), + ].includes(dirtyID(user.airtable.id))); + + if (!actingTeam) throw "You don't have permission to report the score of this match"; + + if (match.start) { + const startTimeDate = new Date(startTime); + const matchTimeDate = new Date(match.start); + if (startTimeDate.getTime() === matchTimeDate.getTime()) { + throw "The match is already scheduled for this time"; + } + } + + const response = await this.helpers.createRecord("Reports", { + "Type": "Rescheduling", + "Player": [dirtyID(user.airtable.id)], + "Team": [dirtyID(actingTeam.id)], + "Match": [dirtyID(match.id)], + "Data": JSON.stringify({ + start: startTime + }), + "Log": [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + `text=Requested ${match.start ? "schedule" : "reschedule"}`, + "key=submitted_request" + ].join("|") + }); + if ("error" in response) { + throw `Airtable error: ${response.errorMessage}`; + } + if (response?.[0]?.id) { + await this.helpers.updateRecord("Matches", match, { + "Reports": [...(match.reports || []), response?.[0].id] + }); + await this.helpers.updateRecord("Reports", deAirtableRecord(response[0]), { + "Approved by team": true + }); + return response?.[0].id; + } + } +// @ts-expect-error Needs some action refactoring before it can fully satisfy +} satisfies Action; diff --git a/server/src/actions/submit-score-report.ts b/server/src/actions/submit-score-report.ts index ec0d9cd8..4319df85 100644 --- a/server/src/actions/submit-score-report.ts +++ b/server/src/actions/submit-score-report.ts @@ -42,7 +42,13 @@ export default { "Team": [dirtyID(actingTeam.id)], "Match": [dirtyID(match.id)], "Data": JSON.stringify(reportData), - "Log": `${(new Date()).toLocaleString()}: ${user.airtable.name} reported score as ${actingTeam?.name}` + "Log": [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + "text=Reported score", + "key=submitted_score_report" + ].join("|"), }); console.log(response); @@ -79,7 +85,13 @@ export default { if (!actingTeam) throw "You don't have permission to counter the score report of this match"; const response = await this.helpers.updateRecord("Reports", report, { - "Log": report.log + "\n" + `${(new Date()).toLocaleString()}: ${user.airtable.name} countered score report as ${actingTeam?.name}`, + "Log": report.log + "\n" + [ + `date=${(new Date()).getTime()}`, + `user=${user.airtable.id}`, + `team=${actingTeam?.id}`, + "text=Coutered score report", + "key=countered_score_report" + ].join("|"), "Countered Data": JSON.stringify(reportData), "Countered by opponent": true }); diff --git a/server/src/actions/toggle-flip-teams.js b/server/src/actions/toggle-flip-teams.js index 8f45143e..d027d92d 100644 --- a/server/src/actions/toggle-flip-teams.js +++ b/server/src/actions/toggle-flip-teams.js @@ -3,7 +3,7 @@ export default { auth: ["client"], /*** * @param {Object?} params - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/update-break-automation.js b/server/src/actions/update-break-automation.js index 93903fc5..252d6118 100644 --- a/server/src/actions/update-break-automation.js +++ b/server/src/actions/update-break-automation.js @@ -6,7 +6,7 @@ export default { requiredParams: ["options"], /*** * @param {string[]} options - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/update-break-display.js b/server/src/actions/update-break-display.js index 3e3ef0be..a302a1c9 100644 --- a/server/src/actions/update-break-display.js +++ b/server/src/actions/update-break-display.js @@ -6,7 +6,7 @@ export default { requiredParams: ["option"], /*** * @param {string} option - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/update-broadcast.js b/server/src/actions/update-broadcast.js index 7059fe46..92b62da1 100644 --- a/server/src/actions/update-broadcast.js +++ b/server/src/actions/update-broadcast.js @@ -6,7 +6,7 @@ export default { optionalParams: ["match", "advertise", "playerCams", "mapAttack", "title", "manualGuests", "deskDisplayMode", "deskDisplayText", "showLiveMatch", "countdownEnd", "highlightTeamID", "highlightHeroID", "highlightPlayerID", "highlightMediaID"], /*** * @param {AnyAirtableID} match - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/update-gfx-index.js b/server/src/actions/update-gfx-index.js index 0a1eefcc..3d167e7a 100644 --- a/server/src/actions/update-gfx-index.js +++ b/server/src/actions/update-gfx-index.js @@ -7,7 +7,7 @@ export default { /*** * @param {AnyAirtableID} gfxID * @param {number} index - * @param {ClientData} client + * @param {ActionAuth["client"]} client * @returns {Promise} */ // eslint-disable-next-line no-empty-pattern diff --git a/server/src/actions/update-map-data.js b/server/src/actions/update-map-data.js index 4bc7b148..2e48b03a 100644 --- a/server/src/actions/update-map-data.js +++ b/server/src/actions/update-map-data.js @@ -17,7 +17,8 @@ export default { * @param {number?} mapData.score_2 * @param {boolean?} mapData.draw * - * @param {UserData} user + * @param {ActionAuth["user"]} user + * @param {ActionAuth["isAutomation"]} isAutomation * @returns {Promise} */ async handler({ matchID, mapData }, { user, isAutomation }) { diff --git a/server/src/actions/update-match-data.js b/server/src/actions/update-match-data.js index a3419ea9..2c0597c5 100644 --- a/server/src/actions/update-match-data.js +++ b/server/src/actions/update-match-data.js @@ -5,7 +5,8 @@ export default { /*** * @param {AnyAirtableID} matchID * @param {object?} updatedData - * @param {UserData} user + * @param {ActionAuth["user"]} user + * @param {ActionAuth["isAutomation"]} isAutomation * @returns {Promise} */ async handler({ matchID, updatedData }, { user, isAutomation }) { diff --git a/server/src/actions/update-player-discord-id.js b/server/src/actions/update-player-discord-id.js index e5700d2e..31b8f5a2 100644 --- a/server/src/actions/update-player-discord-id.js +++ b/server/src/actions/update-player-discord-id.js @@ -12,7 +12,7 @@ export default { /*** * @param {import("discord.js").User} discordData * @param {string} slmnggId - * @param {UserData} user + * @param {ActionAuth["user"]} user * @returns {Promise} */ async handler({ discordData, slmnggId }, { user }) { diff --git a/server/src/actions/update-profile-data.js b/server/src/actions/update-profile-data.js index 6926340a..ed7524e1 100644 --- a/server/src/actions/update-profile-data.js +++ b/server/src/actions/update-profile-data.js @@ -7,7 +7,7 @@ export default { auth: ["user"], /*** * @param {object} profileData - * @param {UserData} user + * @param {ActionAuth["user"]} user * @returns {Promise} */ async handler({ profileData }, { user }) { diff --git a/server/src/automation/on-score-report-update.ts b/server/src/automation/on-score-report-update.ts index 464e6b6c..5c19fcb9 100644 --- a/server/src/automation/on-score-report-update.ts +++ b/server/src/automation/on-score-report-update.ts @@ -1,22 +1,29 @@ -import { AnyAirtableID, EventSettings, Report } from "../types.js"; +import { + AnyAirtableID, + EventSettings, + Report, + ReschedulingReportKeys, + ScoreReportingReportKeys, + Theme +} from "../types.js"; import { get } from "../action-utils/action-cache.js"; import * as Cache from "../cache.js"; import { getInternalManager } from "../action-utils/action-manager.js"; -import { cleanID, updateRecord } from "../action-utils/action-utils.js"; +import { cleanID, hammerTime, updateRecord } from "../action-utils/action-utils.js"; import client from "../discord/client.js"; import { MapObject } from "../discord/managers.js"; -import { generateMatchReportText } from "../action-utils/ts-action-utils.js"; +import { + generateMatchReportText, + looseDeleteRecordedMessage, + looseDeleteRecordedMessages, + sendRecordedMessage +} from "../action-utils/ts-action-utils.js"; +import { ButtonBuilder, ButtonStyle } from "discord.js"; const processing = new Set(); +const dataServer = process.env.NODE_ENV === "development" ? "http://localhost:8901" : "https://data.slmn.gg"; export default { - /** - * - * @param {AnyAirtableID} id - * @param {object} newData - * @param {object?} oldData - * @returns {Promise} - */ async handler({ id, newData: report, oldData }: { id: AnyAirtableID, newData: Report, oldData: Report }) { if (!process.env.IS_SLMNGG_MAIN_SERVER) return; if (report?.__tableName !== "Reports") return; @@ -37,22 +44,26 @@ export default { const event = await get(match?.event?.[0]); if (!event?.id || !event?.blocks) return; + const defaultColor = "#4468a8"; + let eventTheme: Theme | null = null; + if (event?.theme?.length) { + eventTheme = await get(event.theme[0]); + } + const eventColor = parseInt((eventTheme?.color_theme_on_dark || eventTheme?.color_theme || defaultColor).slice(1), 16); + const opponentIDs = (match.teams || []).filter(id => cleanID(id) !== cleanID(report.team?.[0])); const opponents = await Promise.all(opponentIDs.map(id => get(id))); const submittingTeam = report.team?.[0] ? await get(report.team?.[0]) : null; + const allTeams = await Promise.all((match.teams || []).map(id => get(id))); let subdomain = ""; - - if (match?.event?.length) { - const event = await Cache.get(match?.event?.[0]); - if (event?.subdomain || event?.partial_subdomain) { - subdomain = (event.subdomain || event.partial_subdomain || "") + "."; - } + if (event?.subdomain || event?.partial_subdomain) { + subdomain = (event.subdomain || event.partial_subdomain || "") + "."; } - const matchLink = `https://${subdomain}slmn.gg/match/${cleanID(match.id)}/score-reporting`; + const matchLink = `https://${subdomain}slmn.gg/match/${cleanID(match.id)}`; const eventSettings = JSON.parse(event.blocks) as EventSettings; - const messageData = new MapObject(report.message_data); + let messageData = new MapObject(report.message_data); if (report.type === "Scores" && report.data) { if (!eventSettings?.reporting?.score?.use) return; @@ -60,7 +71,9 @@ export default { report.force_approved || ( (report.approved_by_team && (eventSettings.reporting.score.opponentApprove ? report.approved_by_opponent : true) && - (eventSettings.reporting.score.staffApprove ? report.approved_by_staff : true)) + (eventSettings.reporting.score.staffApprove ? report.approved_by_staff : true)) && + !report.denied_by_staff && + !report.denied_by_opponent ); if (reportApproved) { @@ -69,7 +82,7 @@ export default { const manager = getInternalManager(); if (!manager) return console.error("No internal manager can run automation action"); try { - const { matchData, mapData } = JSON.parse(report.data); + const {matchData, mapData} = JSON.parse(report.data); console.log({ matchData, mapData @@ -89,53 +102,29 @@ export default { // Delete record here (not implemented?) console.log("Can now delete the score report"); - if (messageData.get("staff_notification_message_id") && messageData.get("staff_notification_channel_id")) { - try { - const channel = await client.channels.fetch(messageData.get("staff_notification_channel_id")); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get("staff_notification_message_id")); - } catch (e) { - console.error("Error trying to delete previous staff message", e); - } finally { - messageData.push("staff_notification_message_id", null); - messageData.push("staff_notification_channel_id", null); - } - } + messageData = await looseDeleteRecordedMessage(messageData, "report_staff_notification"); - if (client && - opponents.length && - eventSettings?.logging?.staffCompletedScoreReport - ) { - const channel = await client.channels.fetch(eventSettings.logging.staffCompletedScoreReport); - if (channel?.isTextBased()) { - try { - const scoreReportMessage = await channel.send(`🎉 Score report approved\n${report.log}\n${matchLink}`); - messageData.push("staff_confirmation_channel_id", channel.id); - messageData.push("staff_confirmation_message_id", scoreReportMessage.id); - } catch (e) { - console.error("Channel sending error", e); - } - } + if (client && eventSettings?.logging?.staffCompletedScoreReport) { + messageData = await sendRecordedMessage({ + key: "report_completed_staff", + mapObject: messageData, + channelID: eventSettings.logging.staffCompletedScoreReport, + content: `🎉 Score report approved\n${report.log}\n${matchLink}/score-reporting` + }); } if (client && eventSettings?.logging?.postMatchReports) { - const channel = await client.channels.fetch(eventSettings.logging.postMatchReports); - if (channel?.isTextBased()) { - try { - const reportText = await generateMatchReportText(await get(match.id)); - if (reportText) { - const scoreReportMessage = await channel.send(reportText); - messageData.push("score_report_channel_id", channel.id); - messageData.push("score_report_message_id", scoreReportMessage.id); - } - } catch (e) { - console.error("Channel sending error", e); - } - } + messageData = await sendRecordedMessage({ + key: "report_completed_public", + mapObject: messageData, + channelID: eventSettings.logging.postMatchReports, + content: await generateMatchReportText(await get(match.id)) + }); } await updateRecord(Cache, "Reports", report, { "Approved": true, "Message Data": messageData.textMap - }); + }, "automation/on-score-report-update"); } catch (e) { console.error("Action error - not continuing"); @@ -154,19 +143,17 @@ export default { eventSettings?.reporting?.score?.staffApprove && !report.approved_by_staff ) { - const channel = await client.channels.fetch(eventSettings.logging.staffScoreReport); - if (channel?.isTextBased()) { - try { - const staffNotification = await channel.send(`📣 A score report from ${submittingTeam ? submittingTeam.name : "a team"} has been approved by their opponent and is ready for staff approval\n${matchLink}`); - messageData.push("staff_notification_channel_id", channel.id); - messageData.push("staff_notification_message_id", staffNotification.id); + messageData = await sendRecordedMessage({ + key: "report_staff_notification", + mapObject: messageData, + channelID: eventSettings.logging.staffScoreReport, + content: `📣 A score report from ${submittingTeam ? submittingTeam.name : "a team"} has been approved by their opponent and is ready for staff approval\n${matchLink}/score-reporting`, + success: async (mapObject : MapObject) => { await updateRecord(Cache, "Reports", report, { - "Message Data": messageData.textMap - }); - } catch (e) { - console.error("Channel sending error", e); + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); } - } + }); } } else if (!oldData.approved_by_team && report.approved_by_team) { @@ -181,44 +168,41 @@ export default { eventSettings?.reporting?.score?.opponentApprove && !report.approved_by_opponent ) { - const channel = await client.channels.fetch(eventSettings.logging.captainNotifications); - if (channel?.isTextBased()) { - const opponentPings = opponents.map(opponent => { - const discordControl = new MapObject(opponent?.discord_control); - return discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : opponent.name; - }); - try { - const opponentNotification = await channel.send(`📣 ${opponentPings.join(" ")}\nA score report from ${submittingTeam ? submittingTeam.name : "your opponent"} is ready for approval\n${matchLink}`); - messageData.push("opponent_captain_notification_channel_id", channel.id); - messageData.push("opponent_captain_notification_message_id", opponentNotification.id); + const opponentPings = opponents.map(opponent => { + const discordControl = new MapObject(opponent?.discord_control); + return discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : opponent.name; + }); + messageData = await sendRecordedMessage({ + key: "report_opponent_notification", + mapObject: messageData, + channelID: eventSettings.logging.captainNotifications, + content: `📣 ${opponentPings.join(" ")}\nA score report from ${submittingTeam ? submittingTeam.name : "your opponent"} is ready for approval\n${matchLink}/score-reporting`, + success: async (mapObject : MapObject) => { await updateRecord(Cache, "Reports", report, { - "Message Data": messageData.textMap - }); - } catch (e) { - console.error("Channel sending error", e); + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); } - } + }); // we can also go straight to staff approving if necessary } else if (client && (!eventSettings?.reporting?.score?.opponentApprove || report.approved_by_opponent) && // passed opponent approval - eventSettings?.logging?.staffScoreReport && + eventSettings?.logging?.staffApprovalNotifications && eventSettings?.reporting?.score?.staffApprove && !report.approved_by_staff ) { - const channel = await client.channels.fetch(eventSettings.logging.staffScoreReport); - if (channel?.isTextBased()) { - try { - const staffNotification = await channel.send(`📣 A score report from ${submittingTeam ? submittingTeam.name : "a team"} has been approved by their opponent and is ready for staff approval\n${matchLink}`); - messageData.push("staff_notification_channel_id", channel.id); - messageData.push("staff_notification_message_id", staffNotification.id); + messageData = await sendRecordedMessage({ + key: "report_staff_notification", + mapObject: messageData, + channelID: eventSettings.logging.staffApprovalNotifications, + content: `📣 A score report from ${submittingTeam ? submittingTeam.name : "a team"} has been approved by their opponent and is ready for staff approval\n${matchLink}/score-reporting`, + success: async (mapObject : MapObject) => { await updateRecord(Cache, "Reports", report, { - "Message Data": messageData.textMap - }); - } catch (e) { - console.error("Channel sending error", e); + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); } - } + }); + } } else if (!oldData.countered_by_opponent && report.countered_by_opponent) { @@ -226,18 +210,7 @@ export default { console.log("Report has been counted by opposing team"); console.log({oldData, newData: report}); - if (messageData.get("opponent_captain_notification_message_id") && messageData.get("opponent_captain_notification_channel_id")) { - try { - const channel = await client.channels.fetch(messageData.get("opponent_captain_notification_channel_id")); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get("opponent_captain_notification_message_id")); - } catch (e) { - console.error("Error trying to delete previous opponent captain notification message", e); - } finally { - messageData.push("opponent_captain_notification_message_id", null); - messageData.push("opponent_captain_notification_channel_id", null); - } - } - + messageData = await looseDeleteRecordedMessage(messageData, "report_opponent_notification"); // tell opponent if (client && @@ -245,21 +218,15 @@ export default { eventSettings?.logging?.captainNotifications && !report.approved_by_opponent ) { - const channel = await client.channels.fetch(eventSettings.logging.captainNotifications); - if (channel?.isTextBased()) { - - const discordControl = new MapObject(submittingTeam.discord_control); - const originalPing = discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : submittingTeam.name; - - try { - const originalNotification = await channel.send(`📣 ${originalPing}\nYour score report has been denied and countered by your opponent. Please check their submission to see if it is correct:\n${matchLink}`); - messageData.push("original_captain_notification_channel_id", channel.id); - messageData.push("original_captain_notification_message_id", originalNotification.id); - } catch (e) { - console.error("Channel sending error", e); - } - } - + const discordControl = new MapObject(submittingTeam.discord_control); + const originalPing = discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : submittingTeam.name; + + messageData = await sendRecordedMessage({ + key: "report_opponent_notification", + mapObject: messageData, + channelID: eventSettings.logging.captainNotifications, + content: `📣 ${originalPing}\nYour score report has been denied and countered by your opponent. Please check their submission to see if it is correct:\n${matchLink}/score-reporting` + }); } // tell staff? @@ -267,37 +234,475 @@ export default { eventSettings?.logging?.staffScoreReport && !report.approved_by_staff ) { - if (messageData.get("staff_notification_channel_id") && messageData.get("staff_notification_message_id")) { - try { - const channel = await client.channels.fetch(messageData.get("staff_notification_channel_id")); - if (channel?.isTextBased()) await channel.messages.delete(messageData.get("staff_notification_message_id")); - } catch (e) { - console.error("Error trying to delete previous staff notification message", e); - } finally { - messageData.push("staff_notification_message_id", null); - messageData.push("staff_notification_channel_id", null); + messageData = await looseDeleteRecordedMessage(messageData, "report_staff_notification"); + messageData = await sendRecordedMessage({ + key: "report_staff_notification", + mapObject: messageData, + channelID: eventSettings.logging.staffScoreReport, + content: `📣 A score report from ${submittingTeam ? submittingTeam.name : "a team"} has been **denied and countered** by their opponent.\n${matchLink}/score-reporting`, + success: async (mapObject : MapObject) => { + await updateRecord(Cache, "Reports", report, { + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); } - } + }); + } - const channel = await client.channels.fetch(eventSettings.logging.staffScoreReport); - if (channel?.isTextBased()) { - try { - const staffNotification = await channel.send(`📣 A score report from ${submittingTeam ? submittingTeam.name : "a team"} has been **denied and countered** by their opponent.\n${matchLink}`); - messageData.push("staff_notification_channel_id", channel.id); - messageData.push("staff_notification_message_id", staffNotification.id); - } catch (e) { - console.error("Channel sending error", e); + } else { + // other change + console.log("Report changed something else"); + const keys = Object.keys({...report,...structuredClone(oldData)}) as (keyof Report)[]; + const changes = keys + .map(key => ({ key, newVal: report[key], oldVal: structuredClone(oldData[key])})) + .filter(({ oldVal, newVal}) => JSON.stringify(oldVal) !== JSON.stringify(newVal)); + + console.log({oldData, newData: report}); + console.log(changes); + } + } + } + else if (report.type === "Rescheduling" && report.data) { + if (!eventSettings?.reporting?.rescheduling?.use) return; + const { start: proposedStart } = JSON.parse(report.data); + + const reportApproved = + report.force_approved || ( + (report.approved_by_team && + (eventSettings.reporting.rescheduling.opponentApprove ? report.approved_by_opponent : true) && + (eventSettings.reporting.rescheduling.staffApprove ? report.approved_by_staff : true)) + ); + + if (reportApproved) { + // Process approval + console.log("Reschedule request is now ready for approval"); + const manager = getInternalManager(); + if (!manager) return console.error("No internal manager can run automation action"); + + try { + const { start } = JSON.parse(report.data); + if (start) { + await manager.runActionAsAutomation("update-match-data", { + matchID: match.id, + updatedData: { + start } - } + }); } + // Delete record here (not implemented?) + console.log("Can now delete the score report"); + // approval messages that need to be deleted + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_opponent_approval", "reschedule_staff_approval", "reschedule_staff_preapproval"]); + + // #3 Teams pinged about reschedule + + const teamPings = allTeams.map(opponent => { + const discordControl = new MapObject(opponent?.discord_control); + return discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : opponent.name; + }); + + // remove old completion message + messageData = await looseDeleteRecordedMessage(messageData, "reschedule_completed"); + messageData = await sendRecordedMessage({ + key: "reschedule_completed", + mapObject: messageData, + channelID: eventSettings.logging?.matchTimeChanges, + content: `⌚ Match reschedule ${teamPings.join(" ")}: \n${opponents.map(t => t.name || t.code).join(" vs ")} ${match.start ? "rescheduled to" : "scheduled for"} ${hammerTime(proposedStart)}.\n${matchLink}` + }); await updateRecord(Cache, "Reports", report, { + "Approved": true, "Message Data": messageData.textMap + }, "automation/on-score-report-update"); + + } catch (e) { + console.error("Action error - not continuing"); + } + + } else { + // Not ready to approve - see what changed though + if ( + (!oldData.approved_by_team && report.approved_by_team) && // just submitted + eventSettings.reporting.rescheduling.staffApprove && // staff approval required + eventSettings.logging?.sendStaffPreapprovalNotifications && // staff pre approval messages enabled + eventSettings.logging?.staffApprovalNotifications && // staff notification channel set up + !report.approved_by_staff // not yet approved by staff + ) { + // Send a pre-approval message + console.log("Triggering pre-approval message for staff"); + messageData = await sendRecordedMessage({ + key: "reschedule_staff_preapproval", + mapObject: messageData, + channelID: eventSettings.logging.staffApprovalNotifications, + content: { + content: "", + embeds: [{ + title: `${match.start ? "Reschedule" : "Schedule"} request: ${allTeams.map(t => t.name || t.code).join(" vs ")}`, + url: `${matchLink}/rescheduling`, + description: `${submittingTeam?.name ? `${submittingTeam.name} have` : "An opponent has"} ${match.start ? "requested a reschedule" : "requested a start time"} for the match.\nStaff approval is required, pre-approval is available now.`, + fields: [ + { + name: "Proposed start time", + value: hammerTime(proposedStart), + inline: true + }, + { + name: "Current start time", + value: match.start ? hammerTime(match.start) : "Not yet scheduled", + inline: true + } + ], + thumbnail: { + url: `${dataServer}/match.png?id=${cleanID(match.id)}&size=720&padding=30`, + }, + color: eventColor + }], + components: [ + { + type: 1, + components: [ + new ButtonBuilder() + .setLabel("Pre-approve") + .setEmoji("<:shield_check:1322375448260247632>") + .setStyle(ButtonStyle.Primary) + .setCustomId(`reschedule_staff_approval/${cleanID(match.id)}/pre-approve`), + new ButtonBuilder() + .setLabel("Force approve") + .setEmoji("<:checksolidsq:1322072500082839623>") + .setStyle(ButtonStyle.Success) + .setCustomId(`reschedule_staff_approval/${cleanID(match.id)}/force-approve`), + new ButtonBuilder() + .setLabel("Deny") + .setEmoji("<:timessolidsq:1322072513697808504>") + .setStyle(ButtonStyle.Danger) + .setCustomId(`reschedule_staff_approval/${cleanID(match.id)}/deny`), + new ButtonBuilder() + .setLabel("Details") + .setStyle(ButtonStyle.Link) + .setURL(`${matchLink}/rescheduling`) + ] + } + ] + }, + success: async (mapObject: MapObject) => { + await updateRecord(Cache, "Reports", report, { + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); + } + }); + } + + if ( + (!oldData.approved_by_team && report.approved_by_team) && // just submitted + eventSettings.reporting.rescheduling.opponentApprove && // opponent approval required + !report.approved_by_opponent // not yet approved by opponent + ) { + // #1 Opponent approval request + console.log("Opponent approval request"); + + const opponentPings = opponents.map(opponent => { + const discordControl = new MapObject(opponent?.discord_control); + return discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : opponent.name; + }); + + + if (eventSettings.logging?.captainNotifications) { + messageData = await sendRecordedMessage({ + key: "reschedule_opponent_approval", + mapObject: messageData, + channelID: eventSettings.logging?.captainNotifications, + content: { + content: `📣 ${opponentPings.join(" ")}`, + embeds: [{ + title: `${match.start ? "Reschedule" : "Schedule"} request: ${allTeams.map(t => t.name || t.code).join(" vs ")}`, + url: `${matchLink}/rescheduling`, + description: `${submittingTeam?.name ? `${submittingTeam.name} have` : "Your opponent has"} ${match.start ? "requested a reschedule" : "requested a start time"} for the match.`, + fields: [ + { + name: "Proposed start time", + value: hammerTime(proposedStart), + inline: true + }, + { + name: "Current start time", + value: match.start ? hammerTime(match.start) : "Not yet scheduled", + inline: true + } + ], + // author: { + // name: event.name, + // icon_url: eventTheme?.id ? `${dataServer}/theme.png?id=${cleanID(eventTheme?.id)}&size=500&padding=20` : null, + // author_url: `https://${subdomain}slmn.gg/event/${cleanID(event.id)}` + // }, + thumbnail: { + url: `${dataServer}/match.png?id=${cleanID(match.id)}&size=720&padding=30`, + }, + color: eventColor + }], + components: [ + { + type: 1, + components: [ + new ButtonBuilder() + .setLabel("Approve") + .setEmoji("<:checksolidsq:1322072500082839623>") + .setStyle(ButtonStyle.Success) + .setCustomId(`reschedule_opponent_approval/${cleanID(match.id)}/approve`), + new ButtonBuilder() + .setLabel("Deny") + .setEmoji("<:timessolidsq:1322072513697808504>") + .setStyle(ButtonStyle.Danger) + .setCustomId(`reschedule_opponent_approval/${cleanID(match.id)}/deny`), + new ButtonBuilder() + .setLabel("Details") + // .setEmoji("<:infocircle:1322010140916056225>") + .setStyle(ButtonStyle.Link) + .setURL(`${matchLink}/rescheduling`) + ] + } + ] + }, + success: async (mapObject: MapObject) => { + await updateRecord(Cache, "Reports", report, { + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); + } + }); + } + } else if ( + ( + (!oldData.approved_by_team && report.approved_by_team && + !eventSettings.reporting.rescheduling.opponentApprove) || // just submitted and opponent not required + (!oldData.approved_by_opponent && report.approved_by_opponent) // or opponent approval just completed + ) && + eventSettings.reporting.rescheduling.staffApprove && // staff approval required + !report.approved_by_staff // not yet approved by staff + ) { + // #2 Staff prompted about request + console.log("Staff prompt for request"); + + messageData = await looseDeleteRecordedMessage(messageData, "reschedule_staff_preapproval"); + messageData = await sendRecordedMessage({ + key: "reschedule_staff_approval", + mapObject: messageData, + channelID: eventSettings.logging?.staffApprovalNotifications, + content: { + content: "", + embeds: [{ + title: `${match.start ? "Reschedule" : "Schedule"} request: ${allTeams.map(t => t.name || t.code).join(" vs ")}`, + url: `${matchLink}/rescheduling`, + description: `${submittingTeam?.name ? `${submittingTeam.name} have` : "An opponent has"} ${match.start ? "requested a reschedule" : "requested a start time"} for the match.\nStaff approval is required.`, + fields: [ + { + name: "Proposed start time", + value: hammerTime(proposedStart), + inline: true + }, + { + name: "Current start time", + value: match.start ? hammerTime(match.start) : "Not yet scheduled", + inline: true + } + ], + thumbnail: { + url: `${dataServer}/match.png?id=${cleanID(match.id)}&size=720&padding=30`, + }, + color: eventColor + }], + components: [ + { + type: 1, + components: [ + new ButtonBuilder() + .setLabel("Approve") + .setEmoji("<:checksolidsq:1322072500082839623>") + .setStyle(ButtonStyle.Success) + .setCustomId(`reschedule_staff_approval/${cleanID(match.id)}/approve`), + new ButtonBuilder() + .setLabel("Deny") + .setEmoji("<:timessolidsq:1322072513697808504>") + .setStyle(ButtonStyle.Danger) + .setCustomId(`reschedule_staff_approval/${cleanID(match.id)}/deny`), + new ButtonBuilder() + .setLabel("Details") + .setStyle(ButtonStyle.Link) + .setURL(`${matchLink}/rescheduling`) + ] + } + ] + }, + success: async (mapObject: MapObject) => { + await updateRecord(Cache, "Reports", report, { + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); + } + }); + } else if ( + eventSettings.reporting.rescheduling.opponentApprove && // opponent approval required + (!oldData.denied_by_opponent && report.denied_by_opponent) && // just denied by opponent + client && eventSettings?.logging?.captainNotifications + ) { + // opponent denial + console.log("Just Denied"); + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_opponent_approval", "reschedule_staff_approval", "reschedule_staff_preapproval"]); + + const teamPings = allTeams.map(opponent => { + const discordControl = new MapObject(opponent?.discord_control); + return discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : opponent.name; + }); + + const opponentNames = opponents.map(opponent => opponent.name || opponent.code).join(" & "); + + console.log("Message send start"); + messageData = await sendRecordedMessage({ + key: "reschedule_opponent_denial", + mapObject: messageData, + channelID: eventSettings.logging?.captainNotifications, + content: { + content: `📣 ${teamPings.join(" ")}`, + embeds: match.start ? [{ + title: `Reschedule request denied: ${allTeams.map(t => t.name || t.code).join(" vs ")}`, + url: `${matchLink}/rescheduling`, + description: `<:danger_exclamation_circle:1322072455346655334> ${opponentNames ? `${opponentNames} have` : "Your opponent has"} denied a reschedule for the match.\nYou can start a new request on SLMN.GG.`, + fields: [ + { + name: "Match start time", + value: `The match has **not been rescheduled** and is still set for ${hammerTime(match.start)}` + }, + { + name: "Proposed start time", + value: hammerTime(proposedStart), + inline: true + }, + + ], + thumbnail: { + url: `${dataServer}/match.png?id=${cleanID(match.id)}&size=720&padding=30`, + }, + color: parseInt("dc3545", 16) + }] : [{ + title: `Schedule request denied: ${allTeams.map(t => t.name || t.code).join(" vs ")}`, + url: `${matchLink}/rescheduling`, + description: `<:danger_exclamation_circle:1322072455346655334> ${opponentNames ? `${opponentNames} have` : "Your opponent has"} denied the requested start time for the match.\nYou can start a new request on SLMN.GG.`, + fields: [ + { + name: "Match start time", + value: "The match has **not been scheduled**.", + inline: true + }, + { + name: "Proposed start time", + value: hammerTime(proposedStart), + inline: true + }, + ], + thumbnail: { + url: `${dataServer}/match.png?id=${cleanID(match.id)}&size=720&padding=30`, + }, + color: parseInt("dc3545", 16) + }], + components: [ + { + type: 1, + components: [ + new ButtonBuilder() + .setLabel("Details") + // .setEmoji("<:infocircle:1322010140916056225>") + .setStyle(ButtonStyle.Link) + .setURL(`${matchLink}/rescheduling`) + ] + } + ] + }, + success: async (mapObject) => { + console.log("Message send success", mapObject.data); + await updateRecord(Cache, "Reports", report, { + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); + } + }); + } if ( + (!oldData.denied_by_staff && report.denied_by_staff) && // just denied by staff + client && eventSettings?.logging?.captainNotifications + ) { + // opponent denial + console.log("Just Denied by staff"); + messageData = await looseDeleteRecordedMessages(messageData, ["reschedule_opponent_approval", "reschedule_staff_approval", "reschedule_staff_preapproval", "reschedule_opponent_denial"]); + + const teamPings = allTeams.map(opponent => { + const discordControl = new MapObject(opponent?.discord_control); + return discordControl.get("role_id") ? `<@&${discordControl.get("role_id")}>` : opponent.name; }); + console.log("Message send start"); + messageData = await sendRecordedMessage({ + key: "reschedule_staff_denial", + mapObject: messageData, + channelID: eventSettings.logging?.captainNotifications, + content: { + content: `📣 ${teamPings.join(" ")}`, + embeds: match.start ? [{ + title: `Reschedule request denied: ${allTeams.map(t => t.name || t.code).join(" vs ")}`, + url: `${matchLink}/rescheduling`, + description: "<:danger_exclamation_circle:1322072455346655334> Staff have denied a reschedule for the match.\nRescheduling for this match has been locked, contact staff for more information.", + fields: [ + { + name: "Match start time", + value: `The match has **not been rescheduled** and is still set for ${hammerTime(match.start)}` + }, + { + name: "Proposed start time", + value: hammerTime(proposedStart), + inline: true + }, + + ], + thumbnail: { + url: `${dataServer}/match.png?id=${cleanID(match.id)}&size=720&padding=30`, + }, + color: parseInt("dc3545", 16) + }] : [{ + title: `Schedule request denied: ${allTeams.map(t => t.name || t.code).join(" vs ")}`, + url: `${matchLink}/rescheduling`, + description: "<:danger_exclamation_circle:1322072455346655334> Staff have denied the requested start time for the match.\nScheduling for this match has been locked, contact staff for more information.", + fields: [ + { + name: "Match start time", + value: "The match has **not been scheduled**.", + inline: true + }, + { + name: "Proposed start time", + value: hammerTime(proposedStart), + inline: true + }, + ], + thumbnail: { + url: `${dataServer}/match.png?id=${cleanID(match.id)}&size=720&padding=30`, + }, + color: parseInt("dc3545", 16) + }], + components: [ + { + type: 1, + components: [ + new ButtonBuilder() + .setLabel("Details") + // .setEmoji("<:infocircle:1322010140916056225>") + .setStyle(ButtonStyle.Link) + .setURL(`${matchLink}/rescheduling`) + ] + } + ] + }, + success: async (mapObject) => { + console.log("Message send success", mapObject.data); + await updateRecord(Cache, "Reports", report, { + "Message Data": mapObject.textMap + }, "automation/on-score-report-update"); + } + }); } else { - // other change - console.log("Report changed something else"); + console.log("Reschedule report changed something else"); const keys = Object.keys({...report,...structuredClone(oldData)}) as (keyof Report)[]; const changes = keys .map(key => ({ key, newVal: report[key], oldVal: structuredClone(oldData[key])})) @@ -305,7 +710,6 @@ export default { console.log({oldData, newData: report}); console.log(changes); - } } } diff --git a/server/src/cache.js b/server/src/cache.js index 92958223..60b6ad56 100644 --- a/server/src/cache.js +++ b/server/src/cache.js @@ -411,6 +411,7 @@ async function getTwitchAccessToken(channel) { // get stored access token, check if it's valid // otherwise / or if no token, get from refresh token if (!channel) return null; + // TODO: put this in a different map to help with typing let storedToken = authMap.get(`twitch_access_token_${channel.channel_id}`); if (!storedToken || accessTokenIsExpired(storedToken)) { diff --git a/server/src/discord/auth.js b/server/src/discord/auth.js index 72935587..33f9cf46 100644 --- a/server/src/discord/auth.js +++ b/server/src/discord/auth.js @@ -151,7 +151,7 @@ export default ({ app, router, cors, Cache }) => { if (airtable && ![discord.username, `${discord.username}#${discord.discriminator}`].includes(airtable.discord_tag)) { const updatedUsername = discord.discriminator === "0" ? discord.username : `${discord.username}#${discord.discriminator}`; - await updateRecord(Cache, "Players", airtable, { "Discord Tag": updatedUsername }); + await updateRecord(Cache, "Players", airtable, { "Discord Tag": updatedUsername }, "discord/auth"); } return { discord, airtable }; diff --git a/server/src/discord/commands/admin/set-user-id.js b/server/src/discord/commands/admin/set-user-id.js index 7da73e66..78d90cf0 100644 --- a/server/src/discord/commands/admin/set-user-id.js +++ b/server/src/discord/commands/admin/set-user-id.js @@ -1,8 +1,11 @@ import { ActionRowBuilder, - ApplicationCommandType, ButtonBuilder, ButtonStyle, + ApplicationCommandType, + ButtonBuilder, + ButtonStyle, ContextMenuCommandBuilder, - StringSelectMenuBuilder, StringSelectMenuOptionBuilder, + StringSelectMenuBuilder, + StringSelectMenuOptionBuilder, userMention } from "discord.js"; import * as Cache from "../../../cache.js"; @@ -85,7 +88,7 @@ export default { const internalManager = getInternalManager(); if (!internalManager) { - return interaction.followUp({ ephemeral, content: "No action handlers can process your request." }); + return interaction.followUp({ ephemeral, content: "Could not handle this request (no internal system available)" }); } const potentials = await internalManager.runAction("match-discord-slmngg-player", { discordData: interaction.targetUser }, token); diff --git a/server/src/discord/commands/prod/webcam.js b/server/src/discord/commands/prod/webcam.js index 5f875e59..23ed8a46 100644 --- a/server/src/discord/commands/prod/webcam.js +++ b/server/src/discord/commands/prod/webcam.js @@ -41,7 +41,7 @@ export default { const internalManager = getInternalManager(); if (!internalManager) { - return interaction.editReply(startingPing + "No action handlers can process your request."); + return interaction.editReply(startingPing + "Could not handle this request (no internal system available)"); } await internalManager.runAction("create-live-guest", {}, token) diff --git a/server/src/discord/interactions.ts b/server/src/discord/interactions.ts new file mode 100644 index 00000000..5df2b47b --- /dev/null +++ b/server/src/discord/interactions.ts @@ -0,0 +1,95 @@ +import fs from "node:fs"; +import path from "node:path"; +import { Collection, Events, InteractionType, MessageComponentInteraction } from "discord.js"; +import client from "./client.js"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const DIRNAME = path.dirname(fileURLToPath(import.meta.url)); + +export type InteractionHandler = { + name?: string; + execute: (interaction: MessageComponentInteraction, args: string[]) => Promise; +} + +if (client) { + const interactions = new Collection(); + const foldersPath = path.join(DIRNAME, "interactions"); + const commandFolders = fs.readdirSync(foldersPath); + + console.log("[interactions] loading interactions"); + + for (const folder of commandFolders) { + const commandsPath = path.join(foldersPath, folder); + const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith(".js") || file.endsWith(".ts")); + for (const file of commandFiles) { + const filePath = path.join(commandsPath, file); + // @ts-expect-error esm import requires a url here + const { default: command } = await import(pathToFileURL(filePath)) as { default: InteractionHandler }; + // Set a new item in the Collection with the key as the command name and the value as the exported module + if ("execute" in command) { + const commandName = command?.name || [folder, (path.basename(filePath).replace(/\.(js|ts)$/, ""))].join("_"); + console.log("~/", commandName); + + interactions.set(commandName, command); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } + } + + async function respond(interaction: MessageComponentInteraction, response: string, isError = false) { + if (isError) { + response = `<:danger_exclamation_circle:1322072455346655334> ${response}`; + } else { + response = `<:success_check_circle:1322072439718416466> ${response}`; + } + if (!response.trim().endsWith(".")) response += "."; + + try { + if (interaction.replied || interaction.deferred) { + await interaction.followUp({ + content: response, + ephemeral: true + }); + } else { + await interaction.reply({ + content: response, + ephemeral: true + }); + } + } catch (e) { + console.error("Error sending follow up/reply to interaction", e); + } + } + + client.on(Events.InteractionCreate, async (interaction) => { + // console.dir(interaction, { depth: null }); + + if (interaction.type !== InteractionType.MessageComponent) return; + const [id, ...args] = interaction.customId.split("/"); + const command = interactions.get(id); + + + if (!command) { + if (id) console.error(`No interaction matching ID ${id} was found.`, { id, args }); + return await respond(interaction, "Unknown command", true); + } + + try { + const response = await command.execute(interaction, args); + // console.log("Interaction execute response", response, response.error); + if (typeof response === "object" && response.error) { + throw response.error; + } else if (typeof response === "string") { + await respond(interaction, response); + } + // console.log({ replied: interaction.replied, deferred: interaction.deferred }); + } catch (error: any) { + console.error(error); + const errorMessage = typeof error === "string" ? error : (error.errorMessage || error.message); + await respond(interaction, errorMessage, true); + } + }); +} else { + console.warn("Discord interactions will not be set up because no Discord key is set."); +} diff --git a/server/src/discord/interactions/reschedule/opponent_approval.ts b/server/src/discord/interactions/reschedule/opponent_approval.ts new file mode 100644 index 00000000..7e612615 --- /dev/null +++ b/server/src/discord/interactions/reschedule/opponent_approval.ts @@ -0,0 +1,31 @@ +import { MatchResolvableID } from "../../../types.js"; +import * as Cache from "../../../cache.js"; +import { getInternalManager } from "../../../action-utils/action-manager.js"; +import { InteractionHandler } from "../../interactions.js"; + +export default { + execute: async (interaction, args) => { + + // auth through discord + const { token } = await Cache.auth.startRawDiscordAuth(interaction.user); + if (!token) throw "Could not authenticate you with SLMN.GG."; + + const matchID = args[0] as MatchResolvableID; + const action = args[1] as "approve" | "deny"; + + console.log("approval", { matchID, action }, interaction.message); + + const internalManager = getInternalManager(); + if (!internalManager) { + throw "Could not handle this request (no internal system available)"; + } + const managerResponse = await internalManager.runAction("approve-match-reschedule", { matchID: matchID, reaction: action }, token); + console.log(managerResponse); + + if (managerResponse?.error) { + throw managerResponse.error; + } else { + return managerResponse; + } + } +} satisfies InteractionHandler; diff --git a/server/src/discord/interactions/reschedule/staff_approval.ts b/server/src/discord/interactions/reschedule/staff_approval.ts new file mode 100644 index 00000000..32d98e4e --- /dev/null +++ b/server/src/discord/interactions/reschedule/staff_approval.ts @@ -0,0 +1,30 @@ +import { MatchResolvableID } from "../../../types.js"; +import * as Cache from "../../../cache.js"; +import { getInternalManager } from "../../../action-utils/action-manager.js"; +import { InteractionHandler } from "../../interactions.js"; + +export default { + execute: async (interaction, args) => { + // auth through discord + const { token } = await Cache.auth.startRawDiscordAuth(interaction.user); + if (!token) throw "Could not authenticate you with SLMN.GG."; + + const matchID = args[0] as MatchResolvableID; + const action = args[1] as "approve" | "pre-approve" | "force-approve" | "deny" | "delete"; + + console.log("approval", { matchID, action }, interaction.message); + + const internalManager = getInternalManager(); + if (!internalManager) { + throw "Could not handle this request (no internal system available)"; + } + const managerResponse = await internalManager.runAction("staff-approve-match-reschedule", { matchID: matchID, reaction: action }, token); + console.log(managerResponse); + + if (managerResponse?.error) { + throw managerResponse.error; + } else { + return managerResponse; + } + } +} satisfies InteractionHandler; diff --git a/server/src/index.js b/server/src/index.js index 4aaee744..6a4fbad2 100644 --- a/server/src/index.js +++ b/server/src/index.js @@ -61,6 +61,7 @@ const Cache = (await import("./cache.js")).setup(io); actions.load(app, localCors, Cache, io); await import("./discord/slash-commands.js"); +await import("./discord/interactions.ts"); await import("./automation-manager.js"); app.use(express.urlencoded({ extended: true, limit: "50mb" })); diff --git a/server/src/routes.js b/server/src/routes.js index 96fd0d9f..44df7a89 100644 --- a/server/src/routes.js +++ b/server/src/routes.js @@ -441,7 +441,7 @@ export default ({ app, Cache, io }) => { "Channel ID": tokenInfo.userId, "Name": tokenInfo.userName, "Stream Key": streamKey || undefined - }); + }, "routes/twitch_callback"); } else { airtableResponse = await createRecord(Cache, "Channels", [{ @@ -450,7 +450,7 @@ export default ({ app, Cache, io }) => { "Channel ID": tokenInfo.userId, "Name": tokenInfo.userName, "Stream Key": streamKey || undefined - }]); + }], "routes/twitch_callback"); } // console.log(airtableResponse); diff --git a/server/src/types.ts b/server/src/types.ts index 28268503..ac6efe75 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -174,20 +174,58 @@ export interface Report extends Base { approved?: boolean; approved_by_team?: boolean; approved_by_opponent?: boolean; + denied_by_opponent?: boolean; countered_by_opponent?: boolean; approved_by_staff?: boolean; + denied_by_staff?: boolean; force_approved?: boolean; data?: string; countered_data?: string; message_data?: string; log?: string; - type?: "Scores" | "Attributes"; + type?: "Scores" | "Attributes" | "Rescheduling"; match?: MatchResolvableID[]; player?: PlayerResolvableID[]; team?: TeamResolvableID[]; } + +// export type ReschedulingReportMessageTypes = +// "reschedule_opponent_approval" +// | "reschedule_staff_preapproval" +// | "reschedule_staff_approval" +// +// | "reschedule_completed" +// +// | "reschedule_opponent_denial" +// | "reschedule_staff_denial" +// | "reschedule_team_cancel" +// | "reschedule_opponent_cancel" // unlikely to use this + +const ReschedulingReportMessageTypes = [ + "reschedule_opponent_approval", + "reschedule_staff_preapproval", + "reschedule_staff_approval", + "reschedule_completed", + "reschedule_opponent_denial", + "reschedule_staff_denial", + "reschedule_team_cancel", + "reschedule_opponent_cancel", +] as const; + +export type ReschedulingReportKeys = (typeof ReschedulingReportMessageTypes)[number]; + + +const ScoreReportingMessageTypes = [ + "report_staff_notification", + "report_opponent_notification", + "report_completed_public", + "report_completed_staff" +] as const; + +export type ScoreReportingReportKeys = (typeof ScoreReportingMessageTypes)[number]; + interface LogFile extends Base { } @@ -227,8 +265,53 @@ interface AdRead extends Base { interface LiveGuest extends Base { } -interface Theme extends Base { +export interface Theme extends Base { + id: ThemeResolvableID; + __tableName: "Themes"; + + event?: EventResolvableID[]; + events_as_title_sponsor?: EventResolvableID[]; + live_guests?: LiveGuestResolvableID[]; + team?: TeamResolvableID[]; + + team_blue?: TeamResolvableID[]; + team_red?: TeamResolvableID[]; + players?: PlayerResolvableID[]; + name?: string; + description?: string; + desk_colors?: string; + available_for_factboxes?: boolean; + + color_accent?: `#${string}`; + color_alt?: `#${string}`; + color_body?: `#${string}`; + color_dark?: `#${string}`; + color_gradient?: `#${string}`; + color_hero_recolor_primary?: `#${string}`; + color_hero_recolor_secondary?: `#${string}`; + color_logo_accent?: `#${string}`; + color_logo_background?: `#${string}`; + color_navbar?: `#${string}`; + color_text_on_body?: `#${string}`; + color_text_on_dark?: `#${string}`; + color_text_on_logo_background?: `#${string}`; + color_text_on_theme?: `#${string}`; + color_theme?: `#${string}`; + color_theme_on_dark?: `#${string}`; + color_website_active?: `#${string}`; + color_website_passive?: `#${string}`; + + default_logo?: CacheAttachment[]; + default_wordmark?: CacheAttachment[]; + small_logo?: CacheAttachment[]; + + logo_on_dark?: CacheAttachment[]; + logo_on_light?: CacheAttachment[]; + logo_on_theme?: CacheAttachment[]; + wordmark_on_dark?: CacheAttachment[]; + wordmark_on_light?: CacheAttachment[]; + wordmark_on_theme?: CacheAttachment[]; } interface Accolade extends Base { @@ -296,6 +379,7 @@ export interface Match extends Base { placeholder_teams?: string; player_relationships?: PlayerRelationshipResolvableID[]; reports?: ReportResolvableID[]; + report_history?: ReportResolvableID[]; round?: string; schedule_text?: string; scheduled_broadcast?: string; @@ -306,6 +390,8 @@ export interface Match extends Base { show_on_secondary_overlays?: boolean; special_event?: boolean; start?: string; + earliest_start?: string; + latest_start?: string; stats?: string; stream_code?: string; sub_event?: string; @@ -453,9 +539,10 @@ export type AuthUserData = { airtable: Player; } } +export type UserData = AuthUserData["user"]; export type ActionAuth = { - user: AuthUserData["user"]; + user: UserData; client?: Client; isAutomation?: boolean; } @@ -497,6 +584,11 @@ export type EventSettings = { showHeroBans?: boolean; showMapBans?: boolean; allowForfeits?: boolean; + }, + rescheduling: { + use?: boolean; + staffApprove?: boolean; + opponentApprove?: boolean; } }; logging?: { @@ -504,9 +596,12 @@ export type EventSettings = { matchTimeChanges?: Snowflake; postMatchReports?: Snowflake; captainNotifications?: Snowflake; + + staffApprovalNotifications?: Snowflake; staffScoreReport?: Snowflake; staffCompletedScoreReport?: Snowflake; + sendStaffPreapprovalNotifications?: boolean; hideNonStaffRosterChanges?: boolean; } } diff --git a/website/src/components/broadcast/BroadcastMapDisplay.vue b/website/src/components/broadcast/BroadcastMapDisplay.vue index 9eb02cd5..56148c39 100644 --- a/website/src/components/broadcast/BroadcastMapDisplay.vue +++ b/website/src/components/broadcast/BroadcastMapDisplay.vue @@ -52,6 +52,9 @@ export default { audioStatus: "not playing" }), computed: { + /** + * @returns {Match|null} + */ match() { if (this.virtualMatch) return this.virtualMatch; if (!this.broadcast?.live_match) return null; @@ -137,7 +140,7 @@ export default { // maximum: current maps + however many to get a win // or current + 1 if no winner // if (!this.match.maps) return this.match.first_to; - const scores = [this.match.score_1, this.match.score_2].map(s => s || 0); + const scores = [this.match.score_1, this.match.score_2].map(s => s); if (scores.some(s => s === this.match.first_to)) { // match complete diff --git a/website/src/components/broadcast/cams/CamsWrapper.vue b/website/src/components/broadcast/cams/CamsWrapper.vue index 104e3f60..4734cdc5 100644 --- a/website/src/components/broadcast/cams/CamsWrapper.vue +++ b/website/src/components/broadcast/cams/CamsWrapper.vue @@ -42,7 +42,7 @@ export default { }, stringify(obj) { if (!obj) return ""; - return "&" + Object.entries(obj).map(item => item.filter(x => x).join("=")).join("&"); + return "&" + Object.entries(obj).map(item => item.filter(Boolean).join("=")).join("&"); } } }; diff --git a/website/src/components/website/LoggedInUser.vue b/website/src/components/website/LoggedInUser.vue index fa7c3bae..485c4cc6 100644 --- a/website/src/components/website/LoggedInUser.vue +++ b/website/src/components/website/LoggedInUser.vue @@ -20,6 +20,7 @@ Token Re-authenticate + Logout @@ -29,7 +30,7 @@ import { url } from "@/utils/content-utils.js"; import { getMainDomain, isOnMainDomain } from "@/utils/fetch"; import { bg } from "@/utils/images"; import TokenModal from "@/components/website/dashboard/TokenModal.vue"; -import { mapState, mapWritableState } from "pinia"; +import { mapState } from "pinia"; import { useAuthStore } from "@/stores/authStore"; export default { @@ -44,6 +45,9 @@ export default { }, avatar() { return bg(this.user.avatar); + }, + siteMode() { + return import.meta.env.VITE_DEPLOY_MODE || import.meta.env.NODE_ENV; } }, methods: { @@ -58,6 +62,11 @@ export default { }, rootLinkRouter(url) { return isOnMainDomain() ? url : null; + }, + handleLogout() { + const auth = useAuthStore(); + auth.logout(); + this.$router.push("/"); } } diff --git a/website/src/components/website/ReportLog.vue b/website/src/components/website/ReportLog.vue new file mode 100644 index 00000000..df827637 --- /dev/null +++ b/website/src/components/website/ReportLog.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/website/src/components/website/ReportStepsTop.vue b/website/src/components/website/ReportStepsTop.vue new file mode 100644 index 00000000..25af6b97 --- /dev/null +++ b/website/src/components/website/ReportStepsTop.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/website/src/components/website/WebsiteNav.vue b/website/src/components/website/WebsiteNav.vue index c5864c8b..7decf1fe 100644 --- a/website/src/components/website/WebsiteNav.vue +++ b/website/src/components/website/WebsiteNav.vue @@ -19,9 +19,10 @@ Beta development version: things may break. Use slmn.gg for the latest stable update. - + SLMN.GG is running in local development mode but not using a local data server. + {{ reloadAfterRebuild ? 'Page will reload after data server rebuilds' : '' }} @@ -78,6 +79,8 @@ + + Login @@ -89,6 +92,41 @@ + +
+
+ For development use only +
+
Force authentication using an existing auth token from the data server.
+ + +
+ + + Force + +
+
+
+
Data server: {{ getDataServerAddress() }}
+ +
+ Loaded user data +
{{ user || 'no valid data' }}
+
+
+
+

Change your timezone for dates and times across SLMN.GG:

@@ -110,7 +148,7 @@ import WebsiteNavBanner from "@/components/website/WebsiteNavBanner"; import { resizedImageNoWrap } from "@/utils/images"; import LoggedInUser from "@/components/website/LoggedInUser"; import TimezoneSwapper from "@/components/website/schedule/TimezoneSwapper"; -import { getMainDomain } from "@/utils/fetch"; +import { getDataServerAddress, getMainDomain } from "@/utils/fetch"; import { mapState, mapWritableState } from "pinia"; import { useAuthStore } from "@/stores/authStore"; @@ -127,7 +165,9 @@ export default { data: () => ({ pageNoLongerNew: false, resizeObserver: null, - height: 0 + height: 0, + reloadAfterRebuild: false, + forceTokenInput: null }), computed: { ...mapWritableState(useAuthStore, ["user"]), @@ -209,11 +249,33 @@ export default { } }, methods: { + getDataServerAddress, slmnggURL(page) { return `${this.slmnggDomain}/${page}`; }, onResize() { this.height = this.$el.offsetHeight; + }, + async forceToken(token) { + if (!token) { + this.$notyf.error("No token supplied. You can force a logout from the user dropdown."); + return; + } + const auth = useAuthStore(); + const response = await auth.authenticateWithToken(token); + console.log(response); + if (response.error) { + this.$notyf.error(response.errorMessage); + } else { + this.$notyf.success("New token authenticated and stored"); + } + } + }, + watch: { + isRebuilding(rebuilding) { + if (!rebuilding && this.reloadAfterRebuild) { + document.location.reload(); + } } }, mounted() { @@ -287,4 +349,8 @@ export default { .toggler.navbar-toggler { --bs-navbar-toggler-border-color: rgba(255,255,255,0.15); } +.fake-pw { + text-security: disc; + -webkit-text-security: disc; +} diff --git a/website/src/components/website/dashboard/AdvancedDateEditor.vue b/website/src/components/website/dashboard/AdvancedDateEditor.vue index 88e329f9..e4e0411b 100644 --- a/website/src/components/website/dashboard/AdvancedDateEditor.vue +++ b/website/src/components/website/dashboard/AdvancedDateEditor.vue @@ -1,14 +1,14 @@ @@ -47,4 +59,10 @@ export default { text-transform: uppercase; line-height: 1; } + .badge { + font-weight: bold; + font-size: .7em; + text-transform: uppercase; + padding: 0.25em 0.5em; + } diff --git a/website/src/router/shared-routes.js b/website/src/router/shared-routes.js index caf9bfbf..3593bd1d 100644 --- a/website/src/router/shared-routes.js +++ b/website/src/router/shared-routes.js @@ -77,6 +77,7 @@ export default [ { path: "", component: () => import("@/views/sub-views/MatchVOD.vue") }, { path: "history", component: () => import("@/views/sub-views/MatchStats.vue") }, { path: "score-reporting", component: () => import("@/views/sub-views/MatchScoreReporting.vue"), meta: { requiresAuth: true } }, + { path: "rescheduling", component: () => import("@/views/sub-views/MatchRescheduling.vue"), meta: { requiresAuth: true } }, { path: "editor", component: () => import("@/views/sub-views/event/EventMatchEditor.vue"), meta: { requiresAuth: true } } ] }, diff --git a/website/src/socket.js b/website/src/socket.js index 743d3bd2..c081f0ee 100644 --- a/website/src/socket.js +++ b/website/src/socket.js @@ -11,7 +11,9 @@ export const socket = io(getDataServerAddress(), { transports: ["websocket", "po socket.on("connect", () => { state.connected = true; - socket.emit("subscribe-multiple", store.state.subscribed_ids); + if (store.state.subscribed_ids?.length) { + socket.emit("subscribe-multiple", store.state.subscribed_ids); + } }); socket.on("disconnect", () => { diff --git a/website/src/stores/authStore.ts b/website/src/stores/authStore.ts index ae5ea5ed..a4eeab87 100644 --- a/website/src/stores/authStore.ts +++ b/website/src/stores/authStore.ts @@ -165,6 +165,12 @@ export const useAuthStore = defineStore("auth", () => { }; } + function logout() { + token.value = null; + user.value = null; + authNext.value = null; + } + return { token, authNext, @@ -178,7 +184,8 @@ export const useAuthStore = defineStore("auth", () => { authenticateWithDiscord, setAuthNext, getAuthNext, - authenticateWithToken + authenticateWithToken, + logout }; }, { persist: { diff --git a/website/src/utils/content-utils.js b/website/src/utils/content-utils.js index 9b598758..c45703e9 100644 --- a/website/src/utils/content-utils.js +++ b/website/src/utils/content-utils.js @@ -119,7 +119,7 @@ export function multiImage(theme, keys, minSize = 30, useResizer = true) { } export function getMatchContext(match, { light } = {}) { - let pieces = []; + let pieces; if (light) { pieces = [match?.sub_event].filter(Boolean); } else { @@ -672,18 +672,30 @@ export function decoratePlayerWithDraftData(player, eventID) { }; } - -export function getScoreReportingBadge(state, report, eventSettings) { +/** + * + * @param {Object} state - The state object + * @param {boolean} state.reports_enabled - Indicates if reports are enabled + * @param {boolean} state.has_existing_report - Indicates if there is an existing report + * @param {boolean} state.is_on_teams - Indicates if the user is on teams + * @param {boolean} state.is_opponent - Indicates if the user is an opponent + * @param {boolean} state.is_submitter - Indicates if the user is a submitter + * @param {boolean} state.is_staff - Indicates if the user is staff + * @param {Object} report - The report object + * @param {Object} eventSettings - The event settings object + * @returns {{small: string, variant: string, text: string, title: string}|{variant: string, text: string}|null} + */ +export function getScoreReportingBadge({ state, report }, eventSettings) { if (!state.reports_enabled) return null; - if (state.is_complete) return null; + if (report?.approved || state.match_complete) return null; - if (state.existing_report && report.approved) { + if (state.has_existing_report && report.approved) { return { variant: "success", text: "Complete" }; } - if (state.existing_report && report.approved_by_opponent && eventSettings?.reporting?.score?.staffApprove) { + if (state.has_existing_report && report.approved_by_opponent && eventSettings?.reporting?.score?.staffApprove) { // Waiting for staff approval if (state.is_staff) { return { @@ -701,7 +713,7 @@ export function getScoreReportingBadge(state, report, eventSettings) { }; } } - if (state.existing_report && report.approved_by_team && eventSettings?.reporting?.score?.opponentApprove) { + if (state.has_existing_report && report.approved_by_team && eventSettings?.reporting?.score?.opponentApprove) { // Waiting for opponent approval if (state.is_opponent) { return { @@ -716,7 +728,7 @@ export function getScoreReportingBadge(state, report, eventSettings) { return { variant: "success", text: "Pre-approve", - small: "Pre-app", + small: "Pre-approve", title: "Pre-approval ready" }; } else { @@ -738,7 +750,7 @@ export function getScoreReportingBadge(state, report, eventSettings) { } } - if (!state.existing_report && state.is_on_teams) { + if (!state.has_existing_report && state.is_on_teams) { return { variant: "primary", text: "Available", @@ -749,6 +761,126 @@ export function getScoreReportingBadge(state, report, eventSettings) { return null; } + + +/** + * + * @param {Object} state - The state object + * @param {boolean} state.reports_enabled - Indicates if reports are enabled + * @param {boolean} state.has_existing_report - Indicates if there is an existing report + * @param {boolean} state.is_on_teams - Indicates if the user is on teams + * @param {boolean} state.is_opponent - Indicates if the user is an opponent + * @param {boolean} state.is_submitter - Indicates if the user is a submitter + * @param {boolean} state.is_staff - Indicates if the user is staff + * @param {boolean} state.reports_loading + * @param {boolean} state.has_start + * @param {boolean} state.match_complete + * @param {Report} report - The report object + * @param {Object} eventSettings - The event settings object + * @returns {{small: string, variant: string, text: string, title: string}|{variant: string, text: string}|null} + */ +export function getReschedulingBadge({ state, report }, eventSettings) { + console.log("rescheduling", { state, report }); + if (state.reports_loading) return null; + if (!state.reports_enabled) return null; + if (!(state.is_on_teams || state.is_staff)) return null; + if (report?.approved || state.match_complete) return null; + + const reschedule = state.has_start ? "Reschedule" : "Schedule"; + + console.log("existing report", state.has_existing_report, "is staff", state.is_staff); + if (!state.has_existing_report && !state.is_staff) { + // no report but could make one + console.log("could make", JSON.stringify(report), state.has_existing_report); + return { + variant: "primary", + text: "Available", + small: reschedule, + title: `You can submit a ${reschedule.toLowerCase()} request for this match` + }; + } + + if (state.has_existing_report) { + // some sort of state + + if (report.denied_by_staff) { + return { + variant: "danger", + text: "Denied", + small: "Denied", + title: `Staff have denied this ${reschedule.toLowerCase()} request` + }; + } + if (report.denied_by_opponent) { + return { + variant: "danger", + text: "Denied", + small: "Denied", + title: `${reschedule} request was denied by ${state.is_opponent ? "you" : "opponent"}` + }; + } + + if (report.approved_by_team) { + if (eventSettings?.reporting?.rescheduling?.opponentApprove) { + if (!report.approved_by_opponent) { + if (state.is_submitter) { + return { + variant: "dark", + text: "Submitted", + small: "Submitted", + title: `Waiting for opponent to approve this ${reschedule.toLowerCase()} request` + }; + } else if (state.is_opponent) { + return { + variant: "success", + text: "Needs approval", + small: "Needs approval", + title: `You can approve this ${reschedule.toLowerCase()} request` + }; + } else if (state.is_staff) { + if (eventSettings?.reporting?.rescheduling?.staffApprove) { + return { + variant: "primary", + text: "Pre-approve", + small: "Pre-app", + title: "Pre-approval ready" + }; + } else { + return { + variant: "dark", + text: "Needs opponent", + small: "Need opp", + title: `Waiting for opponent to approve this ${reschedule.toLowerCase()} request` + }; + } + } + } + } + + if (eventSettings?.reporting?.rescheduling?.staffApprove) { + if (!report.approved_by_staff) { + if (state.is_staff) { + return { + variant: "success", + text: "Needs approval", + small: "Needs approval", + title: `You can approve this ${reschedule.toLowerCase()} request` + }; + } else if (state.is_on_teams) { + return { + variant: "dark", + text: "Submitted", + small: "Waiting", + title: "Waiting for staff approval" + }; + } + } + } + } + } +} + + /** * @param {number} r * @param {number} g diff --git a/website/src/utils/dashboard.ts b/website/src/utils/dashboard.ts index d987ce2c..ee87a7af 100644 --- a/website/src/utils/dashboard.ts +++ b/website/src/utils/dashboard.ts @@ -273,7 +273,11 @@ export async function authenticatedRequest(url: U, data?: }, body: JSON.stringify(data ?? {}) }).then(async res => await res.json()).catch((error: Error) => { - notyf.error({ message: `Request error: ${error.message}` }); + if (error.message === "Failed to fetch") { + notyf.error({ message: "Request failed: connection error" }); + } else { + notyf.error({ message: `Request error: ${error.message}` }); + } console.error("Fetch error", error); }); console.log(request); diff --git a/website/src/views/BracketCreator.vue b/website/src/views/BracketCreator.vue index 72d8c5ab..3aed424a 100644 --- a/website/src/views/BracketCreator.vue +++ b/website/src/views/BracketCreator.vue @@ -101,7 +101,7 @@ class="connection-button" :class="{ 'active': activeConnectionMatches(bi, ci, mi, 'win') || getConnection(bi, ci, mi, 'win'), - 'champion':getConnection(bi, ci, mi, 'win') === 'champion', + 'champion':getConnection(bi, ci, mi, 'win') === 'champion' }" @mouseenter="showConnection(bi, ci, mi, 'win')" @mouseleave="hideConnection()" @@ -113,7 +113,7 @@ class="connection-button" :class="{ 'active': activeConnectionMatches(bi, ci, mi, 'lose') || getConnection(bi, ci, mi, 'lose'), - 'eliminated':getConnection(bi, ci, mi, 'lose') === 'eliminated', + 'eliminated':getConnection(bi, ci, mi, 'lose') === 'eliminated' }" @mouseenter="showConnection(bi, ci, mi, 'lose')" @mouseleave="hideConnection()" diff --git a/website/src/views/Match.vue b/website/src/views/Match.vue index e681dc46..5b044be8 100644 --- a/website/src/views/Match.vue +++ b/website/src/views/Match.vue @@ -4,7 +4,7 @@
-
+
@@ -24,6 +24,24 @@
+
+ + + +
+
+
+ + +
+
{{ match.start ? 'Rescheduling' : 'Scheduling' }}
+ +
+
+ {{ reschedulingBadge.text }} +
+ +
@@ -80,7 +98,13 @@ import { ReactiveArray, ReactiveRoot, ReactiveThing } from "@/utils/reactive"; import MatchHero from "@/components/website/match/MatchHero"; import MatchScore from "@/components/website/match/MatchScore"; import LinkedPlayers from "@/components/website/LinkedPlayers"; -import { cleanID, formatTime, getMatchContext, getScoreReportingBadge, url } from "@/utils/content-utils"; +import { + cleanID, + formatTime, + getMatchContext, + getReschedulingBadge, getScoreReportingBadge, + url +} from "@/utils/content-utils"; import { resizedImageNoWrap } from "@/utils/images"; import { canEditMatch, isEventStaffOrHasRole } from "@/utils/client-action-permissions"; import { useSettingsStore } from "@/stores/settingsStore"; @@ -108,6 +132,7 @@ export default { event: ReactiveThing("event", { theme: ReactiveThing("theme"), player_relationships: ReactiveArray("player_relationships"), + "staff": ReactiveArray("staff"), broadcasts: ReactiveArray("broadcasts") }), casters: ReactiveArray("casters"), @@ -191,6 +216,9 @@ export default { scoreReportingEnabled() { return this.eventSettings?.reporting?.score?.use; }, + reschedulingEnabled() { + return this.eventSettings?.reporting?.rescheduling?.use && this.match?.earliest_start && this.match?.latest_start; + }, authenticated() { const { isAuthenticated } = useAuthStore(); return isAuthenticated; @@ -204,7 +232,8 @@ export default { if (this.showHeadToHead) items.push("head-to-head"); if (this.showEditor) items.push("editor"); - if ((this.scoreReportingEnabled && !this.matchComplete) || (this.$route?.path?.endsWith("/score-reporting"))) items.push("score-reporting"); + if ((this.scoreReportingEnabled) || (this.$route?.path?.endsWith("/score-reporting"))) items.push("score-reporting"); + if ((this.reschedulingEnabled) || (this.$route?.path?.endsWith("/rescheduling"))) items.push("rescheduling"); return items; }, @@ -227,33 +256,65 @@ export default { ...team.owners || [], ].some(personID => cleanID(player?.id) === cleanID(personID))); }, - existingScoreReport() { - return (ReactiveRoot(this.match?.id, { + scoreReportingBadge() { + const { isAuthenticated, user } = useAuthStore(); + if (!isAuthenticated) return null; + if (!(this.controllableTeams?.length || isEventStaffOrHasRole(user, { event: this.match?.event, websiteRoles: ["Can edit any match", "Can edit any event"] }))) return null; + + const reports = (ReactiveRoot(this.match?.id, { "reports": ReactiveArray("reports", { "team": ReactiveThing("team"), "player": ReactiveThing("player") }) - })?.reports || []).find(report => report.type === "Scores" && cleanID(report.match?.[0]) === cleanID(this.match?.id)); + })?.reports || []); + + return getScoreReportingBadge(this.scoreReportState(reports, "Scores"), this.eventSettings); }, - scoreReportingBadge() { - const { user } = useAuthStore(); - const state = { - "reports_enabled": this.scoreReportingEnabled, - "existing_report": !!this.existingScoreReport?.id, - "is_on_teams": !!this.controllableTeams?.length, - "is_opponent": this.existingScoreReport?.team?.id ? this.controllableTeams.some(t => cleanID(t.id) !== cleanID(this.existingScoreReport?.team?.id)) : null, - "is_submitter": this.existingScoreReport?.team?.id ? this.controllableTeams.some(t => cleanID(t.id) === cleanID(this.existingScoreReport?.team?.id)) : null, - "is_staff": isEventStaffOrHasRole(user, { event: this.match?.event, websiteRoles: ["Can edit any match", "Can edit any event"] }) - }; + reschedulingBadge() { + const { isAuthenticated, user } = useAuthStore(); + if (!isAuthenticated) return null; + if (!(this.controllableTeams?.length || isEventStaffOrHasRole(user, { event: this.match?.event, websiteRoles: ["Can edit any match", "Can edit any event"] }))) return null; - return getScoreReportingBadge(state, this.existingScoreReport, this.eventSettings); - } + const reports = (ReactiveRoot(this.match?.id, { + "reports": ReactiveArray("reports", { + "team": ReactiveThing("team"), + "player": ReactiveThing("player") + }) + })?.reports || []); + return getReschedulingBadge(this.scoreReportState(reports, "Rescheduling"), this.eventSettings); + }, }, methods: { subLink(subLinkURL) { return `/match/${this.match.id}/${subLinkURL}`; }, - url + url, + + scoreReportState(reports, type) { + const { isAuthenticated, user } = useAuthStore(); + if (!isAuthenticated) return null; + + const report = reports.find(report => report.type === type && cleanID(report.match?.[0]) === cleanID(this.match?.id)); + let loading = false; + if (reports.some(r => r.__loading || !r.id)) loading = true; + + console.log("score report state", report); + return { + report, + state: { + "reports_enabled": this.scoreReportingEnabled, + "has_existing_report": report?.id, + "reports_loading": loading, + "is_complete": this.match.first_to && [this.match.score_1, this.match.score_2].some(s => s === this.match.first_to), + "is_on_teams": !!this.controllableTeams?.length, + "is_opponent": report?.team?.id ? this.controllableTeams.some(t => cleanID(t.id) !== cleanID(report?.team?.id)) : null, + "is_submitter": report?.team?.id ? this.controllableTeams.some(t => cleanID(t.id) === cleanID(report?.team?.id)) : null, + "is_staff": isEventStaffOrHasRole(user, { event: this.match?.event, websiteRoles: ["Can edit any match", "Can edit any event"] }), + "settings": this.eventSettings, + "match_complete": [this.match.score_1 || 0, this.match.score_2 || 0].some(score => this.match.first_to === score) + } + }; + }, }, watch: { eventID: { diff --git a/website/src/views/sub-views/MatchRescheduling.vue b/website/src/views/sub-views/MatchRescheduling.vue new file mode 100644 index 00000000..e0c3d697 --- /dev/null +++ b/website/src/views/sub-views/MatchRescheduling.vue @@ -0,0 +1,677 @@ + + + + + diff --git a/website/src/views/sub-views/MatchScoreReporting.vue b/website/src/views/sub-views/MatchScoreReporting.vue index 6f370714..892c8e6a 100644 --- a/website/src/views/sub-views/MatchScoreReporting.vue +++ b/website/src/views/sub-views/MatchScoreReporting.vue @@ -1,213 +1,302 @@ @@ -216,11 +305,13 @@ import MatchEditor from "@/components/website/dashboard/MatchEditor.vue"; import { useAuthStore } from "@/stores/authStore"; -import { canEditMatch, isEventStaffOrHasRole } from "@/utils/client-action-permissions"; +import { isEventStaffOrHasRole } from "@/utils/client-action-permissions"; import { ReactiveArray, ReactiveRoot, ReactiveThing } from "@/utils/reactive"; import { cleanID, url } from "@/utils/content-utils"; import MatchExplainerList from "@/components/website/dashboard/MatchExplainerList.vue"; import { authenticatedRequest } from "@/utils/dashboard.ts"; +import ReportStepsTop from "@/components/website/ReportStepsTop.vue"; +import ReportLog from "@/components/website/ReportLog.vue"; /** * @typedef {object} Report @@ -238,10 +329,10 @@ import { authenticatedRequest } from "@/utils/dashboard.ts"; export default { name: "MatchScoreReporting", - components: { MatchExplainerList, MatchEditor }, + components: { ReportLog, ReportStepsTop, MatchExplainerList, MatchEditor }, props: ["match"], data: () => ({ - processing: {}, + processing: false, denyEditor: false }), computed: { @@ -299,7 +390,7 @@ export default { return this.controllableTeams.some(t => cleanID(t.id) !== cleanID(this.existingScoreReport?.team?.id)); }, opponentTeam() { - return this.controllableTeams.find(t => cleanID(t.id) !== cleanID(this.existingScoreReport?.team?.id)); + return this.teams.find(t => cleanID(t.id) !== cleanID(this.existingScoreReport?.team?.id)); }, controllableTeams() { const { isAuthenticated, player } = useAuthStore(); @@ -324,7 +415,52 @@ export default { if (editorPerm) status.staff = true; return status; }, + currentAction() { + if (!this.currentStep) return null; + let action; + if (this.authStatus?.team && !this.isOpponent) { + action = this.currentStep?.actions?.submitter; + } else if (this.authStatus?.team && this.isOpponent) { + action = this.currentStep?.actions?.opponent; + } + if (!action && this.authStatus?.team && this.currentStep?.actions?.teams) { + action = this.currentStep?.actions?.teams; + } + if (!action && this.authStatus?.staff) { + action = this.currentStep?.actions?.staff; + } + return action; + }, + approvalWarning() { + const types = {}; + if (this.eventSettings?.reporting?.score?.opponentApprove) { + types.opponent = true; + } + if (this.eventSettings?.reporting?.score?.staffApprove) { + types.staff = true; + } + if (this.existingScoreReport?.approved_by_opponent || this.existingScoreReport?.countered_by_opponent) { + types.opponent = false; + } + if (this.existingScoreReport?.approved_by_staff) { + types.staff = false; + } + return { + short: types.opponent && types.staff ? "opponent and staff" : (types.opponent ? "opponent" : (types.staff ? "staff" : null)), + people: types.opponent && types.staff ? "an opponent and a staff member" : (types.opponent ? "an opponent" : (types.staff ? "a staff member" : null)), + types: Object.entries(types).filter(([key, active]) => active).map(([key]) => key) + }; + }, steps() { + const warningFooter = this.approvalWarning?.short ? [{ + icon: "fas fa-exclamation-circle", + heading: `This event requires ${this.approvalWarning.short} approval`, + description: `The score report will not be confirmed until ${this.approvalWarning.people} ${this.approvalWarning.types.length === 1 ? "has" : "have"} approved the request.` + }] : (this.needsNoApproval ? [{ + icon: "fas fa-check", + heading: "This event does not require any approval for score reporting", + }] : []); + const steps = [ { key: "report", @@ -332,7 +468,27 @@ export default { title: "Score report", description: "Team submits the maps and scores of the match", status: "inactive", - icon: "fas fa-clipboard-list" + icon: "fas fa-clipboard-list", + actions: { + submitter: { + title: { + text: "Submit a score report", + variant: "primary" + }, + content: ["match-editor"], + footer: warningFooter, + buttons: [ + { click: () => { this.$refs["main-editor"].scoreReportConfirmModal = true; }, style: { variant: "success", icon: "fas fa-cloud-upload", text: "Submit report" } }, + ] + }, + staff: { + title: { + text: "No score reported", + variant: "secondary" + }, + content: ["staff editor link"] + }, + } }, ]; @@ -351,7 +507,109 @@ export default { title: "Opponent approval", description: "Opposing team approves the report", status: "inactive", - icon: "fas fa-clipboard-list-check" + icon: "fas fa-clipboard-list-check", + actions: { + submitter: { + title: { text: "Score report submitted" }, + content: ["proposed-report"], + footer: [{ heading: "Waiting for an opponent's response", icon: "fas fa-clock" }, ...warningFooter].filter(Boolean) + }, + opponent: { + title: { text: "Approve or deny score report", variant: "primary" }, + content: ["proposed-report", "deny-editor"], + footer: warningFooter, + buttons: this.denyEditor ? [ + { click: () => { this.$refs["deny-editor"].scoreReportConfirmModal = true; }, style: { variant: "success", icon: "fas fa-check", text: "Submit counter report" } }, + { click: () => { this.denyEditor = false; }, style: { variant: "secondary", icon: "fas fa-times", text: "Cancel counter edit" } } + ] : [ + { disabled: this.denyEditor, reaction: "approve", action: "approve-score-report", style: { variant: "success", icon: "fas fa-check", text: "Approve" } }, + { click: () => { this.denyEditor = true; }, style: { variant: "danger", icon: "fas fa-times", text: "Deny & counter" } } + ] + }, + staff: (this.existingScoreReport?.approved_by_staff && !this.existingScoreReport?.denied_by_staff) + ? { + title: { + text: "Score report pre-approved", + variant: "success" + }, + content: ["proposed-report", "log"], + buttons: [ + { + reaction: "force-approve", + action: "staff-approve-score-report", + style: { + variant: "primary", + icon: "fas fa-shield-check", + text: "Force approve" + }, + successToast: "Score report force approved" + }, + ...this.eventSettings?.reporting?.score?.staffApprove ? [{ + reaction: "pre-approve", + action: "staff-approve-score-report", + disabled: true, + successToast: "Score report pre-approved", + style: { + variant: "success", + icon: "fas fa-check", + text: "Pre-approve" + } + }] : [], + { + reaction: "deny", + action: "staff-approve-score-report", + style: { + variant: "danger", + icon: "fas fa-times", + text: "Deny" + }, + successToast: "Score report denied" + }, + // { reaction: "delete", action: "staff-approve-score-report", style: { variant: "danger", icon: "fas fa-trash", text: "Delete" }, successToast: `Score report deleted` }, + ], + footer: [{ heading: "Waiting for opponent approval", icon: "fas fa-clock" }] + } + : { + title: { + text: this.eventSettings?.reporting?.score?.staffApprove ? "Pre-approve score report" : "Score report waiting for opponent approval", + variant: "primary" + }, + content: ["information", "proposed-report"], + buttons: [ + { + reaction: "force-approve", + action: "staff-approve-score-report", + style: { + variant: "primary", + icon: "fas fa-shield-check", + text: "Force approve" + }, + successToast: "Score report force approved" + }, + ...this.eventSettings?.reporting?.score?.staffApprove ? [{ + reaction: "pre-approve", + action: "staff-approve-score-report", + successToast: "Score report pre-approved", + style: { + variant: "success", + icon: "fas fa-check", + text: "Pre-approve" + } + }] : [], + { + reaction: "deny", + action: "staff-approve-score-report", + style: { + variant: "danger", + icon: "fas fa-times", + text: "Deny" + }, + successToast: "Score report denied & locked" + }, + ], + footer: warningFooter + }, + } }; if (this.existingScoreReport?.approved_by_opponent) { @@ -369,16 +627,106 @@ export default { title: "Opponent approval", description: "Opponent approvals not needed on this match", status: "disabled", - icon: "fas fa-check" + icon: "fas fa-check", + actions: { + teams: { + title: { text: "Waiting for staff approval" }, + content: ["proposed-report"], + footer: warningFooter + }, + staff: (this.existingScoreReport?.approved_by_staff && !this.existingScoreReport?.denied_by_staff) + ? { + title: { + text: "Score report pre-approved", + variant: "success" + }, + content: ["proposed-report", "log"], + buttons: [ + { + reaction: "force-approve", + action: "staff-approve-score-report", + style: { + variant: "primary", + icon: "fas fa-shield-check", + text: "Force approve" + }, + successToast: "Score report force approved" + }, + ...this.eventSettings?.reporting?.score?.staffApprove ? [{ + reaction: "pre-approve", + action: "staff-approve-score-report", + disabled: true, + successToast: "Score report pre-approved", + style: { + variant: "success", + icon: "fas fa-check", + text: "Pre-approve" + } + }] : [], + { + reaction: "deny", + action: "staff-approve-score-report", + style: { + variant: "danger", + icon: "fas fa-times", + text: "Deny" + }, + successToast: "Score report denied" + }, + // { reaction: "delete", action: "staff-approve-score-report", style: { variant: "danger", icon: "fas fa-trash", text: "Delete" }, successToast: `Score report deleted` }, + ], + footer: [{ heading: "Waiting for opponent approval", icon: "fas fa-clock" }] + } + : { + title: { + text: this.eventSettings?.reporting?.score?.staffApprove ? "Pre-approve score report" : "Score report waiting for opponent approval", + variant: "primary" + }, + content: ["information", "proposed-report"], + buttons: [ + { + reaction: "force-approve", + action: "staff-approve-score-report", + style: { + variant: "primary", + icon: "fas fa-shield-check", + text: "Force approve" + }, + successToast: "Score report force approved" + }, + ...this.eventSettings?.reporting?.score?.staffApprove ? [{ + reaction: "pre-approve", + action: "staff-approve-score-report", + successToast: "Score report pre-approved", + style: { + variant: "success", + icon: "fas fa-check", + text: "Pre-approve" + } + }] : [], + { + reaction: "deny", + action: "staff-approve-score-report", + style: { + variant: "danger", + icon: "fas fa-times", + text: "Deny" + }, + successToast: "Score report denied & locked" + }, + ], + footer: this.eventSettings?.reporting?.score?.staffApprove ? + [{ heading: "This event requires staff approval", icon: "fas fa-exclamation-circle" }] : + [{ heading: "This event does not require staff approval", icon: "fas fa-check" }] + }, + } }); } if (this.existingScoreReport?.countered_by_opponent) { steps[steps.length-1].status = "countered"; steps[steps.length-1].icon = "fas fa-exchange"; - if (this.opponentTeam?.name) { - steps[steps.length-1].description = `${this.opponentTeam.name} submitted a counter report`; - } + steps[steps.length-1].description = `${this.opponentTeam?.name || "Opponent"} submitted a counter report`; steps.push( { key: "counterReportApprove", @@ -386,7 +734,47 @@ export default { title: "Counter approval", description: `${this.existingScoreReport?.team?.name || "Original team"} approves counter report`, status: "inactive", - icon: "fas fa-clipboard-list" + icon: "fas fa-clipboard-list", + actions: { + submitter: { + title: { text: "Approve or deny counter report", variant: "primary" }, + content: ["proposed-report", "proposed-counter-report"], + footer: [...warningFooter].filter(Boolean), + buttons: [ + { reaction: "counter-approve", action: "approve-score-report", style: { variant: "success", icon: "fas fa-check", text: "Approve counter" } }, + { reaction: "counter-deny", action: "approve-score-report", style: { variant: "danger", icon: "fas fa-times", text: "Deny counter" } } + ] + }, + opponent: { + title: { text: "Counter report submitted" }, + content: ["proposed-report", "proposed-counter-report"], + footer: [{ heading: "Waiting for an opponent's response", icon: "fas fa-clock" }, ...warningFooter].filter(Boolean) + }, + staff: { + content: ["proposed-report", "proposed-counter-report"], + buttons: [ + { reaction: "force-approve", action: "staff-approve-score-report", style: { variant: "primary", icon: "fas fa-shield-check", text: "Force-approve original" }, tooltip: "Force-approve the original report" }, + { reaction: "pre-approve", action: "staff-approve-score-report", style: { variant: "success", icon: "fas fa-check", text: "Pre-approve" }, tooltip: "Auto approve this report once the counter report is approved by both teams" }, + { reaction: "deny", action: "staff-approve-score-report", style: { variant: "danger", icon: "fas fa-times", text: "Deny" }, tooltip: "Force deny this report" }, + { reaction: "force-counter-approve", action: "staff-approve-score-report", style: { variant: "primary", icon: "fas fa-shield-check", text: "Force-approve counter" }, tooltip: "Force-approve the counter report without waiting for team approval" }, + ] + } + // staff: this.existingScoreReport?.approved_by_staff && !this.existingScoreReport?.denied_by_staff ? + // { + // title: { text: "Report pre-approved", variant: "success" }, + // content: ["proposed-report", "proposed-counter-report"], + // + // } : (this.eventSettings?.reporting?.score?.staffApprove ? + // { + // title: { text: "Pre-approve report", variant: "primary" }, + // content: ["proposed-report", "proposed-counter-report"], + // + // } : { + // title: { text: "Counter report submitted" }, + // content: ["proposed-report", "proposed-counter-report"], + // + // }) + } } ); @@ -399,14 +787,41 @@ export default { title: "Staff approval", description: "Staff member approves the report", status: "inactive", - icon: "fas fa-clipboard-check" + icon: "fas fa-clipboard-check", + actions: { + teams: { + title: { text: "Waiting for staff approval" }, + content: ["proposed-report"], + footer: warningFooter + }, + staff: { + title: { text: "Approve or deny score report" }, + content: ["proposed-report"], + footer: warningFooter, + buttons: [ + { + reaction: "approve", + action: "staff-approve-score-report", + style: { + variant: "success", + icon: "fas fa-check", + text: "Approve" + } + }, + { + reaction: "delete", + action: "staff-approve-score-report", + style: { + variant: "danger", + icon: "fas fa-times", + text: "Deny" + } + } + ] + } + } }; - - if (this.existingScoreReport?.force_approved) { - newStep.status = "complete"; - newStep.icon = "fas fa-shield-check"; - newStep.description = "Staff member force approved this report"; - } else if (this.existingScoreReport?.approved_by_staff) { + if (this.existingScoreReport?.approved_by_staff) { newStep.status = "complete"; newStep.icon = "fas fa-check"; } @@ -422,6 +837,12 @@ export default { }); } + if (this.existingScoreReport?.force_approved) { + steps[steps.length - 1].status = "complete"; + steps[steps.length - 1].icon = "fas fa-shield-check"; + steps[steps.length - 1].description = "Staff member force approved this report"; + } + let firstAvailable = steps.findIndex(s => s.status === "inactive"); if (firstAvailable !== -1) { steps[firstAvailable].status = "active"; @@ -435,8 +856,25 @@ export default { }, methods: { url, + async actionButtonPress(button) { + this.processing = true; + try { + const response = await authenticatedRequest(`actions/${button.action}`, { + matchID: this.match.id, + reaction: button.reaction + }); + + if (!response.error) { + this.$notyf.success(`Success: ${button.successToast || button.style.text.toLowerCase()}`); + } + + } finally { + this.processing = false; + this.proposedTime = null; + } + }, async approveReport(reaction) { - this.processing.approval = true; + this.processing = true; try { const response = await authenticatedRequest("actions/approve-score-report", { matchID: this.match.id, @@ -448,11 +886,11 @@ export default { } } finally { - this.processing.approval = false; + this.processing = false; } }, async staffApproveReport(reaction) { - this.processing.approval = true; + this.processing = true; try { const response = await authenticatedRequest("actions/staff-approve-score-report", { matchID: this.match.id, @@ -464,7 +902,7 @@ export default { } } finally { - this.processing.approval = false; + this.processing = false; } } }, @@ -472,6 +910,8 @@ export default { diff --git a/website/src/views/sub-views/ThingTheme.vue b/website/src/views/sub-views/ThingTheme.vue index a33a6b12..76b0f8d4 100644 --- a/website/src/views/sub-views/ThingTheme.vue +++ b/website/src/views/sub-views/ThingTheme.vue @@ -15,11 +15,11 @@
Theme
- +
Logo Background
- +
@@ -38,7 +38,7 @@
- +
{{ thing?.name }}
{{ thing?.name }}
@@ -61,7 +61,7 @@
{{ safeColor(text.value) }}
- +
@@ -135,6 +135,7 @@ import CopyTextButton from "@/components/website/CopyTextButton"; import { calculateContrastHex, url } from "@/utils/content-utils"; import { mapWritableState } from "pinia"; import { useSettingsStore } from "@/stores/settingsStore"; +import ContrastBadge from "@/components/website/ContrastBadge.vue"; function cleanKey(key) { return key.replace(/_/g, " "); @@ -142,7 +143,7 @@ function cleanKey(key) { export default { name: "ThingTheme", - components: { CopyTextButton, /* HeroColorControls, RecoloredHero, */ BracketTeam, IngameTeam, ContentRow, ContentThing, StandingsTeam }, + components: { ContrastBadge, CopyTextButton, /* HeroColorControls, RecoloredHero, */ BracketTeam, IngameTeam, ContentRow, ContentThing, StandingsTeam }, props: ["team", "event"], computed: { ...mapWritableState(useSettingsStore, ["removeHashInHex"]), diff --git a/website/src/views/sub-views/event-settings/EventSettingsDiscordLogging.vue b/website/src/views/sub-views/event-settings/EventSettingsDiscordLogging.vue index e7cc4d6d..5764824e 100644 --- a/website/src/views/sub-views/event-settings/EventSettingsDiscordLogging.vue +++ b/website/src/views/sub-views/event-settings/EventSettingsDiscordLogging.vue @@ -4,7 +4,7 @@
@@ -17,86 +17,118 @@
-
-

Logging

-
-
- - - - - - - - - + +
+
+

Logging & notification channels

+ + + Save logging channels + +
+ +
+

Public

+ +
+
+ + + + + + + + + + + + +
-
-
-

Score reporting

-
-
- - - - - - + +
+

Staff

+
+
+ + + + + + + + + + + + +
-
- - - - - - +
+
+

Captains

+
+
+ + + +
- - -
- - - Save logging channels - -
@@ -122,9 +154,12 @@ export default { postMatchReports: null, captainNotifications: null, + staffApprovalNotifications: null, + staffScoreReport: null, staffCompletedScoreReport: null, + sendStaffPreapprovalNotifications: false, hideNonStaffRosterChanges: false } }), @@ -278,5 +313,8 @@ export default { diff --git a/website/src/views/sub-views/event-settings/editor/ReportingEventSettingsGroup.vue b/website/src/views/sub-views/event-settings/editor/ReportingEventSettingsGroup.vue index cc9a30e7..b530de0e 100644 --- a/website/src/views/sub-views/event-settings/editor/ReportingEventSettingsGroup.vue +++ b/website/src/views/sub-views/event-settings/editor/ReportingEventSettingsGroup.vue @@ -1,48 +1,73 @@ @@ -83,7 +108,8 @@ export default { if (JSON.stringify(data) === JSON.stringify(this.reportingData)) return; this.reportingData = data || { attributes: {}, - score: {} + score: {}, + rescheduling: {} }; } diff --git a/website/src/views/sub-views/event-settings/editor/multiSelectEditor.js b/website/src/views/sub-views/event-settings/editor/multiSelectEditor.js index 1b07ea71..399ee881 100644 --- a/website/src/views/sub-views/event-settings/editor/multiSelectEditor.js +++ b/website/src/views/sub-views/event-settings/editor/multiSelectEditor.js @@ -15,7 +15,7 @@ export class MultiSelectEditor extends Handsontable.editors.TextEditor { this.registerHooks(); this.updateFunction = () => {}; - this.localValue; + this.localValue = null; this.select.addEventListener("change", (e) => { console.log("select change", e.target.value); diff --git a/website/src/views/sub-views/event/EventStaff.vue b/website/src/views/sub-views/event/EventStaff.vue index 15afd353..1a8ce2ae 100644 --- a/website/src/views/sub-views/event/EventStaff.vue +++ b/website/src/views/sub-views/event/EventStaff.vue @@ -39,8 +39,6 @@