diff --git a/package-lock.json b/package-lock.json index e9f2612a9..abf268ea9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,12 @@ "dependencies": { "@discordjs/voice": "^0.16.1", "@napi-rs/canvas": "^0.1.44", - "@octokit/types": "^12.3.0", + "@octokit/types": "^12.4.0", "@octokit/webhooks-types": "^7.3.1", - "@prisma/client": "^5.5.2", - "@sentry/node": "^7.80.0", + "@prisma/client": "^5.7.0", + "@sentry/node": "^7.85.0", "argon2": "^0.31.2", - "axios": "^1.6.1", + "axios": "^1.6.2", "body-parser": "^1.20.2", "common-tags": "^1.8.2", "convert-units": "^2.3.4", @@ -24,12 +24,12 @@ "cross-fetch": "^4.0.0", "databank": "^1.0.9", "databank-redis": "^0.19.6", - "discord-api-types": "^0.37.63", - "discord.js": "^14.14.0", + "discord-api-types": "^0.37.65", + "discord.js": "^14.14.1", "dotenv": "^16.3.1", "escape-html": "^1.0.3", "express": "^4.18.2", - "express-rate-limit": "^7.1.4", + "express-rate-limit": "^7.1.5", "fuse.js": "^7.0.0", "he": "^1.2.0", "helmet": "^7.1.0", @@ -37,44 +37,44 @@ "keycloak-admin": "^1.14.22", "knex": "^3.0.1", "knex-types": "^0.5.0", - "luxon": "^3.4.3", + "luxon": "^3.4.4", "morgan": "^1.10.0", "ms": "^2.1.3", "node-os-utils": "^1.3.7", - "objection": "^3.1.2", - "octokit": "^3.1.1", - "openai": "^4.17.4", + "objection": "^3.1.3", + "octokit": "^3.1.2", + "openai": "^4.20.1", "pg": "^8.11.3", "rss-parser": "^3.13.0", "sonar-scanner": "^3.1.0", "source-map-support": "^0.5.21", - "telegraf": "^4.15.0", + "telegraf": "^4.15.3", "trivia-api": "^1.0.1", "underscore": "^1.13.6", "winston": "^3.11.0", "youtube-search": "^1.1.6", - "youtube-search-without-api-key": "^1.1.0" + "youtube-search-without-api-key": "^2.0.1" }, "devDependencies": { "@types/common-tags": "^1.8.4", "@types/convert-units": "^2.3.9", - "@types/cors": "^2.8.16", + "@types/cors": "^2.8.17", "@types/escape-html": "^1.0.4", "@types/express": "^4.17.21", "@types/he": "^1.2.3", - "@types/jest": "^29.5.8", - "@types/luxon": "^3.3.4", + "@types/jest": "^29.5.11", + "@types/luxon": "^3.3.7", "@types/morgan": "^1.9.9", "@types/ms": "^0.7.34", - "@types/node": "^20.9.0", + "@types/node": "^20.10.3", "@types/node-os-utils": "^1.3.4", "@types/source-map-support": "^0.5.10", "@types/supertest": "^2.0.16", - "@types/underscore": "^1.11.14", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", + "@types/underscore": "^1.11.15", + "@typescript-eslint/eslint-plugin": "^6.13.2", + "@typescript-eslint/parser": "^6.13.2", "axios-mock-adapter": "^1.22.0", - "eslint": "^8.53.0", + "eslint": "^8.55.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-google": "^0.14.0", @@ -83,14 +83,14 @@ "eslint-plugin-jest": "^27.6.0", "eslint-plugin-sonarjs": "^0.23.0", "jest": "^29.7.0", - "nodemon": "^3.0.1", - "npm-check-updates": "^16.14.6", - "prisma": "^5.5.2", + "nodemon": "^3.0.2", + "npm-check-updates": "^16.14.11", + "prisma": "^5.7.0", "supertest": "^6.3.3", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "tsc-watch": "^6.0.4", - "typescript": "^5.2.2" + "typescript": "^5.3.2" }, "engines": { "node": "v21.1.0", @@ -977,9 +977,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -1000,9 +1000,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1966,45 +1966,32 @@ } }, "node_modules/@octokit/app": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.0.0.tgz", - "integrity": "sha512-g/zDXttroZ9Se08shK0d0d/j0cgSA+h4WV7qGUevNEM0piNBkIlfb4Fm6bSwCNAZhNf72mBgERmYOoxicPkqdw==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.0.2.tgz", + "integrity": "sha512-NCSCktSx+XmjuSUVn2dLfqQ9WIYePGP95SDJs4I9cn/0ZkeXcPkaoCLl64Us3dRKL2ozC7hArwze5Eu+/qt1tg==", "dependencies": { "@octokit/auth-app": "^6.0.0", "@octokit/auth-unauthenticated": "^5.0.0", "@octokit/core": "^5.0.0", "@octokit/oauth-app": "^6.0.0", - "@octokit/plugin-paginate-rest": "^8.0.0", - "@octokit/types": "^11.1.0", - "@octokit/webhooks": "^12.0.1" + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/app/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/app/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/auth-app": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.0.tgz", - "integrity": "sha512-OKct7Rukf3g9DjpzcpdacQsdmd6oPrJ7fZND22JkjzhDvfhttUOnmh+qPS4kHhaNNyTxqSThnfrUWvkqNLd1nw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.0.1.tgz", + "integrity": "sha512-tjCD4nzQNZgmLH62+PSnTF6eGerisFgV4v6euhqJik6yWV96e1ZiiGj+NXIqbgnpjLmtnBqVUrNyGKu3DoGEGA==", "dependencies": { "@octokit/auth-oauth-app": "^7.0.0", "@octokit/auth-oauth-user": "^4.0.0", "@octokit/request": "^8.0.2", "@octokit/request-error": "^5.0.0", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "deprecation": "^2.3.1", "lru-cache": "^10.0.0", "universal-github-app-jwt": "^1.1.1", @@ -2014,36 +2001,23 @@ "node": ">= 18" } }, - "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-app/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/auth-app/node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "engines": { "node": "14 || >=16.14" } }, "node_modules/@octokit/auth-oauth-app": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.0.tgz", - "integrity": "sha512-8JvJEXGoEqrbzLwt3SwIUvkDd+1wrM8up0KawvDIElB8rbxPbvWppGO0SLKAWSJ0q8ILcVq+mWck6pDcZ3a9KA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.0.1.tgz", + "integrity": "sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==", "dependencies": { "@octokit/auth-oauth-device": "^6.0.0", "@octokit/auth-oauth-user": "^4.0.0", "@octokit/request": "^8.0.2", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "@types/btoa-lite": "^1.0.0", "btoa-lite": "^1.0.0", "universal-user-agent": "^6.0.0" @@ -2052,55 +2026,29 @@ "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/auth-oauth-device": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.0.tgz", - "integrity": "sha512-Zgf/LKhwWk54rJaTGYVYtbKgUty+ouil6VQeRd+pCw7Gd0ECoSWaZuHK6uDGC/HtnWHjpSWFhzxPauDoHcNRtg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.0.1.tgz", + "integrity": "sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==", "dependencies": { "@octokit/oauth-methods": "^4.0.0", "@octokit/request": "^8.0.0", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/auth-oauth-user": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.0.tgz", - "integrity": "sha512-VOm5aIkVGHaOhIvsF/4YmSjoYDzzrKbbYkdSEO0KqHK7I8SlO3ZndSikQ1fBlNPUEH0ve2BOTxLrVvI1qBf9/Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.0.1.tgz", + "integrity": "sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==", "dependencies": { "@octokit/auth-oauth-device": "^6.0.0", "@octokit/oauth-methods": "^4.0.0", "@octokit/request": "^8.0.2", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "btoa-lite": "^1.0.0", "universal-user-agent": "^6.0.0" }, @@ -2108,19 +2056,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/auth-token": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", @@ -2130,30 +2065,17 @@ } }, "node_modules/@octokit/auth-unauthenticated": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.0.tgz", - "integrity": "sha512-AjOI6FNB2dweJ85p6rf7D4EhE4y6VBcwYfX/7KJkR5Q9fD9ET6NABAjajUTSNFfCxmNIaQgISggZ3pkgwtTqsA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", "dependencies": { "@octokit/request-error": "^5.0.0", - "@octokit/types": "^11.0.0" + "@octokit/types": "^12.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/core": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.0.tgz", @@ -2263,37 +2185,24 @@ } }, "node_modules/@octokit/oauth-methods": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.0.0.tgz", - "integrity": "sha512-dqy7BZLfLbi3/8X8xPKUKZclMEK9vN3fK5WF3ortRvtplQTszFvdAGbTo71gGLO+4ZxspNiLjnqdd64Chklf7w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.0.1.tgz", + "integrity": "sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==", "dependencies": { "@octokit/oauth-authorization-url": "^6.0.2", "@octokit/request": "^8.0.2", "@octokit/request-error": "^5.0.0", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "btoa-lite": "^1.0.0" }, "engines": { "node": ">= 18" } }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/openapi-types": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.0.2.tgz", - "integrity": "sha512-8li32fUDUeml/ACRp/njCWTsk5t17cfTM1jp9n08pBrqs5cDFJubtjsSnuz56r5Tad6jdEPJld7LxNp9dNcyjQ==" + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==" }, "node_modules/@octokit/plugin-paginate-graphql": { "version": "4.0.0", @@ -2307,11 +2216,11 @@ } }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-8.0.0.tgz", - "integrity": "sha512-2xZ+baZWUg+qudVXnnvXz7qfrTmDeYPCzangBVq/1gXxii/OiS//4shJp9dnCCvj1x+JAm9ji1Egwm1BA47lPQ==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", + "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", "dependencies": { - "@octokit/types": "^11.0.0" + "@octokit/types": "^12.4.0" }, "engines": { "node": ">= 18" @@ -2320,19 +2229,6 @@ "@octokit/core": ">=5" } }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { - "version": "18.1.1", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", - "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" - }, - "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", - "dependencies": { - "@octokit/openapi-types": "^18.0.0" - } - }, "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "10.1.2", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.1.2.tgz", @@ -2446,17 +2342,17 @@ } }, "node_modules/@octokit/types": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.3.0.tgz", - "integrity": "sha512-nJ8X2HRr234q3w/FcovDlA+ttUU4m1eJAourvfUUtwAWeqL8AsyRqfnLvVnYn3NFbUnsmzQCzLNdFerPwdmcDQ==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", + "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", "dependencies": { - "@octokit/openapi-types": "^19.0.2" + "@octokit/openapi-types": "^19.1.0" } }, "node_modules/@octokit/webhooks": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.0.3.tgz", - "integrity": "sha512-8iG+/yza7hwz1RrQ7i7uGpK2/tuItZxZq1aTmeg2TNp2xTUB8F8lZF/FcZvyyAxT8tpDMF74TjFGCDACkf1kAQ==", + "version": "12.0.10", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.0.10.tgz", + "integrity": "sha512-Q8d26l7gZ3L1SSr25NFbbP0B431sovU5r0tIqcvy8Z4PrD1LBv0cJEjvDLOieouzPSTzSzufzRIeXD7S+zAESA==", "dependencies": { "@octokit/request-error": "^5.0.0", "@octokit/webhooks-methods": "^4.0.0", @@ -2545,13 +2441,10 @@ } }, "node_modules/@prisma/client": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.5.2.tgz", - "integrity": "sha512-54XkqR8M+fxbzYqe+bIXimYnkkcGqgOh0dn0yWtIk6CQT4IUCAvNFNcQZwk2KqaLU+/1PHTSWrcHtx4XjluR5w==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.0.tgz", + "integrity": "sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==", "hasInstallScript": true, - "dependencies": { - "@prisma/engines-version": "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a" - }, "engines": { "node": ">=16.13" }, @@ -2564,17 +2457,50 @@ } } }, + "node_modules/@prisma/debug": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz", + "integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==", + "devOptional": true + }, "node_modules/@prisma/engines": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.5.2.tgz", - "integrity": "sha512-Be5hoNF8k+lkB3uEMiCHbhbfF6aj1GnrTBnn5iYFT7GEr3TsOEp1soviEcBR0tYCgHbxjcIxJMhdbvxALJhAqg==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz", + "integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==", "devOptional": true, - "hasInstallScript": true + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.7.0", + "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", + "@prisma/fetch-engine": "5.7.0", + "@prisma/get-platform": "5.7.0" + } }, "node_modules/@prisma/engines-version": { - "version": "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a.tgz", - "integrity": "sha512-O+qHFnZvAyOFk1tUco2/VdiqS0ym42a3+6CYLScllmnpbyiTplgyLt2rK/B9BTjYkSHjrgMhkG47S0oqzdIckA==" + "version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz", + "integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz", + "integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.7.0", + "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", + "@prisma/get-platform": "5.7.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz", + "integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.7.0" + } }, "node_modules/@sapphire/async-queue": { "version": "1.5.0", @@ -2608,39 +2534,39 @@ } }, "node_modules/@sentry-internal/tracing": { - "version": "7.80.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.80.0.tgz", - "integrity": "sha512-P1Ab9gamHLsbH9D82i1HY8xfq9dP8runvc4g50AAd6OXRKaJ45f2KGRZUmnMEVqBQ7YoPYp2LFMkrhNYbcZEoQ==", + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.85.0.tgz", + "integrity": "sha512-p3YMUwkPCy2su9cm/3+7QYR4RiMI0+07DU1BZtht9NLTzY2O87/yvUbn1v2yHR3vJQTy/+7N0ud9/mPBFznRQQ==", "dependencies": { - "@sentry/core": "7.80.0", - "@sentry/types": "7.80.0", - "@sentry/utils": "7.80.0" + "@sentry/core": "7.85.0", + "@sentry/types": "7.85.0", + "@sentry/utils": "7.85.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/core": { - "version": "7.80.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.80.0.tgz", - "integrity": "sha512-nJiiymdTSEyI035/rdD3VOq6FlOZ2wWLR5bit9LK8a3rzHU3UXkwScvEo6zYgs0Xp1sC0yu1S9+0BEiYkmi29A==", + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.85.0.tgz", + "integrity": "sha512-DFDAc4tWmHN5IWhr7XbHCiyF1Xgb95jz8Uj/JTX9atlgodId1UIbER77qpEmH3eQGid/QBdqrlR98zCixgSbwg==", "dependencies": { - "@sentry/types": "7.80.0", - "@sentry/utils": "7.80.0" + "@sentry/types": "7.85.0", + "@sentry/utils": "7.85.0" }, "engines": { "node": ">=8" } }, "node_modules/@sentry/node": { - "version": "7.80.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.80.0.tgz", - "integrity": "sha512-J35fqe8J5ac/17ZXT0ML3opYGTOclqYNE9Sybs1y9n6BqacHyzH8By72YrdI03F7JJDHwrcGw+/H8hGpkCwi0Q==", - "dependencies": { - "@sentry-internal/tracing": "7.80.0", - "@sentry/core": "7.80.0", - "@sentry/types": "7.80.0", - "@sentry/utils": "7.80.0", + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.85.0.tgz", + "integrity": "sha512-uiBtRW9G017NHoCXBlK3ttkTwHXLFyI8ndHpaObtyajKTv3ptGIThVEn7DuK7Pwor//RjwjSEEOa7WDK+FdMVQ==", + "dependencies": { + "@sentry-internal/tracing": "7.85.0", + "@sentry/core": "7.85.0", + "@sentry/types": "7.85.0", + "@sentry/utils": "7.85.0", "https-proxy-agent": "^5.0.0" }, "engines": { @@ -2648,19 +2574,19 @@ } }, "node_modules/@sentry/types": { - "version": "7.80.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.80.0.tgz", - "integrity": "sha512-4bpMO+2jWiWLDa8zbTASWWNLWe6yhjfPsa7/6VH5y9x1NGtL8oRbqUsTgsvjF3nmeHEMkHQsC8NHPaQ/ibFmZQ==", + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.85.0.tgz", + "integrity": "sha512-R5jR4XkK5tBU2jDiPdSVqzkmjYRr666bcGaFGUHB/xDQCjPsjk+pEmCCL+vpuWoaZmQJUE1hVU7rgnVX81w8zg==", "engines": { "node": ">=8" } }, "node_modules/@sentry/utils": { - "version": "7.80.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.80.0.tgz", - "integrity": "sha512-XbBCEl6uLvE50ftKwrEo6XWdDaZXHXu+kkHXTPWQEcnbvfZKLuG9V0Hxtxxq3xQgyWmuF05OH1GcqYqiO+v5Yg==", + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.85.0.tgz", + "integrity": "sha512-JZ7seNOLvhjAQ8GeB3GYknPQJkuhF88xAYOaESZP3xPOWBMFUN+IO4RqjMqMLFDniOwsVQS7GB/MfP+hxufieg==", "dependencies": { - "@sentry/types": "7.80.0" + "@sentry/types": "7.85.0" }, "engines": { "node": ">=8" @@ -2747,14 +2673,14 @@ } }, "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dependencies": { - "defer-to-connect": "^2.0.0" + "defer-to-connect": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=14.16" } }, "node_modules/@telegraf/types": { @@ -2842,9 +2768,9 @@ } }, "node_modules/@types/aws-lambda": { - "version": "8.10.119", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.119.tgz", - "integrity": "sha512-Vqm22aZrCvCd6I5g1SvpW151jfqwTzEZ7XJ3yZ6xaZG31nUEOEyzzVImjRcsN8Wi/QyPxId/x8GTtgIbsy8kEw==" + "version": "8.10.130", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.130.tgz", + "integrity": "sha512-HxTfLeGvD1wTJqIGwcBCpNmHKenja+We1e0cuzeIDFfbEj3ixnlTInyPR/81zAe0Ss/Ip12rFK6XNeMLVucOSg==" }, "node_modules/@types/babel__core": { "version": "7.20.3", @@ -2898,20 +2824,9 @@ } }, "node_modules/@types/btoa-lite": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.0.tgz", - "integrity": "sha512-wJsiX1tosQ+J5+bY5LrSahHxr2wT+uME5UDwdN1kg4frt40euqA+wzECkmq4t5QbveHiJepfdThgQrPw6KiSlg==" - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==" }, "node_modules/@types/common-tags": { "version": "1.8.4", @@ -2941,9 +2856,9 @@ "dev": true }, "node_modules/@types/cors": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", - "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, "dependencies": { "@types/node": "*" @@ -3030,9 +2945,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.8", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", - "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3052,25 +2967,17 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", - "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", + "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/luxon": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.4.tgz", - "integrity": "sha512-H9OXxv4EzJwE75aTPKpiGXJq+y4LFxjpsdgKwSmr503P5DkWc3AG7VAFYrFNVvqemT5DfgZJV9itYhqBHSGujA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.7.tgz", + "integrity": "sha512-gKc9P2d4g5uYwmy4s/MO/yOVPmvHyvzka1YH6i5dM03UrFofHSmgc0D0ymbDRStFWHusk6cwwF6nhLm/ckBbbQ==", "dev": true }, "node_modules/@types/mime": { @@ -3095,9 +3002,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", "dependencies": { "undici-types": "~5.26.4" } @@ -3142,14 +3049,6 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, - "node_modules/@types/responselike": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", - "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/semver": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", @@ -3217,9 +3116,9 @@ "integrity": "sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==" }, "node_modules/@types/underscore": { - "version": "1.11.14", - "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.14.tgz", - "integrity": "sha512-13RYuwqoXZgLO3Nu4zsISqYAexCILtKIMGx+7+vY6gEsGFjrcHU57iDxPmaA2E5d4v5NwebNweiXLbaZWHWI9A==", + "version": "1.11.15", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.11.15.tgz", + "integrity": "sha512-HP38xE+GuWGlbSRq9WrZkousaQ7dragtZCruBVMi0oX1migFZavZ3OROKHSkNp/9ouq82zrWtZpg18jFnVN96g==", "dev": true }, "node_modules/@types/ws": { @@ -3246,16 +3145,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz", - "integrity": "sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz", + "integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.10.0", - "@typescript-eslint/type-utils": "6.10.0", - "@typescript-eslint/utils": "6.10.0", - "@typescript-eslint/visitor-keys": "6.10.0", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/type-utils": "6.13.2", + "@typescript-eslint/utils": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -3281,15 +3180,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.10.0.tgz", - "integrity": "sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.2.tgz", + "integrity": "sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.10.0", - "@typescript-eslint/types": "6.10.0", - "@typescript-eslint/typescript-estree": "6.10.0", - "@typescript-eslint/visitor-keys": "6.10.0", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/typescript-estree": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4" }, "engines": { @@ -3309,13 +3208,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz", - "integrity": "sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz", + "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.10.0", - "@typescript-eslint/visitor-keys": "6.10.0" + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3326,13 +3225,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz", - "integrity": "sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz", + "integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.10.0", - "@typescript-eslint/utils": "6.10.0", + "@typescript-eslint/typescript-estree": "6.13.2", + "@typescript-eslint/utils": "6.13.2", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3353,9 +3252,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.10.0.tgz", - "integrity": "sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz", + "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3366,13 +3265,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz", - "integrity": "sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz", + "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.10.0", - "@typescript-eslint/visitor-keys": "6.10.0", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3393,17 +3292,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.10.0.tgz", - "integrity": "sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz", + "integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.10.0", - "@typescript-eslint/types": "6.10.0", - "@typescript-eslint/typescript-estree": "6.10.0", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/typescript-estree": "6.13.2", "semver": "^7.5.4" }, "engines": { @@ -3418,12 +3317,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz", - "integrity": "sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz", + "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.10.0", + "@typescript-eslint/types": "6.13.2", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -3863,9 +3762,9 @@ } }, "node_modules/axios": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", - "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -4432,11 +4331,11 @@ } }, "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", "engines": { - "node": ">=10.6.0" + "node": ">=14.16" } }, "node_modules/cacheable-request": { @@ -5317,14 +5216,14 @@ } }, "node_modules/discord-api-types": { - "version": "0.37.63", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.63.tgz", - "integrity": "sha512-WbEDWj/1JGCIC1oCMIC4z9XbYY8PrWpV5eqFFQymJhJlHMqgIjqoYbU812X5oj5cwbRrEh6Va4LNLumB2Nt6IQ==" + "version": "0.37.65", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.65.tgz", + "integrity": "sha512-CQHW3Nu04LEHIj1Xps/sfGhTdrowilxnek2tirpLhwvrmgmLr1C6A+4JFLs+0kJMH2IX2QgDyA9GfNehqN+xPQ==" }, "node_modules/discord.js": { - "version": "14.14.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.0.tgz", - "integrity": "sha512-ovreSRM365CLfKnfdxB2j56kPnE9lysZS0ZushWCOqFzg1i0jhtA7TbWGFVVqSDP6HVSElNpxhINCdfCVwsaNw==", + "version": "14.14.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", + "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", "dependencies": { "@discordjs/builders": "^1.7.0", "@discordjs/collection": "1.5.3", @@ -5475,14 +5374,6 @@ "node": ">=0.10.0" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -5659,15 +5550,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -6294,12 +6185,15 @@ } }, "node_modules/express-rate-limit": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.4.tgz", - "integrity": "sha512-mv/6z+EwnWpr+MjGVavMGvM4Tl8S/tHmpl9ZsDfrQeHpYy4Hfr0UYdKEf9OOTe280oIr70yPxLRmQ6MfINfJDw==", + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.1.5.tgz", + "integrity": "sha512-/iVogxu7ueadrepw1bS0X0kaRC/U0afwiYRSLg68Ts+p4Dc85Q5QKsOnPS/QUjPMHvOJQtBDrZgvkOzf8ejUYw==", "engines": { "node": ">= 16" }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, "peerDependencies": { "express": "4 || 5 || ^5.0.0-beta.1" } @@ -6910,7 +6804,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "engines": { "node": ">=10" }, @@ -7338,12 +7231,12 @@ } }, "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dependencies": { "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" + "resolve-alpn": "^1.2.0" }, "engines": { "node": ">=10.19.0" @@ -8791,14 +8684,20 @@ ] }, "node_modules/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dependencies": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">=12", @@ -9184,11 +9083,31 @@ "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-2.3.0.tgz", "integrity": "sha512-NYJ2r2cwy3tkx/saqbIZEX6oQUzjWTnGRu7d/zmBjMCZos3eHBxCpbvWFWSetv8jFVrptsp6EbWjzNgBKhUoOA==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, "node_modules/lodash.isfunction": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-2.3.0.tgz", "integrity": "sha512-X5lteBYlCrVO7Qc00fxP8W90fzRp6Ax9XcHANmU3OsZHdSyIVZ9ZlX5QTTpRq8aGY+9I5Rmd0UTzTIIyWPugEQ==" }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, "node_modules/lodash.isobject": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.3.0.tgz", @@ -9197,6 +9116,16 @@ "lodash._objecttypes": "~2.3.0" } }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.keys": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.3.0.tgz", @@ -9224,6 +9153,11 @@ "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-2.3.0.tgz", "integrity": "sha512-NpSm8HRm1WkBBWHUveDukLF4Kfb5P5E3fjHc9Qre9A11nNubozLWD2wH3UBTZbu+KSuX8aSUvy9b+PUyEceJ8g==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", @@ -9268,9 +9202,9 @@ } }, "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -10130,13 +10064,13 @@ "dev": true }, "node_modules/nodemon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", - "integrity": "sha512-g9AZ7HmkhQkqXkRc20w+ZfQ73cHLbE8hnPbtaFbFtCumZsjyMhKk9LajQ07U5Ux28lvFjZ5X7HvWR1xzU8jHVw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz", + "integrity": "sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==", "dev": true, "dependencies": { "chokidar": "^3.5.2", - "debug": "^3.2.7", + "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", @@ -10157,15 +10091,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, "node_modules/nodemon/node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -10293,9 +10218,9 @@ } }, "node_modules/npm-check-updates": { - "version": "16.14.6", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.6.tgz", - "integrity": "sha512-sJ6w4AmSDP7YzBXah94Ul2JhiIbjBDfx9XYgib15um2wtiQkOyjE7Lov3MNUSQ84Ry7T81mE4ynMbl/mGbK4HQ==", + "version": "16.14.11", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.11.tgz", + "integrity": "sha512-0MMWGbGci22Pu77bR9jRsy5qgxdQSJVqNtSyyFeubDPtbcU36z4gjEDITu26PMabFWPNkAoVfKF31M3uKUvzFg==", "dev": true, "dependencies": { "chalk": "^5.3.0", @@ -10683,11 +10608,11 @@ } }, "node_modules/objection": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/objection/-/objection-3.1.2.tgz", - "integrity": "sha512-V8YwRWz+DFbB9JS/m7TBLhRPVAFK/VX7yV3ZDAMkfUG9qYHLRyG/K4ZS0acKtGPWtRdVrCuN4qM1VkH3PuJ5Lg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/objection/-/objection-3.1.3.tgz", + "integrity": "sha512-X4DH8/xKBS34bwWOSLAPyceg0JgLhLiUuz+cEEyDA8iDFoT1UM9UbtwBpwHV11hYskAKxOgVlNHeveFQiOPDXA==", "dependencies": { - "ajv": "^8.6.2", + "ajv": "^8.12.0", "ajv-formats": "^2.1.1", "db-errors": "^0.2.3" }, @@ -10719,11 +10644,11 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/octokit": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.1.tgz", - "integrity": "sha512-AKJs5XYs7iAh7bskkYpxhUIpsYZdLqjnlnqrN5s9FFZuJ/a6ATUHivGpUKDpGB/xa+LGDtG9Lu8bOCfPM84vHQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.2.tgz", + "integrity": "sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==", "dependencies": { - "@octokit/app": "^14.0.0", + "@octokit/app": "^14.0.2", "@octokit/core": "^5.0.0", "@octokit/oauth-app": "^6.0.0", "@octokit/plugin-paginate-graphql": "^4.0.0", @@ -10738,20 +10663,6 @@ "node": ">= 18" } }, - "node_modules/octokit/node_modules/@octokit/plugin-paginate-rest": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.2.tgz", - "integrity": "sha512-euDbNV6fxX6btsCDnZoZM4vw3zO1nj1Z7TskHAulO6mZ9lHoFTpwll6farf+wh31mlBabgU81bBYdflp0GLVAQ==", - "dependencies": { - "@octokit/types": "^12.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": ">=5" - } - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -10803,9 +10714,9 @@ } }, "node_modules/openai": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.17.4.tgz", - "integrity": "sha512-ThRFkl6snLbcAKS58St7N3CaKuI5WdYUvIjPvf4s+8SdymgNtOfzmZcZXVcCefx04oKFnvZJvIcTh3eAFUUhAQ==", + "version": "4.20.1", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.20.1.tgz", + "integrity": "sha512-Dd3q8EvINfganZFtg6V36HjrMaihqRgIcKiHua4Nq9aw/PxOP48dhbsk8x5klrxajt5Lpnc1KTOG5i1S6BKAJA==", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -10959,27 +10870,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/package-json/node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/package-json/node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, - "engines": { - "node": ">=14.16" - } - }, "node_modules/package-json/node_modules/cacheable-request": { "version": "10.2.14", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", @@ -11065,19 +10955,6 @@ "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, - "node_modules/package-json/node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", - "dev": true, - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/package-json/node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -11631,13 +11508,13 @@ } }, "node_modules/prisma": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.5.2.tgz", - "integrity": "sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz", + "integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.5.2" + "@prisma/engines": "5.7.0" }, "bin": { "prisma": "build/index.js" @@ -11767,15 +11644,6 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -13098,9 +12966,9 @@ } }, "node_modules/telegraf": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.15.0.tgz", - "integrity": "sha512-jOQhpMZxZ7gTD1/pIQkXHv0BNVRMfn0xtRHnxHv6GQdeISMErzzAe82rMm78ZPSwwKCS4vVu3n/c2LMY8UFiKg==", + "version": "4.15.3", + "resolved": "https://registry.npmjs.org/telegraf/-/telegraf-4.15.3.tgz", + "integrity": "sha512-pm2ZQAisd0YlUvnq6xdymDfoQR++8wTalw0nfw7Tjy0va+V/0HaBLzM8kMNid8pbbt7GHTU29lEyA5CAAr8AqA==", "dependencies": { "@telegraf/types": "^6.9.1", "abort-controller": "^3.0.0", @@ -13565,9 +13433,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -14259,11 +14127,11 @@ } }, "node_modules/youtube-search-without-api-key": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/youtube-search-without-api-key/-/youtube-search-without-api-key-1.1.0.tgz", - "integrity": "sha512-1SaOviXU5CZ9wU1YlMQx7NBysIr5IDRNe+mD/8ml6IAiiDlEOgZ8a01Hs+OhmpQAb+UuY1cOsfca8pcs/CGLUA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/youtube-search-without-api-key/-/youtube-search-without-api-key-2.0.1.tgz", + "integrity": "sha512-+MehH/7NGJgHjhrO+om5lDJRa9W5GBOr+cVsB1d+A9WsMFJWHpWutoGjw1iYLOVc5+KWofSc9y0Mt/HsDn0U4A==", "dependencies": { - "got": "^11.8.3" + "got": "^13.0.0" }, "funding": { "type": "paypal", @@ -14271,31 +14139,31 @@ } }, "node_modules/youtube-search-without-api-key/node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" } }, "node_modules/youtube-search-without-api-key/node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=14.16" } }, "node_modules/youtube-search-without-api-key/node_modules/decompress-response": { @@ -14312,39 +14180,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/youtube-search-without-api-key/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dependencies": { - "pump": "^3.0.0" - }, + "node_modules/youtube-search-without-api-key/node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/youtube-search-without-api-key/node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "engines": { + "node": ">= 14.17" + } + }, "node_modules/youtube-search-without-api-key/node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" }, "engines": { - "node": ">=10.19.0" + "node": ">=16" }, "funding": { "url": "https://github.com/sindresorhus/got?sponsor=1" @@ -14361,57 +14234,63 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/youtube-search-without-api-key/node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/youtube-search-without-api-key/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/youtube-search-without-api-key/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "engines": { - "node": ">=10" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/youtube-search-without-api-key/node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", + "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", "engines": { - "node": ">=10" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/youtube-search-without-api-key/node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", "engines": { - "node": ">=8" + "node": ">=12.20" } }, "node_modules/youtube-search-without-api-key/node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "dependencies": { - "lowercase-keys": "^2.0.0" + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 4a8515dda..8570c3552 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "db:formatSchema": "docker exec -it tripbot npx prisma format", "db:validateSchema": "docker exec -it tripbot npx prisma validate", "db:generateClient": "npx prisma generate && docker exec -it tripbot npx prisma generate", - "db:push": "docker exec -it tripbot npx prisma db push", - "db:migrateDev": "docker exec -it tripbot npx prisma migrate dev -n aiUsage", + "db:pushDev": "docker exec -it tripbot npx prisma db push && npx prisma generate", + "db:migrateDev": "docker exec -it tripbot npx prisma migrate dev -n betterImageLogging", "## PGADMIN ##": "", "pgadmin": "docker compose --project-name tripbot up -d --force-recreate --build tripbot_pgadmin", "pgadmin:logs": "docker container logs tripbot_pgadmin -f -n 100", @@ -64,12 +64,12 @@ "dependencies": { "@discordjs/voice": "^0.16.1", "@napi-rs/canvas": "^0.1.44", - "@octokit/types": "^12.3.0", + "@octokit/types": "^12.4.0", "@octokit/webhooks-types": "^7.3.1", - "@prisma/client": "^5.5.2", - "@sentry/node": "^7.80.0", + "@prisma/client": "^5.7.0", + "@sentry/node": "^7.85.0", "argon2": "^0.31.2", - "axios": "^1.6.1", + "axios": "^1.6.2", "body-parser": "^1.20.2", "common-tags": "^1.8.2", "convert-units": "^2.3.4", @@ -77,12 +77,12 @@ "cross-fetch": "^4.0.0", "databank": "^1.0.9", "databank-redis": "^0.19.6", - "discord-api-types": "^0.37.63", - "discord.js": "^14.14.0", + "discord-api-types": "^0.37.65", + "discord.js": "^14.14.1", "dotenv": "^16.3.1", "escape-html": "^1.0.3", "express": "^4.18.2", - "express-rate-limit": "^7.1.4", + "express-rate-limit": "^7.1.5", "fuse.js": "^7.0.0", "he": "^1.2.0", "helmet": "^7.1.0", @@ -90,44 +90,44 @@ "keycloak-admin": "^1.14.22", "knex": "^3.0.1", "knex-types": "^0.5.0", - "luxon": "^3.4.3", + "luxon": "^3.4.4", "morgan": "^1.10.0", "ms": "^2.1.3", "node-os-utils": "^1.3.7", - "objection": "^3.1.2", - "octokit": "^3.1.1", - "openai": "^4.17.4", + "objection": "^3.1.3", + "octokit": "^3.1.2", + "openai": "^4.20.1", "pg": "^8.11.3", "rss-parser": "^3.13.0", "sonar-scanner": "^3.1.0", "source-map-support": "^0.5.21", - "telegraf": "^4.15.0", + "telegraf": "^4.15.3", "trivia-api": "^1.0.1", "underscore": "^1.13.6", "winston": "^3.11.0", "youtube-search": "^1.1.6", - "youtube-search-without-api-key": "^1.1.0" + "youtube-search-without-api-key": "^2.0.1" }, "devDependencies": { "@types/common-tags": "^1.8.4", "@types/convert-units": "^2.3.9", - "@types/cors": "^2.8.16", + "@types/cors": "^2.8.17", "@types/escape-html": "^1.0.4", "@types/express": "^4.17.21", "@types/he": "^1.2.3", - "@types/jest": "^29.5.8", - "@types/luxon": "^3.3.4", + "@types/jest": "^29.5.11", + "@types/luxon": "^3.3.7", "@types/morgan": "^1.9.9", "@types/ms": "^0.7.34", - "@types/node": "^20.9.0", + "@types/node": "^20.10.3", "@types/node-os-utils": "^1.3.4", "@types/source-map-support": "^0.5.10", "@types/supertest": "^2.0.16", - "@types/underscore": "^1.11.14", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", + "@types/underscore": "^1.11.15", + "@typescript-eslint/eslint-plugin": "^6.13.2", + "@typescript-eslint/parser": "^6.13.2", "axios-mock-adapter": "^1.22.0", - "eslint": "^8.53.0", + "eslint": "^8.55.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-google": "^0.14.0", @@ -136,13 +136,13 @@ "eslint-plugin-jest": "^27.6.0", "eslint-plugin-sonarjs": "^0.23.0", "jest": "^29.7.0", - "nodemon": "^3.0.1", - "npm-check-updates": "^16.14.6", - "prisma": "^5.5.2", + "nodemon": "^3.0.2", + "npm-check-updates": "^16.14.11", + "prisma": "^5.7.0", "supertest": "^6.3.3", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "tsc-watch": "^6.0.4", - "typescript": "^5.2.2" + "typescript": "^5.3.2" } } diff --git a/src/discord/commands/guild/d.ai.ts b/src/discord/commands/global/d.ai.ts similarity index 66% rename from src/discord/commands/guild/d.ai.ts rename to src/discord/commands/global/d.ai.ts index 9d238c252..e91edfdd3 100644 --- a/src/discord/commands/guild/d.ai.ts +++ b/src/discord/commands/global/d.ai.ts @@ -41,15 +41,13 @@ import { import OpenAI from 'openai'; import { Run } from 'openai/resources/beta/threads/runs/runs'; import { MessageContentText } from 'openai/resources/beta/threads/messages/messages'; -import { DateTime } from 'luxon'; -import { paginationEmbed } from '../../utils/pagination'; import { SlashCommand } from '../../@types/commandDef'; import { embedTemplate } from '../../utils/embedTemplate'; import commandContext from '../../utils/context'; import { moderate } from '../../../global/commands/g.moderate'; -import { sleep } from './d.bottest'; +import { sleep } from '../guild/d.bottest'; import aiChat, { - aiModerate, createImage, createMessage, getAssistant, getMessages, getThread, readRun, runThread, + aiModerate, createMessage, getAssistant, getMessages, getThread, readRun, runThread, } from '../../../global/commands/g.ai'; import { UserActionType } from '../../../global/@types/database'; import { parseDuration } from '../../../global/utils/parseDuration'; @@ -61,10 +59,8 @@ const F = f(__filename); const maxHistoryLength = 5; const ephemeralExplanation = 'Set to "True" to show the response only to you'; -const personaDoesntExist = 'This persona does not exist. Please create it first.'; -const confirmationCodes = new Map(); +const personaDoesNotExist = 'This persona does not exist. Please create it first.'; const tripbotUAT = '@TripBot UAT (Moonbear)'; -const imageLimit = 25; // Costs per 1k tokens const aiCosts = { @@ -96,93 +92,54 @@ const aiCosts = { input: 0.00, output: 0.02, }, -} as AiCosts; - -// define an object as series of keys (AiModel) and value that looks like {input: number, output: number} -type AiCosts = { +} as { [key in ai_model]: { input: number, output: number, } }; -type AiAction = 'HELP' | 'UPSERT' | 'GET' | 'DEL' | 'LINK' | 'MOD'; - async function help( interaction: ChatInputCommandInteraction, ):Promise { const visible = interaction.options.getBoolean('ephemeral') !== false; - await interaction.deferReply({ ephemeral: !visible }); - - const aboutEmbed = embedTemplate() - .setTitle('AI Help') - .setDescription(stripIndents` - Welcome to TripBot's AI module! - - This is not a real AI, this is a Language Learning Model (LLM). - It does not provide any kind of "intelligence", it just knows how to make a sentence that sounds good. - As such, this feature will likely not be introduced to the trip sitting rooms, as it is not 100% trustworthy. - But we can still have some fun and play with it in the social rooms! - Here's how you can do that: - - **/ai set** - This command is used to set the parameters of an AI persona, or create a new persona. - A persona is how the bot will respond to queries. We can have multiple personas, each with their own parameters. - The parameters are explained on the next page. - EG: We can have a "helpful" persona, and a "funny" persona, and a "serious" persona, etc. - **Anyone is welcome to create their own persona in the dev guild!** - - **/ai get** - This command is used to get the parameters of an AI persona. - You can either get the specific name of the persona - Or you can enter a channel name to get which persona is linked to that channel. - - **/ai del** - To delete a persona. You must provide a confirmation code to delete a persona. - - **/ai link** - You can link threads, channels, and even entire categories to a persona. - This allows the bot to respond to messages in those channels with the persona you set. - You can also disable the link, so the bot will not respond in that channel. - `); - - const parametersEmbed = embedTemplate() - .setTitle('AI Help') - /* eslint-disable max-len */ - .setDescription(stripIndents` - This command is used to set the parameters of an AI persona, or create a new persona. - The parameters are: - **Name** - > The name of the persona. This is used to identify the persona in the AI Get command. - **Model** - > The model to use for the persona. This is a dropdown list of available models. - **Prompt** - > The prompt to use for the persona. This is the text that the AI will use to generate responses. - **Max Tokens** - > The maximum number of tokens to use for the AI response. - > What are tokens? https://platform.openai.com/tokenizer - **Temperature** - > Adjusts the randomness of the AI's answers. - > Lower values make the AI more predictable and focused, while higher values make it more random and varied. - > *Use this OR Top P, not both.* - **Top P** - > Limits the word choices the AI considers. - > Using a high value means the AI picks from the most likely words, while a lower value allows for more variety but might include less common words. - > *Use this OR Temperature, not both.* - **Presence Penalty** - > This adjusts the probability of words that are not initially very likely to appear, by making them more likely. - > A higher value can make a response more creative or diverse, as it promotes the presence of words or phrases that the model might not normally prioritize. - > For example, if you're looking for creative or unconventional answers, increasing the presence penalty can make the model consider a wider range of vocabularies. - **Frequency Penalty** - > This penalizes words or phrases that appear repeatedly in the output. - > A positive value reduces the likelihood of repetition, which can be useful if you're noticing that the model is repeating certain phrases or words too often. - > Conversely, a negative value would increase the chance of repetition, which might be useful if you want the model to emphasize a certain point. - **Logit Bias** - > How often to bias certain tokens. This is a JSON list. - > *You can likely ignore this unless you really wanna tweak the AI.* - `); - - paginationEmbed(interaction, [aboutEmbed, parametersEmbed]); + await interaction.deferReply({ ephemeral: visible }); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('AI Help') + .setDescription(` + πŸ€– Welcome to TripBot's AI Module! πŸ€– + + 🌐 Powered by OpenAI's API, this module is a Language Learning Model (LLM) – a sophisticated tool for crafting \ + sentences, but not a sentient AI. It's like having a super-smart writing assistant at your fingertips! + + 🚦 A Word of Caution: While GPT-3.5 can be impressively accurate, it's not infallible. Treat its responses as \ + suggestions rather than hard facts. There's no human behind its words, so always apply your own judgment. + + πŸ‘₯ How It Works: + An **AI Persona** defines the AI's interaction style, including tone and content length. + We've tailored our **TripBot** persona to provide harm reduction info with a touch of quirkiness. + Currently, TripBot is the sole persona available outside of TripSit. But there's more to come! + Eager to work with the AI? Join us in the TripSit guild and chat in <#${env.CHANNEL_TRIPBOT}>! + + πŸ”— Bring AI to Your Guild: + Simple Integration: Want this AI wizardry in your server? Just a single command away! +\`\`\` +/ai link + channel:(optional - defaults to current channel) + toggle:(optional - defaults to 'on') +\`\`\` + *You can link entire categories if you want!* + + Lost track of linked channels? Run \`/ai get\` to check how an AI Persona is linked that channel. + + πŸ“ Audit responses: + You can help us improve the AI by auditing its responses. If you see a response that's excellent or improper, \\ + react to it with the provided thumbs. If enough people agree, we'll take note and try to improve the bot behavior. + + πŸš€ Embark on an AI-Enhanced Journey: Prepare for a new era of AI-driven conversations! + `)], + }); } async function makePersonaEmbed( @@ -256,112 +213,6 @@ async function makePersonaEmbed( ]); } -async function upsert( - interaction: ChatInputCommandInteraction, -):Promise { - const personaName = interaction.options.getString('name') ?? interaction.user.username; - - // Validations on the given information - // Name must be < 50 characters - if (personaName.length > 50) { - embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription('The name of the AI persona must be less than 50 characters.'); - return; - } - - const existingPersona = await db.ai_personas.findFirst({ - where: { - name: personaName, - }, - }); - - const modal = new ModalBuilder() - .setCustomId(`aiPromptModal~${interaction.id}`) - .setTitle('Modal') - .addComponents(new ActionRowBuilder() - .addComponents(new TextInputBuilder() - .setCustomId('prompt') - .setPlaceholder(stripIndents` - You are a harm reduction assistant and should only give helpful, non-judgemental advice. - `) - .setValue(existingPersona?.prompt ?? '') - .setLabel('Prompt (Personality)') - .setStyle(TextInputStyle.Paragraph))); - - await interaction.showModal(modal); - - const filter = (i:ModalSubmitInteraction) => i.customId.includes('aiPromptModal'); - interaction.awaitModalSubmit({ filter, time: 0 }) - .then(async i => { - if (i.customId.split('~')[1] !== interaction.id) return; - await i.deferReply({ ephemeral: (interaction.options.getBoolean('ephemeral') === true) }); - - // Get values - let temperature = interaction.options.getNumber('temperature'); - const topP = interaction.options.getNumber('top_p'); - // log.debug(F, `temperature: ${temperature}, top_p: ${topP}`); - - // If both temperature and top_p are set, throw an error - if (temperature && topP) { - // log.debug(F, 'Both temperature and top_p are set'); - embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription('You can only set one of temperature or top_p.'); - return; - } - - // If both temperature and top_p are NOT set, set temperature to 1 - if (!temperature && !topP) { - // log.debug(F, 'Neither temperature nor top_p are set'); - temperature = 1; - } - - const userData = await db.users.upsert({ - where: { discord_id: interaction.user.id }, - create: { discord_id: interaction.user.id }, - update: { discord_id: interaction.user.id }, - }); - - const aiPersona = { - name: personaName, - ai_model: interaction.options.getString('model') as ai_model ?? 'GPT_3_5_TURBO', - prompt: i.fields.getTextInputValue('prompt'), - temperature, - top_p: topP, - presence_penalty: interaction.options.getNumber('presence_penalty') ?? 0, - frequency_penalty: interaction.options.getNumber('frequency_penalty') ?? 0, - max_tokens: interaction.options.getNumber('tokens') ?? 500, - created_by: existingPersona ? existingPersona.created_by : userData.id, - created_at: existingPersona ? existingPersona.created_at : new Date(), - } as ai_personas; - - const alreadyExists = await db.ai_personas.findFirst({ - where: { - name: aiPersona.name, - }, - }); - const action = alreadyExists ? 'updated' : 'created'; - - await db.ai_personas.upsert({ - where: { - name: aiPersona.name, - }, - create: aiPersona, - update: aiPersona, - }); - - await i.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(`Success! This persona has been ${action}!`)], - }); - }); -} - async function get( interaction: ChatInputCommandInteraction, ):Promise { @@ -448,412 +299,45 @@ async function get( embeds: [embedTemplate() .setTitle('Modal') .setColor(Colors.Red) - .setDescription(` - There was an error retrieving the AI persona. - It likely does not exist? - `)], - - }); - return; - } - - const personaEmbed = await makePersonaEmbed(aiPersona); - if (description) personaEmbed.setDescription(description); - await interaction.editReply({ - embeds: [personaEmbed], - }); -} - -async function del( - interaction: ChatInputCommandInteraction, -):Promise { - const visible = interaction.options.getBoolean('ephemeral') !== false; - await interaction.deferReply({ ephemeral: !visible }); - const confirmation = interaction.options.getString('confirmation'); - const personaName = interaction.options.getString('name') ?? interaction.user.username; - - if (!confirmation) { - const aiPersona = await db.ai_personas.findUnique({ - where: { - name: personaName, - }, - }); - - if (!aiPersona) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('AI Del') - .setDescription(stripIndents` - The **"${personaName}"** persona does not exist! - - Make sure you /ai set it first! - `)], - }); - return; - } - - // If the user did not provide a confirmation code, generate a new code and assign it to the user - const code = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); - confirmationCodes.set(`${interaction.user.id}${interaction.user.username}`, code); - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('AI Del') - .setDescription(` - Are you sure you want to delete the "${personaName}" AI persona? - - This action is irreversible, don't regret it later! - - If you're sure, please run the command again with the confirmation code: - - **${code}** - - `)], - }); - return; - } - - // If the user did provide a confirmation code, check if it matches the one in confirmationCodes - if (confirmationCodes.get(`${interaction.user.id}${interaction.user.username}`) !== confirmation) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('AI Del') - .setDescription(stripIndents`The confirmation code you provided was incorrect. - If you want to delete this AI persona, please run the command again and provide the correct code.`)], - }); - return; - } - - await db.ai_personas.delete({ - where: { - name: personaName, - }, - }); - - confirmationCodes.delete(`${interaction.user.id}${interaction.user.username}`); - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Blurple) - .setDescription('Success: Persona was deleted!')], - }); -} - -async function getLinkedChannel( - channel: CategoryChannel | ForumChannel | APIInteractionDataResolvedChannel | TextBasedChannel, -):Promise { - // With the way AI personas work, they can be assigned to a category, channel, or thread - // This function will check if the given channel is linked to an AI persona - // If it is not, it will check the channel's parent; either the Category or Channel (in case of Thread) - // If the parent isn't linked, it'll check the parent's parent; this is only for Thread channels. - // Once a link is fount, it will return that link data - // If no link is found, it'll return null - - // Check if the channel is linked to a persona - let aiLinkData = await db.ai_channels.findFirst({ where: { channel_id: channel.id } }); - - // If the channel isn't listed in the database, check the parent - if (!aiLinkData && 'parent' in channel && channel.parent) { - aiLinkData = await db.ai_channels.findFirst({ where: { channel_id: channel.parent.id } }); - // If /that/ channel doesn't exist, check the parent of the parent, this is for threads - if (!aiLinkData && channel.parent.parent) { - aiLinkData = await db.ai_channels.findFirst({ where: { channel_id: channel.parent.parent.id } }); - } - } - - return aiLinkData; -} - -async function link( - interaction: ChatInputCommandInteraction, -):Promise { - const visible = interaction.options.getBoolean('ephemeral') !== false; - await interaction.deferReply({ ephemeral: !visible }); - - const personaName = interaction.options.getString('name') ?? interaction.user.username; - const toggle = (interaction.options.getString('toggle') ?? 'enable') as 'enable' | 'disable'; - const textChannel = interaction.options.getChannel('channel') ?? interaction.channel; - - const response = '' as string; - if (toggle === 'enable') { - if (!textChannel) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription('You must provide a text channel to link to.')], - - }); - return; - } - // response = await aiLink(personaName, textChannel.id, toggle); - - const personaData = await db.ai_personas.findUnique({ - where: { - name: personaName, - }, - }); - - if (!personaData) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(personaDoesntExist)], - - }); - return; - } - - const aiLinkData = await db.ai_channels.findFirst({ - where: { - channel_id: textChannel.id, - }, - }); - - if (aiLinkData) { - await db.ai_channels.update({ - where: { - id: aiLinkData.id, - }, - data: { - channel_id: textChannel.id, - persona_id: personaData.id, - }, - }); - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(`Success: The link between ${personaName} and <#${textChannel.id}> was updated!`)], - - }); - return; - } - - await db.ai_channels.create({ - data: { - channel_id: textChannel.id, - persona_id: personaData.id, - }, - }); - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(`Success: The link between ${personaName} and <#${textChannel.id}> was created!`)], - - }); - return; - } - - if (textChannel) { - // response = await aiLink(personaName, textChannel.id, toggle); - const existingPersona = await db.ai_personas.findUnique({ - where: { - name: personaName, - }, - }); - - if (!existingPersona) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(personaDoesntExist)], - - }); - return; - } - - let existingLink = await db.ai_channels.findFirst({ - where: { - channel_id: textChannel.id, - persona_id: existingPersona.id, - }, - }); - - if (!existingLink) { - existingLink = await db.ai_channels.findFirst({ - where: { - channel_id: textChannel.id, - }, - }); - - if (!existingLink) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(`Error: No link to <#${textChannel.id}> found!`)], - - }); - return; - } - const personaData = await db.ai_personas.findUnique({ - where: { - id: existingLink.persona_id, - }, - }); - if (!personaData) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription('Error: No persona found for this link!')], - - }); - return; - } - await db.ai_channels.delete({ - where: { - id: existingLink.id, - }, - }); - } - } else if (interaction.channel) { - const aiLinkData = await getLinkedChannel(interaction.channel); - if (aiLinkData) { - // response = await aiLink(personaName, aiLinkData?.channel_id, toggle); - - const existingPersona = await db.ai_personas.findUnique({ - where: { - name: personaName, - }, - }); - - if (!existingPersona) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(personaDoesntExist)], - - }); - return; - } - - let existingLink = await db.ai_channels.findFirst({ - where: { - channel_id: aiLinkData.channel_id, - persona_id: existingPersona.id, - }, - }); - - if (!existingLink) { - existingLink = await db.ai_channels.findFirst({ - where: { - channel_id: aiLinkData.channel_id, - }, - }); - - if (!existingLink) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(`Error: No link to <#${aiLinkData.channel_id}> found!`)], - - }); - return; - } - const personaData = await db.ai_personas.findUnique({ - where: { - id: existingLink.persona_id, - }, - }); - if (!personaData) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription('Error: No persona found for this link!')], - - }); - return; - } - await db.ai_channels.delete({ - where: { - id: existingLink.id, - }, - }); - } - } else { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription('This channel is not linked to an AI persona.')], - - }); - return; - } - } else { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription('You must provide a text channel to link to.')], - - }); - return; - } - - log.debug(F, `response: ${response}`); - - if (!response.startsWith('Success')) { - await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Red) - .setDescription(response)], + .setDescription(` + There was an error retrieving the AI persona. + It likely does not exist? + `)], }); return; } - confirmationCodes.delete(`${interaction.user.id}${interaction.user.username}`); + const personaEmbed = await makePersonaEmbed(aiPersona); + if (description) personaEmbed.setDescription(description); await interaction.editReply({ - embeds: [embedTemplate() - .setTitle('Modal') - .setColor(Colors.Blurple) - .setDescription(response)], + embeds: [personaEmbed], }); } -async function mod( - interaction: ChatInputCommandInteraction, -):Promise { - if (!interaction.guild) return; - await interaction.deferReply({ ephemeral: true }); +async function getLinkedChannel( + channel: CategoryChannel | ForumChannel | APIInteractionDataResolvedChannel | TextBasedChannel, +):Promise { + // With the way AI personas work, they can be assigned to a category, channel, or thread + // This function will check if the given channel is linked to an AI persona + // If it is not, it will check the channel's parent; either the Category or Channel (in case of Thread) + // If the parent isn't linked, it'll check the parent's parent; this is only for Thread channels. + // Once a link is fount, it will return that link data + // If no link is found, it'll return null - const moderationData = await db.ai_moderation.upsert({ - where: { - guild_id: interaction.guild.id, - }, - create: { - guild_id: interaction.guild.id, - }, - update: {}, - }); + // Check if the channel is linked to a persona + let aiLinkData = await db.ai_channels.findFirst({ where: { channel_id: channel.id } }); - await db.ai_moderation.update({ - where: { - guild_id: interaction.guild.id, - }, - data: { - harassment: interaction.options.getNumber('harassment') ?? moderationData.harassment, - harassment_threatening: interaction.options.getNumber('harassment_threatening') ?? moderationData.harassment_threatening, - hate: interaction.options.getNumber('hate') ?? moderationData.hate, - hate_threatening: interaction.options.getNumber('hate_threatening') ?? moderationData.hate_threatening, - self_harm: interaction.options.getNumber('self_harm') ?? moderationData.self_harm, - self_harm_instructions: interaction.options.getNumber('self_harm_instructions') ?? moderationData.self_harm_instructions, - self_harm_intent: interaction.options.getNumber('self_harm_intent') ?? moderationData.self_harm_intent, - sexual: interaction.options.getNumber('sexual') ?? moderationData.sexual, - sexual_minors: interaction.options.getNumber('sexual_minors') ?? moderationData.sexual_minors, - violence: interaction.options.getNumber('violence') ?? moderationData.violence, - violence_graphic: interaction.options.getNumber('violence_graphic') ?? moderationData.violence_graphic, - }, - }); + // If the channel isn't listed in the database, check the parent + if (!aiLinkData && 'parent' in channel && channel.parent) { + aiLinkData = await db.ai_channels.findFirst({ where: { channel_id: channel.parent.id } }); + // If /that/ channel doesn't exist, check the parent of the parent, this is for threads + if (!aiLinkData && channel.parent.parent) { + aiLinkData = await db.ai_channels.findFirst({ where: { channel_id: channel.parent.parent.id } }); + } + } + + return aiLinkData; } async function saveThreshold( @@ -868,22 +352,28 @@ async function saveThreshold( const [,, category, amount] = interaction.customId.split('~'); const amountFloat = parseFloat(amount); - const buttonRows = interaction.message.components.map(row => row.toJSON() as APIActionRowComponent); + const buttonRows = interaction.message.components + .map(row => row.toJSON() as APIActionRowComponent); // log.debug(F, `buttonRows: ${JSON.stringify(buttonRows, null, 2)}`); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const categoryRow = buttonRows.find(row => row.components.find(button => (button as any).custom_id?.includes(category))) as APIActionRowComponent; + const categoryRow = buttonRows + .find(row => row.components + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .find(button => (button as any).custom_id?.includes(category))) as APIActionRowComponent; // log.debug(F, `categoryRow: ${JSON.stringify(categoryRow, null, 2)}`); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const saveButton = categoryRow?.components.find(button => (button as any).custom_id?.includes('save')); + const saveButton = categoryRow.components + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .find(button => (button as any).custom_id?.includes('save')) as APIButtonComponent; - const labelBreakdown = saveButton?.label?.split(' ') as string[]; + const labelBreakdown = (saveButton.label as string).split(' ') as string[]; labelBreakdown.splice(0, 1, 'Saved'); const newLabel = labelBreakdown.join(' '); // Replace the save button with the new value - categoryRow.components?.splice(4, 1, { + categoryRow.components.splice(4, 1, { custom_id: `aiMod~save~${category}~${amountFloat}`, label: newLabel, emoji: 'πŸ’Ύ' as APIMessageComponentEmoji, @@ -943,29 +433,35 @@ async function adjustThreshold( const amountFloat = parseFloat(amount); // Go through the components on the message and find the button that has a customID that includes 'save' - const buttonRows = interaction.message.components.map(row => row.toJSON() as APIActionRowComponent); + const buttonRows = interaction.message.components + .map(row => row.toJSON() as APIActionRowComponent); // log.debug(F, `buttonRows: ${JSON.stringify(buttonRows, null, 2)}`); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const categoryRow = buttonRows.find(row => row.components.find(button => (button as any).custom_id?.includes(category))) as APIActionRowComponent; + const categoryRow = buttonRows + .find(row => row.components + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .find(button => (button as any).custom_id?.includes(category))) as APIActionRowComponent; // log.debug(F, `categoryRow: ${JSON.stringify(categoryRow, null, 2)}`); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const saveButton = categoryRow?.components.find(button => (button as any).custom_id?.includes('save')); + const saveButton = categoryRow.components + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .find(button => (button as any).custom_id?.includes('save')) as APIButtonComponent; log.debug(F, `saveButton: ${JSON.stringify(saveButton, null, 2)}`); - const saveValue = parseFloat(saveButton?.label?.split(' ')[3] as string); + const saveValue = parseFloat((saveButton.label as string).split(' ')[3] as string); log.debug(F, `saveValue: ${JSON.stringify(saveValue, null, 2)}`); const newValue = saveValue + amountFloat; log.debug(F, `newValue: ${JSON.stringify(newValue.toFixed(2), null, 2)}`); - const labelBreakdown = saveButton?.label?.split(' ') as string[]; + const labelBreakdown = (saveButton.label as string).split(' ') as string[]; labelBreakdown.splice(3, 1, newValue.toFixed(2)); const newLabel = labelBreakdown.join(' '); // Replace the save button with the new value - categoryRow.components?.splice(4, 1, { + categoryRow.components.splice(4, 1, { custom_id: `aiMod~save~${category}~${newValue}`, label: newLabel, emoji: 'πŸ’Ύ' as APIMessageComponentEmoji, @@ -999,7 +495,7 @@ async function noteUser( const embed = interaction.message.embeds[0].toJSON(); - const flagsField = embed.fields?.find(field => field.name === 'Flags') as APIEmbedField; + const flagsField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Flags') as APIEmbedField; await interaction.showModal(new ModalBuilder() .setCustomId(`noteModal~${interaction.id}`) @@ -1019,9 +515,9 @@ async function noteUser( if (i.customId.split('~')[1] !== interaction.id) return; await i.deferReply({ ephemeral: true }); - const messageField = embed.fields?.find(field => field.name === 'Message') as APIEmbedField; - const memberField = embed.fields?.find(field => field.name === 'Member') as APIEmbedField; - const urlField = embed.fields?.find(field => field.name === 'Channel') as APIEmbedField; + const messageField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Message') as APIEmbedField; + const memberField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Member') as APIEmbedField; + const urlField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Channel') as APIEmbedField; await moderate( interaction.member as GuildMember, @@ -1094,9 +590,10 @@ async function muteUser( const embed = interaction.message.embeds[0].toJSON(); - const flagsField = embed.fields?.find(field => field.name === 'Flags') as APIEmbedField; - const messageField = embed.fields?.find(field => field.name === 'Message') as APIEmbedField; - const urlField = embed.fields?.find(field => field.name === 'Channel') as APIEmbedField; + const flagsField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Flags') as APIEmbedField; + const messageField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Message') as APIEmbedField; + const memberField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Member') as APIEmbedField; + const urlField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Channel') as APIEmbedField; await interaction.showModal(new ModalBuilder() .setCustomId(`timeoutModal~${interaction.id}`) @@ -1146,8 +643,6 @@ async function muteUser( return; } - const memberField = embed.fields?.find(field => field.name === 'Member') as APIEmbedField; - await moderate( interaction.member as GuildMember, 'TIMEOUT' as UserActionType, @@ -1218,9 +713,10 @@ async function warnUser( const embed = interaction.message.embeds[0].toJSON(); - const flagsField = embed.fields?.find(field => field.name === 'Flags') as APIEmbedField; - const urlField = embed.fields?.find(field => field.name === 'Channel') as APIEmbedField; - const messageField = embed.fields?.find(field => field.name === 'Message') as APIEmbedField; + const flagsField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Flags') as APIEmbedField; + const messageField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Message') as APIEmbedField; + const memberField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Member') as APIEmbedField; + const urlField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Channel') as APIEmbedField; await interaction.showModal(new ModalBuilder() .setCustomId(`warnModal~${interaction.id}`) @@ -1254,8 +750,6 @@ async function warnUser( if (i.customId.split('~')[1] !== interaction.id) return; await i.deferReply({ ephemeral: true }); - const memberField = embed.fields?.find(field => field.name === 'Member') as APIEmbedField; - await moderate( interaction.member as GuildMember, 'WARNING' as UserActionType, @@ -1326,9 +820,10 @@ async function banUser( const embed = interaction.message.embeds[0].toJSON(); - const flagsField = embed.fields?.find(field => field.name === 'Flags') as APIEmbedField; - const urlField = embed.fields?.find(field => field.name === 'Channel') as APIEmbedField; - const messageField = embed.fields?.find(field => field.name === 'Message') as APIEmbedField; + const flagsField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Flags') as APIEmbedField; + const messageField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Message') as APIEmbedField; + const memberField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Member') as APIEmbedField; + const urlField = (embed.fields as APIEmbedField[]).find(field => field.name === 'Channel') as APIEmbedField; await interaction.showModal(new ModalBuilder() .setCustomId(`banModal~${interaction.id}`) @@ -1377,8 +872,6 @@ async function banUser( return; } - const memberField = embed.fields?.find(field => field.name === 'Member') as APIEmbedField; - await moderate( interaction.member as GuildMember, 'FULL_BAN' as UserActionType, @@ -1439,15 +932,7 @@ async function banUser( }); } -// export async function aiModResults( - -// ) { -// const moderation = await aiModResults(message); - -// if (moderation.length === 0) return; -// }; - -export async function aiAudit( +async function aiAudit( aiPersona: ai_personas, messages: Message[], chatResponse: string, @@ -1463,7 +948,7 @@ export async function aiAudit( const contextMessages = messages.slice(0, messages.length - 1); const embed = embedTemplate() - .setFooter({ text: 'What are tokens? https://platform.openai.com/tokenizer' }) + .setFooter({ text: 'What are tokens? ' }) // .setThumbnail(promptMessage.author.displayAvatarURL()) .setColor(Colors.Yellow); @@ -1475,137 +960,323 @@ export async function aiAudit( const promptCost = (promptTokens / 1000) * aiCosts[aiPersona.ai_model].input; const completionCost = (completionTokens / 1000) * aiCosts[aiPersona.ai_model].output; - const userData = await db.users.upsert({ - where: { discord_id: promptMessage.author.id }, - create: { discord_id: promptMessage.author.id }, - update: { discord_id: promptMessage.author.id }, - }); + const userData = await db.users.upsert({ + where: { discord_id: promptMessage.author.id }, + create: { discord_id: promptMessage.author.id }, + update: { discord_id: promptMessage.author.id }, + }); + + const aiUsageData = await db.ai_usage.upsert({ + where: { + user_id: userData.id, + }, + create: { + user_id: userData.id, + }, + update: {}, + }); + + embed.addFields( + { + name: 'Persona', + value: stripIndents`**${aiPersona.name} (${aiPersona.ai_model})** - ${aiPersona.prompt}`, + inline: false, + }, + { + name: 'Context', + value: stripIndents`${contextMessageOutput}`, + inline: false, + }, + { + name: 'Prompt', + value: stripIndents`${promptMessage.url} ${promptMessage.member?.displayName}: ${promptMessage.cleanContent}`, + inline: false, + }, + { + name: 'Result', + value: stripIndents`${chatResponse.slice(0, 1023)}`, + inline: false, + }, + { + name: 'Chat Tokens', + value: stripIndents`${promptTokens + completionTokens} Tokens \n($${(promptCost + completionCost).toFixed(6)})`, + inline: true, + }, + { + name: 'User Tokens', + value: `${aiUsageData.tokens} Tokens\n($${((aiUsageData.tokens / 1000) + * aiCosts[aiPersona.ai_model].output).toFixed(6)})`, + inline: true, + }, + { + name: 'Persona Tokens', + value: `${aiPersona.total_tokens} Tokens\n($${((aiPersona.total_tokens / 1000) + * aiCosts[aiPersona.ai_model].output).toFixed(6)})`, + inline: true, + }, + ); + + // Get the channel to send the message to + const channelAiLog = await discordClient.channels.fetch(env.CHANNEL_AILOG) as TextChannel; + + // Send the message + await channelAiLog.send({ embeds: [embed] }); +} + +async function link( + interaction: ChatInputCommandInteraction, +):Promise { + const visible = interaction.options.getBoolean('ephemeral') !== false; + await interaction.deferReply({ ephemeral: !visible }); + + const personaName = interaction.options.getString('name') ?? 'tripbot'; + const toggle = (interaction.options.getString('toggle') ?? 'enable') as 'enable' | 'disable'; + const textChannel = interaction.options.getChannel('channel') ?? interaction.channel; + + const response = '' as string; + if (toggle === 'enable') { + if (!textChannel) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription('You must provide a text channel to link to.')], + + }); + return; + } + // response = await aiLink(personaName, textChannel.id, toggle); + + const personaData = await db.ai_personas.findUnique({ + where: { + name: personaName, + }, + }); + + if (!personaData) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(personaDoesNotExist)], + + }); + return; + } + + const aiLinkData = await db.ai_channels.findFirst({ + where: { + channel_id: textChannel.id, + }, + }); + + if (aiLinkData) { + await db.ai_channels.update({ + where: { + id: aiLinkData.id, + }, + data: { + channel_id: textChannel.id, + persona_id: personaData.id, + }, + }); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(`Success: The link between ${personaName} and <#${textChannel.id}> was updated!`)], + + }); + return; + } + + await db.ai_channels.create({ + data: { + channel_id: textChannel.id, + persona_id: personaData.id, + }, + }); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(`Success: The link between ${personaName} and <#${textChannel.id}> was created!`)], + + }); + return; + } + + if (textChannel) { + // response = await aiLink(personaName, textChannel.id, toggle); + const existingPersona = await db.ai_personas.findUnique({ + where: { + name: personaName, + }, + }); + + if (!existingPersona) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(personaDoesNotExist)], + + }); + return; + } + + let existingLink = await db.ai_channels.findFirst({ + where: { + channel_id: textChannel.id, + persona_id: existingPersona.id, + }, + }); + + if (!existingLink) { + existingLink = await db.ai_channels.findFirst({ + where: { + channel_id: textChannel.id, + }, + }); + + if (!existingLink) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(`Error: No link to <#${textChannel.id}> found!`)], + + }); + return; + } + const personaData = await db.ai_personas.findUnique({ + where: { + id: existingLink.persona_id, + }, + }); + if (!personaData) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription('Error: No persona found for this link!')], + + }); + return; + } + await db.ai_channels.delete({ + where: { + id: existingLink.id, + }, + }); + } + } else if (interaction.channel) { + const aiLinkData = await getLinkedChannel(interaction.channel); + if (aiLinkData) { + // response = await aiLink(personaName, aiLinkData?.channel_id, toggle); + + const existingPersona = await db.ai_personas.findUnique({ + where: { + name: personaName, + }, + }); - const aiUsageData = await db.ai_usage.upsert({ - where: { - user_id: userData.id, - }, - create: { - user_id: userData.id, - }, - update: {}, - }); + if (!existingPersona) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(personaDoesNotExist)], - embed.addFields( - { - name: 'Persona', - value: stripIndents`**${aiPersona.name} (${aiPersona.ai_model})** - ${aiPersona.prompt}`, - inline: false, - }, - { - name: 'Context', - value: stripIndents`${contextMessageOutput}`, - inline: false, - }, - { - name: 'Prompt', - value: stripIndents`${promptMessage.url} ${promptMessage.member?.displayName}: ${promptMessage.cleanContent}`, - inline: false, - }, - { - name: 'Result', - value: stripIndents`${chatResponse.slice(0, 1023)}`, - inline: false, - }, - { - name: 'Chat Tokens', - value: stripIndents`${promptTokens + completionTokens} Tokens \n($${(promptCost + completionCost).toFixed(6)})`, - inline: true, - }, - { - name: 'User Tokens', - value: `${aiUsageData.tokens} Tokens\n($${((aiUsageData.tokens / 1000) * aiCosts[aiPersona.ai_model].output).toFixed(6)})`, - inline: true, - }, - { - name: 'Persona Tokens', - value: `${aiPersona.total_tokens} Tokens\n($${((aiPersona.total_tokens / 1000) * aiCosts[aiPersona.ai_model].output).toFixed(6)})`, - inline: true, - }, - ); + }); + return; + } - // Get the channel to send the message to - const channelAiLog = await discordClient.channels.fetch(env.CHANNEL_AILOG) as TextChannel; + let existingLink = await db.ai_channels.findFirst({ + where: { + channel_id: aiLinkData.channel_id, + persona_id: existingPersona.id, + }, + }); - // Send the message - await channelAiLog.send({ embeds: [embed] }); -} + if (!existingLink) { + existingLink = await db.ai_channels.findFirst({ + where: { + channel_id: aiLinkData.channel_id, + }, + }); -export async function aiImageAudit( - imageGenerator: ai_model, - message: Message, - image: OpenAI.Images.Image, -) { - // This function takes what was sent and returned from the API and sends it to a discord channel - // for review. This is to ensure that the AI is not being used to break the rules. + if (!existingLink) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(`Error: No link to <#${aiLinkData.channel_id}> found!`)], - // const embed = await makePersonaEmbed(cleanPersona); - const embed = embedTemplate() - .setAuthor({ name: message.author.username, url: message.url, iconURL: message.author.displayAvatarURL() }) - .setColor(Colors.Yellow); + }); + return; + } + const personaData = await db.ai_personas.findUnique({ + where: { + id: existingLink.persona_id, + }, + }); + if (!personaData) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription('Error: No persona found for this link!')], - embed.addFields( - { - name: 'Prompt', - value: stripIndents`${message.cleanContent}`, - inline: false, - }, - { - name: 'Revised Prompt', - value: stripIndents`${image.revised_prompt}`, - inline: false, - }, - ); + }); + return; + } + await db.ai_channels.delete({ + where: { + id: existingLink.id, + }, + }); + } + } else { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription('This channel is not linked to an AI persona.')], - const userData = await db.users.upsert({ - where: { discord_id: message.author.id }, - create: { discord_id: message.author.id }, - update: { discord_id: message.author.id }, - }); + }); + return; + } + } else { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription('You must provide a text channel to link to.')], - const aiUsageData = await db.ai_usage.upsert({ - where: { - user_id: userData.id, - }, - create: { - user_id: userData.id, - }, - update: {}, - }); - const imagesGenerated = aiUsageData.images.length; + }); + return; + } - const allUsageData = await db.ai_usage.findMany(); - const totalImagesGenerated = allUsageData.reduce((acc, cur) => acc + cur.images.length, 0); + log.debug(F, `response: ${response}`); - embed.addFields( - { - name: 'Model', - value: stripIndents`${imageGenerator}`, - inline: true, - }, - { - name: 'User Images Generated', - value: `${imagesGenerated} ($${imagesGenerated * 0.04})`, - inline: true, - }, - { - name: 'Total Images Generated', - value: `${totalImagesGenerated} ($${totalImagesGenerated * 0.04})`, - inline: true, - }, - ); + if (!response.startsWith('Success')) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(response)], - embed.setImage(image.url as string); + }); + return; + } - // Get the channel to send the message to - const channelAiImageLog = await discordClient.channels.fetch(env.CHANNEL_AIIMAGELOG) as TextChannel; - // Send the message - await channelAiImageLog.send({ embeds: [embed] }); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Blurple) + .setDescription(response)], + }); } export async function discordAiModerate( @@ -1750,7 +1421,7 @@ export async function discordAiModerate( export async function discordAiChat( messageData: Message, ):Promise { - log.debug(F, `discordAiChat - messageData: ${JSON.stringify(messageData.cleanContent, null, 2)}`); + // log.debug(F, `discordAiChat - messageData: ${JSON.stringify(messageData.cleanContent, null, 2)}`); if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return; const channelMessages = await messageData.channel.messages.fetch({ limit: 10 }); @@ -1758,98 +1429,11 @@ export async function discordAiChat( const messages = [...channelMessages.values()]; - if (!messages[0].member?.roles.cache.has(env.ROLE_VERIFIED)) return; + // if (!messages[0].member?.roles.cache.has(env.ROLE_VERIFIED)) return; if (messages[0].author.bot) return; if (messages[0].cleanContent.length < 1) return; if (messages[0].channel.type === ChannelType.DM) return; - if (messageData.cleanContent.includes('imagen')) { - if (!messageData.member?.roles.cache.has(env.ROLE_PATRON) - && !messageData.member?.roles.cache.has(env.ROLE_TEAMTRIPSIT) - ) { - await messageData.reply('This beta feature is exclusive to active TripSit [Patreon](https://www.patreon.com/tripsit) subscribers.'); - return; - } - - const userData = await db.users.upsert({ - where: { discord_id: messageData.author.id }, - create: { discord_id: messageData.author.id }, - update: { discord_id: messageData.author.id }, - }); - - const aiUsageData = await db.ai_usage.upsert({ - where: { - user_id: userData.id, - }, - create: { - user_id: userData.id, - }, - update: {}, - }); - - // The usageData.images is a list of dates when images were generated - // Users are limited to 25 images a month, which is roughly 1$ a month - // So we create a list of images that were generated this month, and if it's - // greater than 25, we let the user know. - const imagesThisMonth = aiUsageData.images.filter(imageDate => { - // log.debug(F, `imageDate: ${imageDate}`); - const givenDate = DateTime.fromJSDate(imageDate); - const oneMonthAgo = DateTime.now().minus({ months: 1 }); - return givenDate > oneMonthAgo; - }).length; - - if (imagesThisMonth > imageLimit && messageData.author.id !== env.DISCORD_OWNER_ID) { - await messageData.reply(`While I love the enthusiasm, you have already generated ${imageLimit} images this month, and this API is rather expensive. Please try again next month.`); - return; - } - - const tripbotLog = await discordClient.channels.fetch(env.CHANNEL_BOTLOG) as TextChannel; - await tripbotLog.send(`${messageData.author.toString()} generated an image in ${messageData.url}`); - - let waitingOnGen = true; - createImage( - messageData.cleanContent.replace('imgen', '').replace(tripbotUAT, '').replace('tripbot', '').trim(), - messageData.author.id, - ) - .then(async response => { - log.debug(F, `createImage response: ${JSON.stringify(response, null, 2)}`); - waitingOnGen = false; - const { data } = response; - const [image] = data; - await aiImageAudit( - 'DALL_E_3' as ai_model, - messageData, - image, - ); - if (image.url) { - await messageData.reply( - { - embeds: [embedTemplate() - .setAuthor(null) - .setColor(null) - .setImage(image.url) - .setFooter({ text: `Beta feature only available to active TripSit Patreon subscribers (${imageLimit - imagesThisMonth} images left).` })], - }, - ); - } else { - await tripbotLog.send(`Error generating image: ${JSON.stringify(image, null, 2)}`); - } - }) - .catch(async error => { - waitingOnGen = false; - await tripbotLog.send(`Error generating image: ${JSON.stringify(error, null, 2)}`); - }); - // While the above function is running, send the typing function - while (waitingOnGen) { - // eslint-disable-next-line no-await-in-loop - await messageData.channel.sendTyping(); - // eslint-disable-next-line no-await-in-loop - await sleep(5000); - } - - return; - } - // Check if the channel is linked to a persona const aiLinkData = await getLinkedChannel(messages[0].channel); // log.debug(F, `aiLinkData: ${JSON.stringify(aiLinkData, null, 2)}`); @@ -1862,7 +1446,7 @@ export async function discordAiChat( id: aiLinkData.persona_id, }, }); - // log.debug(F, `aiPersona: ${aiPersona.name}`); + log.debug(F, `aiPersona: ${aiPersona.name}`); // Get the last 3 messages that are not empty or from other bots const messageList = messages @@ -1884,31 +1468,75 @@ export async function discordAiChat( .slice(0, maxHistoryLength) .reverse(); - const result = await aiChat(aiPersona, messageList, messageData.author.id); + const { response, promptTokens, completionTokens } = await aiChat(aiPersona, messageList, messageData.author.id); + + // Increment the tokens used + await db.ai_personas.update({ + where: { + id: aiPersona.id, + }, + data: { + total_tokens: { + increment: completionTokens + promptTokens, + }, + }, + }); + + const costUsd = (aiCosts[aiPersona.ai_model as keyof typeof aiCosts].input * promptTokens) + + (aiCosts[aiPersona.ai_model as keyof typeof aiCosts].output * completionTokens); + + const userData = await db.users.upsert({ + where: { discord_id: messageData.author.id }, + create: { discord_id: messageData.author.id }, + update: {}, + }); + + await db.ai_usage.upsert({ + where: { + user_id: userData.id, + }, + create: { + user_id: userData.id, + tokens: completionTokens + promptTokens, + usd: costUsd, + }, + update: { + usd: { + increment: costUsd, + }, + tokens: { + increment: completionTokens + promptTokens, + }, + }, + }); await aiAudit( aiPersona, cleanMessageList, - result.response, - result.promptTokens, - result.completionTokens, + response, + promptTokens, + completionTokens, ); await messages[0].channel.sendTyping(); - const wpm = 60; - const wordCount = result.response.split(' ').length; + const wpm = 120; + const wordCount = response.split(' ').length; const sleepTime = (wordCount / wpm) * 60000; - // log.debug(F, `Typing ${wordCount} at ${wpm} wpm will take ${sleepTime / 1000} seconds`); - await sleep(sleepTime > 10000 ? 10000 : sleepTime); // Dont wait more than 10 seconds - await messages[0].reply(result.response.slice(0, 2000)); + log.debug(F, `Typing ${wordCount} at ${wpm} wpm will take ${sleepTime / 1000} seconds`); + await sleep(sleepTime > 10000 ? 5000 : sleepTime); // Don't wait more than 5 seconds + const replyMessage = await messages[0].reply(response.slice(0, 2000)); + + // React to that message with thumbs up and thumbs down emojis + await replyMessage.react(env.EMOJI_THUMB_UP); + await replyMessage.react(env.EMOJI_THUMB_DOWN); } export async function discordAiConversate( messageData: Message, ):Promise { if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return; - log.debug(F, `discordAiConversate - messageData: ${JSON.stringify(messageData.cleanContent, null, 2)}`); + // log.debug(F, `discordAiConversate - messageData: ${JSON.stringify(messageData.cleanContent, null, 2)}`); if (!messageData.member?.roles.cache.has(env.ROLE_VERIFIED)) return; if (messageData.author.bot) return; @@ -2006,13 +1634,13 @@ export async function discordAiConversate( // No way to support additional actions at this time break; case 'expired': - // This will happen if the requires_action doesnt geta response in time, so this isnt supported either + // This will happen if the requires_action doesn't get a response in time, so this isn't supported either break; case 'cancelling': - // This would only happen if i manually cancel the request, which isnt supported + // This would only happen if i manually cancel the request, which isn't supported break; case 'cancelled': - // Takea guess =D + // Take a guess =D break; case 'failed': { // This should send an error to the dev @@ -2035,7 +1663,7 @@ export async function discordAiConversate( // }; // Listen for typing event - // This doesnt work for some reason + // This doesn't work for some reason // discordClient.on(Events.TypingStart, interaction => { // // if (interaction.user.id === messageData.author.id && interaction.channel.id === messageData.channel.id) { // log.debug(F, `Typing started: ${interaction.user.id}`); @@ -2087,41 +1715,10 @@ export async function aiModButton( export const aiCommand: SlashCommand = { data: new SlashCommandBuilder() .setName('ai') - .setDescription('Information and commands for TripBot\'s AI personas.') + .setDescription('TripBot\'s AI') .addSubcommand(subcommand => subcommand .setDescription('Information on the AI persona.') .setName('help')) - .addSubcommand(subcommand => subcommand - .setDescription('Set a create or update an AI persona') - .addStringOption(option => option.setName('name') - .setAutocomplete(true) - .setDescription('Name of the AI persona to modify/create.')) - .addStringOption(option => option.setName('model') - .setAutocomplete(true) - .setDescription('Which model to use.')) - .addNumberOption(option => option.setName('tokens') - .setDescription('Maximum tokens to use for this request (Default: 500).') - .setMaxValue(1000) - .setMinValue(100)) - .addNumberOption(option => option.setName('temperature') - .setDescription('Temperature value for the model.') - .setMaxValue(2) - .setMinValue(0)) - .addNumberOption(option => option.setName('top_p') - .setDescription('Top % value for the model. Use this OR temp.') - .setMaxValue(2) - .setMinValue(0)) - .addNumberOption(option => option.setName('presence_penalty') - .setDescription('Presence penalty value for the model.') - .setMaxValue(2) - .setMinValue(-2)) - .addNumberOption(option => option.setName('frequency_penalty') - .setDescription('Frequency penalty value for the model.') - .setMaxValue(2) - .setMinValue(-2)) - .addBooleanOption(option => option.setName('ephemeral') - .setDescription(ephemeralExplanation)) - .setName('upsert')) .addSubcommand(subcommand => subcommand .setDescription('Get information on the AI') .addStringOption(option => option.setName('name') @@ -2132,62 +1729,27 @@ export const aiCommand: SlashCommand = { .addBooleanOption(option => option.setName('ephemeral') .setDescription(ephemeralExplanation)) .setName('get')) - .addSubcommand(subcommand => subcommand - .setDescription('Remove an AI model.') - .addStringOption(option => option.setName('name') - .setDescription('Name of the AI persona to delete.') - .setAutocomplete(true)) - .addStringOption(option => option.setName('confirmation') - .setDescription('Code to confirm you want to delete')) - .addBooleanOption(option => option.setName('ephemeral') - .setDescription(ephemeralExplanation)) - .setName('del')) .addSubcommand(subcommand => subcommand .setDescription('Link an AI model to a channel.') .addStringOption(option => option.setName('name') - .setDescription('Name of the AI persona to link.') + .setDescription('AI persona to link. (Default: TripBot)') .setAutocomplete(true)) .addChannelOption(option => option.setName('channel') - .setDescription('ID or channel mention of the channel to link.')) + .setDescription('Channel, thread or category. (Default: This channel)')) .addStringOption(option => option.setName('toggle') - .setDescription('Should we enable to disable this link?') + .setDescription('Enable to disable this link? (Default: Enable)') .setChoices( { name: 'Enable', value: 'enable' }, { name: 'Disable', value: 'disable' }, )) .addBooleanOption(option => option.setName('ephemeral') .setDescription(ephemeralExplanation)) - .setName('link')) - .addSubcommand(subcommand => subcommand - .setDescription('Change moderation parameters.') - .addNumberOption(option => option.setName('harassment') - .setDescription('Set harassment limit.')) - .addNumberOption(option => option.setName('harassment_threatening') - .setDescription('Set harassment_threatening limit.')) - .addNumberOption(option => option.setName('hate') - .setDescription('Set hate limit.')) - .addNumberOption(option => option.setName('hate_threatening') - .setDescription('Set hate_threatening limit.')) - .addNumberOption(option => option.setName('self_harm') - .setDescription('Set self_harm limit.')) - .addNumberOption(option => option.setName('self_harm_instructions') - .setDescription('Set self_harm_instructions limit.')) - .addNumberOption(option => option.setName('self_harm_intent') - .setDescription('Set self_harm_intent limit.')) - .addNumberOption(option => option.setName('sexual') - .setDescription('Set sexual limit.')) - .addNumberOption(option => option.setName('sexual_minors') - .setDescription('Set sexual_minors limit.')) - .addNumberOption(option => option.setName('violence') - .setDescription('Set violence limit.')) - .addNumberOption(option => option.setName('violence_graphic') - .setDescription('Set violence_graphic limit.')) - .setName('mod')), + .setName('link')), async execute(interaction) { log.info(F, await commandContext(interaction)); - const command = interaction.options.getSubcommand().toUpperCase() as AiAction; + const command = interaction.options.getSubcommand().toUpperCase() as 'HELP' | 'GET' | 'LINK'; switch (command) { case 'HELP': await help(interaction); @@ -2195,18 +1757,9 @@ export const aiCommand: SlashCommand = { case 'GET': await get(interaction); break; - case 'UPSERT': - await upsert(interaction); - break; case 'LINK': await link(interaction); break; - case 'DEL': - await del(interaction); - break; - case 'MOD': - await mod(interaction); - break; default: help(interaction); break; diff --git a/src/discord/commands/global/d.image.ts b/src/discord/commands/global/d.image.ts new file mode 100644 index 000000000..30439767e --- /dev/null +++ b/src/discord/commands/global/d.image.ts @@ -0,0 +1,357 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import { + Colors, + SlashCommandBuilder, + ChatInputCommandInteraction, + TextChannel, + GuildMember, + time, +} from 'discord.js'; +import { stripIndents } from 'common-tags'; +import { + PrismaClient, +} from '@prisma/client'; +import { DateTime } from 'luxon'; +import { ImagesResponse } from 'openai/resources'; +import { SlashCommand } from '../../@types/commandDef'; +import { embedTemplate } from '../../utils/embedTemplate'; +import commandContext from '../../utils/context'; +import { aiModerateReport, createImage } from '../../../global/commands/g.ai'; + +const db = new PrismaClient({ log: ['error'] }); + +const F = f(__filename); + +const ephemeralExplanation = 'Set to "True" to show the response only to you'; +const imageLimits = { + [env.ROLE_TEAMTRIPSIT]: 20, + [env.ROLE_PATREON_TIER_0]: 20, + [env.ROLE_PATREON_TIER_1]: 40, + [env.ROLE_PATREON_TIER_2]: 60, + [env.ROLE_PATREON_TIER_3]: 80, + [env.ROLE_PATREON_TIER_4]: 100, +}; + +async function help( + interaction: ChatInputCommandInteraction, +):Promise { + const visible = interaction.options.getBoolean('ephemeral') !== false; + await interaction.deferReply({ ephemeral: !visible }); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Image Generation Help') + .setDescription(stripIndents` + 🌟 Welcome to TripBot's Advanced Image Generation! 🌟 + + 🎨 Harnessing the power of Dall-E 3, we transform your descriptions into stunning visuals. \ + Imagine the possibilities! + + πŸ”’ Exclusive Access for Our Supporters: This feature is a special thank you to our active TripSit Patreon \ + subscribers. Your support helps us cover the higher costs of this advanced technology (just 4 cents per image!). + + πŸ”„ Fair Usage Policy: As a Patreon subscriber, you're entitled to X images per rolling month. \ + Think of it as a 'credit' system: each image you generate counts against your monthly quota, \ + and you regain that 'credit' 30 days after each generation. This ensures fair access for everyone – \ + it's not a monthly reset, but a continuous cycle of creativity! + + 🧠 Enhanced for Creativity: To ensure the best results, we fine-tune your requests for optimal AI compatibility. \ + Expect more vivid and accurate interpretations of your ideas! + + πŸ“œ Play by the Rules: All requests must comply with OpenAI's terms and conditions. Let's keep creativity \ + responsible! + + πŸ” Respect and Responsibility: We log all requests for accountability. Misuse of this amazing tool will lead \ + to a ban from the bot. Let's maintain a respectful and imaginative community! + + πŸ› οΈ Control for Server Owners: Discord server owners have the power to enable or disable this command through the \ + app menu. We believe in giving you the control to tailor the experience to your community's needs. + + Happy imagining! πŸš€ + `)], + }); +} + +async function generate( + interaction: ChatInputCommandInteraction, +):Promise { + const visible = interaction.options.getBoolean('ephemeral') !== false; + await interaction.deferReply({ ephemeral: !visible }); + + const description = interaction.options.getString('description', true); + + if (!interaction.member) return; + + const guildMember = interaction.member as GuildMember; + + // Find if the user has any of the roles mentioned in imageLimits keys + const imageLimit = Object.entries(imageLimits).find(limit => guildMember.roles.cache.has(limit[0])); + + if (!imageLimit) { + await interaction.editReply( + 'This command is exclusive to active TripSit [Patreon]() subscribers.', + ); + return; + } + + const modReport = await aiModerateReport(description); + + log.debug(F, `modReport: ${JSON.stringify(modReport, null, 2)}`); + + const flagged = modReport.results.find(result => result.flagged === true); + + if (flagged) { + // Go through the flagged.categories and find the ones where the category value is true + const flaggedCategories = Object.entries(flagged.categories).filter(category => category[1] === true); + await interaction.editReply( + `Hold on there there bucko, this request violates OpenAI's usage policies regarding \ + ${flaggedCategories.map(category => category[0]).join(', ')}`, + ); + + return; + } + + const userData = await db.users.upsert({ + where: { discord_id: guildMember.id }, + create: { discord_id: guildMember.id }, + update: {}, + }); + + const aiImageData = await db.ai_images.findMany({ + where: { + user_id: userData.id, + }, + }); + + // The aiUsageData is a list of dates when images were generated + // We create a list of images that were generated this month. + const imagesThisMonth = aiImageData.filter(image => { + // log.debug(F, `imageDate: ${imageDate}`); + const givenDate = DateTime.fromJSDate(image.created_at); + const oneMonthAgo = DateTime.now().minus({ months: 1 }); + return givenDate > oneMonthAgo; + }).length; + + if (imagesThisMonth > imageLimit[1] && guildMember.id !== env.DISCORD_OWNER_ID) { + await interaction.editReply(stripIndents`Sorry, you have already generated ${imagesThisMonth} images this month. + Check out your /image library to see them all.`); + return; + } + + const channelAiImageLog = await discordClient.channels.fetch(env.CHANNEL_AIIMAGELOG) as TextChannel; + await channelAiImageLog + .send(`${guildMember.displayName} requested '${description}' in ${interaction.guild?.name}`); + + let imageData = {} as ImagesResponse; + + try { + imageData = await createImage( + description, + guildMember.id, + ); + } catch (error) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((error as any).error.code === 'content_policy_violation') { + await interaction.editReply(`Hold on there there bucko, this request violates OpenAI's usage policies. + Try again with something more wholesome.`); + await channelAiImageLog.send(`${guildMember.displayName}'s request violated OpenAI's usage policies.`); + return; + } + await channelAiImageLog.send( + `Error generating ${guildMember.displayName}'s image: ${JSON.stringify(error, null, 2)}`, + ); + } + + log.debug(F, `openAi responded response: ${JSON.stringify(imageData, null, 2)}`); + const { data } = imageData; + const [image] = data; + await db.ai_images.create({ + data: { + user_id: userData.id, + prompt: description, + revised_prompt: image.revised_prompt ?? '', + image_url: image.url as string, + model: 'DALL_E_3', + }, + }); + + const newAaiUsageData = await db.ai_images.findMany({ + where: { + user_id: userData.id, + }, + }); + + const imagesGenerated = newAaiUsageData.length; + + const allUsageData = await db.ai_images.findMany(); + const totalImagesGenerated = allUsageData.length; + + await channelAiImageLog.send({ + embeds: [ + embedTemplate() + .setAuthor({ + name: guildMember.user.username, + iconURL: guildMember.displayAvatarURL(), + }) + .setColor(Colors.Yellow) + .addFields( + { + name: 'Prompt', + value: stripIndents`${description}`, + inline: false, + }, + { + name: 'Revised Prompt', + value: stripIndents`${image.revised_prompt}`, + inline: false, + }, + { + name: 'Model', + value: 'DALL_E_3', + inline: true, + }, + { + name: 'User Images Generated', + value: `${imagesGenerated} ($${imagesGenerated * 0.04})`, + inline: true, + }, + { + name: 'Total Images Generated', + value: `${totalImagesGenerated} ($${totalImagesGenerated * 0.04})`, + inline: true, + }, + ) + .setImage(image.url as string), + ], + }); + + await interaction.editReply( + { + embeds: [embedTemplate() + .setAuthor(null) + .setColor(null) + .setImage(image.url as string) + .setFooter({ + text: stripIndents`Beta feature only available to active TripSit Patreon subscribers \ + (${imageLimit[1] - imagesThisMonth} images left).`, + })], + }, + ); +} + +async function library( + interaction: ChatInputCommandInteraction, +):Promise { + const visible = interaction.options.getBoolean('ephemeral') !== false; + await interaction.deferReply({ ephemeral: !visible }); + if (!interaction.member) return; + + const guildMember = interaction.member as GuildMember; + + const userData = await db.users.upsert({ + where: { discord_id: guildMember.id }, + create: { discord_id: guildMember.id }, + update: {}, + }); + + const aiImageData = await db.ai_images.findMany({ + where: { + user_id: userData.id, + }, + }); + + const imagesGenerated = aiImageData.length; + + // Create a string that tells the user the details on each image they have generated + + const imageDetails = aiImageData.map(image => { + const createdDate = DateTime.fromJSDate(image.created_at).toUnixInteger(); + return `${time(createdDate, 'R')} - [${image.prompt}](<${image.image_url}>)`; + }); + + if (imageDetails.length === 0) { + await interaction.editReply({ + embeds: [ + embedTemplate() + .setAuthor({ + name: guildMember.user.username, + iconURL: guildMember.displayAvatarURL(), + }) + .setColor(Colors.Yellow) + .setDescription('You have not generated any images yet.') + .addFields( + { + name: 'Images Generated', + value: `${imagesGenerated} ($${imagesGenerated * 0.04})`, + inline: true, + }, + ), + ], + }); + return; + } + + await interaction.editReply({ + embeds: [ + embedTemplate() + .setAuthor({ + name: guildMember.user.username, + iconURL: guildMember.displayAvatarURL(), + }) + .setColor(Colors.Yellow) + .setDescription(imageDetails.join('\n')) + .addFields( + { + name: 'Images Generated', + value: `${imagesGenerated} ($${imagesGenerated * 0.04})`, + inline: true, + }, + ), + ], + }); +} + +export const image: SlashCommand = { + data: new SlashCommandBuilder() + .setName('image') + .setDescription('TripBot\'s Image Generator') + .addSubcommand(subcommand => subcommand + .setDescription('Information on the imagen function.') + .setName('help')) + .addSubcommand(subcommand => subcommand + .setDescription('Generate an image') + .addStringOption(option => option.setName('description') + .setDescription('Describe your image.') + .setRequired(true)) + .addBooleanOption(option => option.setName('ephemeral') + .setDescription(ephemeralExplanation)) + .setName('generate')) + .addSubcommand(subcommand => subcommand + .setDescription('Returns all images you have generated') + .addBooleanOption(option => option.setName('ephemeral') + .setDescription(ephemeralExplanation)) + .setName('library')), + + async execute(interaction) { + log.info(F, await commandContext(interaction)); + + const command = interaction.options.getSubcommand().toUpperCase() as 'HELP' | 'GENERATE' | 'LIBRARY'; + switch (command) { + case 'HELP': + await help(interaction); + break; + case 'GENERATE': + await generate(interaction); + break; + case 'LIBRARY': + await library(interaction); + break; + default: + help(interaction); + break; + } + + return true; + }, +}; + +export default image; diff --git a/src/discord/commands/guild/d.aiManage.ts b/src/discord/commands/guild/d.aiManage.ts new file mode 100644 index 000000000..f6a989552 --- /dev/null +++ b/src/discord/commands/guild/d.aiManage.ts @@ -0,0 +1,641 @@ +/* eslint-disable sonarjs/no-duplicate-string */ +import { + ActionRowBuilder, + ModalBuilder, + TextInputBuilder, + Colors, + SlashCommandBuilder, + ModalSubmitInteraction, + ChatInputCommandInteraction, + TextChannel, + ThreadChannel, + time, +} from 'discord.js'; +import { + TextInputStyle, +} from 'discord-api-types/v10'; +import { stripIndents } from 'common-tags'; +import { + PrismaClient, + ai_model, + ai_personas, +} from '@prisma/client'; +import { paginationEmbed } from '../../utils/pagination'; +import { SlashCommand } from '../../@types/commandDef'; +import { embedTemplate } from '../../utils/embedTemplate'; +import commandContext from '../../utils/context'; + +const db = new PrismaClient({ log: ['error'] }); + +const F = f(__filename); + +const ephemeralExplanation = 'Set to "True" to show the response only to you'; +const confirmationCodes = new Map(); + +// Costs per 1k tokens +const aiCosts = { + GPT_3_5_TURBO: { + input: 0.0015, + output: 0.002, + }, + GPT_3_5_TURBO_1106: { + input: 0.001, + output: 0.002, + }, + GPT_4: { + input: 0.03, + output: 0.06, + }, + GPT_4_1106_PREVIEW: { + input: 0.01, + output: 0.03, + }, + GPT_4_1106_VISION_PREVIEW: { + input: 0.01, + output: 0.03, + }, + DALL_E_2: { + input: 0.00, + output: 0.04, + }, + DALL_E_3: { + input: 0.00, + output: 0.02, + }, +} as { + [key in ai_model]: { + input: number, + output: number, + } +}; + +async function manageHelp( + interaction: ChatInputCommandInteraction, +):Promise { + const visible = interaction.options.getBoolean('ephemeral') !== false; + await interaction.deferReply({ ephemeral: !visible }); + + const aboutEmbed = embedTemplate() + .setTitle('AI Manage Help') + .setDescription(stripIndents` + Welcome to TripBot's AI module! + + This is not a real AI, this is a Language Learning Model (LLM). + It does not provide any kind of "intelligence", it just knows how to make a sentence that sounds good. + As such, this feature will likely not be introduced to the trip sitting rooms, as it is not 100% trustworthy. + But we can still have some fun and play with it in the social rooms! + Here's how you can do that: + + **/ai set** + This command is used to set the parameters of an AI persona, or create a new persona. + A persona is how the bot will respond to queries. We can have multiple personas, each with their own parameters. + The parameters are explained on the next page. + EG: We can have a "helpful" persona, and a "funny" persona, and a "serious" persona, etc. + **Anyone is welcome to create their own persona in the dev guild!** + + **/ai get** + This command is used to get the parameters of an AI persona. + You can either get the specific name of the persona + Or you can enter a channel name to get which persona is linked to that channel. + + **/ai del** + To delete a persona. You must provide a confirmation code to delete a persona. + + **/ai link** + You can link threads, channels, and even entire categories to a persona. + This allows the bot to respond to messages in those channels with the persona you set. + You can also disable the link, so the bot will not respond in that channel. + `); + + const parametersEmbed = embedTemplate() + .setTitle('AI Help') + /* eslint-disable max-len */ + .setDescription(stripIndents` + This command is used to set the parameters of an AI persona, or create a new persona. + The parameters are: + **Name** + > The name of the persona. This is used to identify the persona in the AI Get command. + **Model** + > The model to use for the persona. This is a dropdown list of available models. + **Prompt** + > The prompt to use for the persona. This is the text that the AI will use to generate responses. + **Max Tokens** + > The maximum number of tokens to use for the AI response. + > What are tokens? + **Temperature** + > Adjusts the randomness of the AI's answers. + > Lower values make the AI more predictable and focused, while higher values make it more random and varied. + > *Use this OR Top P, not both.* + **Top P** + > Limits the word choices the AI considers. + > Using a high value means the AI picks from the most likely words, while a lower value allows for more variety but might include less common words. + > *Use this OR Temperature, not both.* + **Presence Penalty** + > This adjusts the probability of words that are not initially very likely to appear, by making them more likely. + > A higher value can make a response more creative or diverse, as it promotes the presence of words or phrases that the model might not normally prioritize. + > For example, if you're looking for creative or unconventional answers, increasing the presence penalty can make the model consider a wider range of vocabularies. + **Frequency Penalty** + > This penalizes words or phrases that appear repeatedly in the output. + > A positive value reduces the likelihood of repetition, which can be useful if you're noticing that the model is repeating certain phrases or words too often. + > Conversely, a negative value would increase the chance of repetition, which might be useful if you want the model to emphasize a certain point. + **Logit Bias** + > How often to bias certain tokens. This is a JSON list. + > *You can likely ignore this unless you really wanna tweak the AI.* + `); + + paginationEmbed(interaction, [aboutEmbed, parametersEmbed]); +} + +async function upsert( + interaction: ChatInputCommandInteraction, +):Promise { + const personaName = interaction.options.getString('name') ?? interaction.user.username; + + // Validations on the given information + // Name must be < 50 characters + if (personaName.length > 50) { + embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription('The name of the AI persona must be less than 50 characters.'); + return; + } + + const existingPersona = await db.ai_personas.findFirst({ + where: { + name: personaName, + }, + }); + + const modal = new ModalBuilder() + .setCustomId(`aiPromptModal~${interaction.id}`) + .setTitle('Modal') + .addComponents(new ActionRowBuilder() + .addComponents(new TextInputBuilder() + .setCustomId('prompt') + .setPlaceholder(stripIndents` + You are a harm reduction assistant and should only give helpful, non-judgemental advice. + `) + .setValue(existingPersona?.prompt ?? '') + .setLabel('Prompt (Personality)') + .setStyle(TextInputStyle.Paragraph))); + + await interaction.showModal(modal); + + const filter = (i:ModalSubmitInteraction) => i.customId.includes('aiPromptModal'); + interaction.awaitModalSubmit({ filter, time: 0 }) + .then(async i => { + if (i.customId.split('~')[1] !== interaction.id) return; + await i.deferReply({ ephemeral: (interaction.options.getBoolean('ephemeral') === true) }); + + // Get values + let temperature = interaction.options.getNumber('temperature'); + const topP = interaction.options.getNumber('top_p'); + // log.debug(F, `temperature: ${temperature}, top_p: ${topP}`); + + // If both temperature and top_p are set, throw an error + if (temperature && topP) { + // log.debug(F, 'Both temperature and top_p are set'); + embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription('You can only set one of temperature or top_p.'); + return; + } + + // If both temperature and top_p are NOT set, set temperature to 1 + if (!temperature && !topP) { + // log.debug(F, 'Neither temperature nor top_p are set'); + temperature = 1; + } + + const userData = await db.users.upsert({ + where: { discord_id: interaction.user.id }, + create: { discord_id: interaction.user.id }, + update: { discord_id: interaction.user.id }, + }); + + const aiPersona = { + name: personaName, + ai_model: interaction.options.getString('model') as ai_model ?? 'GPT_3_5_TURBO', + prompt: i.fields.getTextInputValue('prompt'), + temperature, + top_p: topP, + presence_penalty: interaction.options.getNumber('presence_penalty') ?? 0, + frequency_penalty: interaction.options.getNumber('frequency_penalty') ?? 0, + max_tokens: interaction.options.getNumber('tokens') ?? 500, + created_by: existingPersona ? existingPersona.created_by : userData.id, + created_at: existingPersona ? existingPersona.created_at : new Date(), + } as ai_personas; + + const alreadyExists = await db.ai_personas.findFirst({ + where: { + name: aiPersona.name, + }, + }); + const action = alreadyExists ? 'updated' : 'created'; + + await db.ai_personas.upsert({ + where: { + name: aiPersona.name, + }, + create: aiPersona, + update: aiPersona, + }); + + await i.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(`Success! This persona has been ${action}!`)], + }); + }); +} + +async function del( + interaction: ChatInputCommandInteraction, +):Promise { + const visible = interaction.options.getBoolean('ephemeral') !== false; + await interaction.deferReply({ ephemeral: !visible }); + const confirmation = interaction.options.getString('confirmation'); + const personaName = interaction.options.getString('name') ?? interaction.user.username; + + if (!confirmation) { + const aiPersona = await db.ai_personas.findUnique({ + where: { + name: personaName, + }, + }); + + if (!aiPersona) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('AI Del') + .setDescription(stripIndents` + The **"${personaName}"** persona does not exist! + + Make sure you /ai set it first! + `)], + }); + return; + } + + // If the user did not provide a confirmation code, generate a new code and assign it to the user + const code = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + confirmationCodes.set(`${interaction.user.id}${interaction.user.username}`, code); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('AI Del') + .setDescription(` + Are you sure you want to delete the "${personaName}" AI persona? + + This action is irreversible, don't regret it later! + + If you're sure, please run the command again with the confirmation code: + + **${code}** + + `)], + }); + return; + } + + // If the user did provide a confirmation code, check if it matches the one in confirmationCodes + if (confirmationCodes.get(`${interaction.user.id}${interaction.user.username}`) !== confirmation) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('AI Del') + .setDescription(stripIndents`The confirmation code you provided was incorrect. + If you want to delete this AI persona, please run the command again and provide the correct code.`)], + }); + return; + } + + await db.ai_personas.delete({ + where: { + name: personaName, + }, + }); + + confirmationCodes.delete(`${interaction.user.id}${interaction.user.username}`); + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Blurple) + .setDescription('Success: Persona was deleted!')], + }); +} + +async function mod( + interaction: ChatInputCommandInteraction, +):Promise { + if (!interaction.guild) return; + await interaction.deferReply({ ephemeral: true }); + + const moderationData = await db.ai_moderation.upsert({ + where: { + guild_id: interaction.guild.id, + }, + create: { + guild_id: interaction.guild.id, + }, + update: {}, + }); + + await db.ai_moderation.update({ + where: { + guild_id: interaction.guild.id, + }, + data: { + harassment: interaction.options.getNumber('harassment') ?? moderationData.harassment, + harassment_threatening: interaction.options.getNumber('harassment_threatening') ?? moderationData.harassment_threatening, + hate: interaction.options.getNumber('hate') ?? moderationData.hate, + hate_threatening: interaction.options.getNumber('hate_threatening') ?? moderationData.hate_threatening, + self_harm: interaction.options.getNumber('self_harm') ?? moderationData.self_harm, + self_harm_instructions: interaction.options.getNumber('self_harm_instructions') ?? moderationData.self_harm_instructions, + self_harm_intent: interaction.options.getNumber('self_harm_intent') ?? moderationData.self_harm_intent, + sexual: interaction.options.getNumber('sexual') ?? moderationData.sexual, + sexual_minors: interaction.options.getNumber('sexual_minors') ?? moderationData.sexual_minors, + violence: interaction.options.getNumber('violence') ?? moderationData.violence, + violence_graphic: interaction.options.getNumber('violence_graphic') ?? moderationData.violence_graphic, + }, + }); +} + +async function makePersonaEmbed( + persona: ai_personas, +) { + const createdBy = await db.users.findUniqueOrThrow({ where: { id: persona.created_by } }); + const guild = await discordClient.guilds.fetch(env.DISCORD_GUILD_ID); + const createdByMember = await guild.members.fetch(createdBy.discord_id as string); + + const totalCost = (persona.total_tokens / 1000) * aiCosts[persona.ai_model].output; + + return embedTemplate() + .setTitle(`Interaction with '${persona.name}' persona:`) + .setColor(Colors.Blurple) + .setFields([ + { + name: 'Prompt', + value: persona.prompt, + inline: false, + }, + { + name: 'Model', + value: persona.ai_model, + inline: true, + }, + { + name: 'Max Tokens', + value: persona.max_tokens.toString(), + inline: true, + }, + { + name: 'Temperature', + value: persona.temperature ? persona.temperature.toString() : 'N/A', + inline: true, + }, + { + name: 'Presence Penalty', + value: persona.presence_penalty.toString(), + inline: true, + }, + { + name: 'Frequency Penalty', + value: persona.frequency_penalty.toString(), + inline: true, + }, + { + name: 'Top P', + value: persona.top_p ? persona.top_p.toString() : 'N/A', + inline: true, + }, + { + name: 'Logit Bias', + value: `${persona.logit_bias}`, + inline: false, + }, + { + name: 'Created At', + value: time(persona.created_at, 'R'), + inline: true, + }, + { + name: 'Created By', + value: `<@${createdByMember.id}>`, + inline: true, + }, + { + name: 'Total Tokens', + value: `$${(totalCost).toFixed(6)}\n(${persona.total_tokens} tokens)`, + inline: true, + }, + ]); +} + +async function get( + interaction: ChatInputCommandInteraction, +):Promise { + const visible = interaction.options.getBoolean('ephemeral') !== false; + await interaction.deferReply({ ephemeral: !visible }); + const modelName = interaction.options.getString('name'); + const channel = interaction.options.getChannel('channel') ?? interaction.channel; + + let aiPersona:ai_personas | null = null; + let description = '' as string; + if (modelName) { + aiPersona = await db.ai_personas.findUnique({ + where: { + name: modelName, + }, + }); + } else if (channel) { + // Check if the channel is linked to a persona + let aiLinkData = await db.ai_channels.findFirst({ + where: { + channel_id: channel.id, + }, + }); + if (aiLinkData) { + log.debug(F, `Found aiLinkData on first go: ${JSON.stringify(aiLinkData, null, 2)}`); + aiPersona = await db.ai_personas.findUnique({ + where: { + id: aiLinkData.persona_id, + }, + }); + if (aiPersona) { + // eslint-disable-next-line max-len + description = `Channel ${(channel as TextChannel).name} is linked with the **"${aiPersona.name ?? aiPersona}"** persona:`; + } + } + + if (!aiLinkData && (channel as ThreadChannel).parent) { + log.debug(F, 'Channel is a Thread'); + // If the channel isn't listed in the database, check the parent + aiLinkData = await db.ai_channels.findFirst({ + where: { + channel_id: (channel as ThreadChannel).parent?.id, + }, + }); + if (aiLinkData) { + aiPersona = await db.ai_personas.findUnique({ + where: { + id: aiLinkData.persona_id, + }, + }); + if (aiPersona) { + // eslint-disable-next-line max-len + description = `Parent category/channel ${(channel as ThreadChannel).parent} is linked with the **"${aiPersona.name}"** persona:`; + } + } + } + + if (!aiLinkData && (channel as ThreadChannel).parent && (channel as ThreadChannel).parent?.parent) { + log.debug(F, 'Channel is a Thread, and no channel was set'); + // Threads have a parent channel, which has a parent category + aiLinkData = await db.ai_channels.findFirst({ + where: { + channel_id: (channel as ThreadChannel).parent?.parent?.id, + }, + }); + if (aiLinkData) { + aiPersona = await db.ai_personas.findUnique({ + where: { + id: aiLinkData.persona_id, + }, + }); + if (aiPersona) { + // eslint-disable-next-line max-len + description = `Parent category ${(channel as ThreadChannel).parent?.parent} is linked with the **"${aiPersona.name}"** persona:`; + } + } + } + } + + // log.debug(F, `aiPersona: ${JSON.stringify(aiPersona, null, 2)}`); + + if (!aiPersona) { + await interaction.editReply({ + embeds: [embedTemplate() + .setTitle('Modal') + .setColor(Colors.Red) + .setDescription(` + There was an error retrieving the AI persona. + It likely does not exist? + `)], + + }); + return; + } + + const personaEmbed = await makePersonaEmbed(aiPersona); + if (description) personaEmbed.setDescription(description); + await interaction.editReply({ + embeds: [personaEmbed], + }); +} + +export const aiCommand: SlashCommand = { + data: new SlashCommandBuilder() + .setName('ai_manage') + .setDescription('Information and commands for TripBot\'s AI personas.') + .addSubcommand(subcommand => subcommand + .setDescription('Information on the AI persona.') + .setName('help')) + .addSubcommand(subcommand => subcommand + .setDescription('Set a create or update an AI persona') + .addStringOption(option => option.setName('name') + .setAutocomplete(true) + .setDescription('Name of the AI persona to modify/create.')) + .addStringOption(option => option.setName('model') + .setAutocomplete(true) + .setDescription('Which model to use.')) + .addNumberOption(option => option.setName('tokens') + .setDescription('Maximum tokens to use for this request (Default: 500).') + .setMaxValue(1000) + .setMinValue(100)) + .addNumberOption(option => option.setName('temperature') + .setDescription('Temperature value for the model.') + .setMaxValue(2) + .setMinValue(0)) + .addNumberOption(option => option.setName('top_p') + .setDescription('Top % value for the model. Use this OR temp.') + .setMaxValue(2) + .setMinValue(0)) + .addNumberOption(option => option.setName('presence_penalty') + .setDescription('Presence penalty value for the model.') + .setMaxValue(2) + .setMinValue(-2)) + .addNumberOption(option => option.setName('frequency_penalty') + .setDescription('Frequency penalty value for the model.') + .setMaxValue(2) + .setMinValue(-2)) + .addBooleanOption(option => option.setName('ephemeral') + .setDescription(ephemeralExplanation)) + .setName('upsert')) + .addSubcommand(subcommand => subcommand + .setDescription('Remove an AI model.') + .addStringOption(option => option.setName('name') + .setDescription('Name of the AI persona to delete.') + .setAutocomplete(true)) + .addStringOption(option => option.setName('confirmation') + .setDescription('Code to confirm you want to delete')) + .addBooleanOption(option => option.setName('ephemeral') + .setDescription(ephemeralExplanation)) + .setName('del')) + .addSubcommand(subcommand => subcommand + .setDescription('Change moderation parameters.') + .addNumberOption(option => option.setName('harassment') + .setDescription('Set harassment limit.')) + .addNumberOption(option => option.setName('harassment_threatening') + .setDescription('Set harassment_threatening limit.')) + .addNumberOption(option => option.setName('hate') + .setDescription('Set hate limit.')) + .addNumberOption(option => option.setName('hate_threatening') + .setDescription('Set hate_threatening limit.')) + .addNumberOption(option => option.setName('self_harm') + .setDescription('Set self_harm limit.')) + .addNumberOption(option => option.setName('self_harm_instructions') + .setDescription('Set self_harm_instructions limit.')) + .addNumberOption(option => option.setName('self_harm_intent') + .setDescription('Set self_harm_intent limit.')) + .addNumberOption(option => option.setName('sexual') + .setDescription('Set sexual limit.')) + .addNumberOption(option => option.setName('sexual_minors') + .setDescription('Set sexual_minors limit.')) + .addNumberOption(option => option.setName('violence') + .setDescription('Set violence limit.')) + .addNumberOption(option => option.setName('violence_graphic') + .setDescription('Set violence_graphic limit.')) + .setName('mod')), + + async execute(interaction) { + log.info(F, await commandContext(interaction)); + + const command = interaction.options.getSubcommand().toUpperCase() as 'HELP' | 'UPSERT' | 'GET' | 'DEL' | 'MOD'; + switch (command) { + case 'HELP': + await manageHelp(interaction); + break; + case 'GET': + await get(interaction); + break; + case 'UPSERT': + await upsert(interaction); + break; + case 'DEL': + await del(interaction); + break; + case 'MOD': + await mod(interaction); + break; + default: + manageHelp(interaction); + break; + } + + return true; + }, +}; + +export default aiCommand; diff --git a/src/discord/events/autocomplete.ts b/src/discord/events/autocomplete.ts index da7884267..b7988e6d7 100644 --- a/src/discord/events/autocomplete.ts +++ b/src/discord/events/autocomplete.ts @@ -511,11 +511,15 @@ async function autocompleteAiNames(interaction:AutocompleteInteraction) { ], }; - const nameList = await db.ai_personas.findMany({ - select: { - name: true, - }, - }); + const nameList = interaction.guild?.id === env.DISCORD_GUILD_ID + ? await db.ai_personas.findMany({ + select: { + name: true, + }, + }) + : [{ + name: 'tripbot', + }]; const fuse = new Fuse(nameList, options); const focusedValue = interaction.options.getFocused(); @@ -561,7 +565,7 @@ export async function autocomplete(interaction:AutocompleteInteraction):Promise< autocompleteConvert(interaction); } else if (interaction.commandName === 'reaction_role') { autocompleteColors(interaction); - } else if (interaction.commandName === 'ai') { + } else if (interaction.commandName === 'ai' || interaction.commandName === 'ai_manage') { const focusedOption = interaction.options.getFocused(true).name; if (focusedOption === 'model') { autocompleteAiModels(interaction); diff --git a/src/discord/events/buttonClick.ts b/src/discord/events/buttonClick.ts index 39942a46d..2d2c95a14 100644 --- a/src/discord/events/buttonClick.ts +++ b/src/discord/events/buttonClick.ts @@ -25,7 +25,7 @@ import { import { helperButton } from '../commands/global/d.setup'; import { appealAccept, appealReject } from '../utils/appeal'; import { mushroomPageOne, mushroomPageTwo } from '../commands/global/d.mushroom_info'; -import { aiModButton } from '../commands/guild/d.ai'; +import { aiModButton } from '../commands/global/d.ai'; const F = f(__filename); diff --git a/src/discord/events/messageCreate.ts b/src/discord/events/messageCreate.ts index fc82582be..8a140bcf0 100644 --- a/src/discord/events/messageCreate.ts +++ b/src/discord/events/messageCreate.ts @@ -18,7 +18,7 @@ import { imagesOnly } from '../utils/imagesOnly'; import { countMessage } from '../commands/guild/d.counting'; import { bridgeMessage } from '../utils/bridge'; // import { discordAiConversate, discordAiModerate } from '../commands/guild/d.ai'; -import { discordAiModerate } from '../commands/guild/d.ai'; +import { discordAiModerate } from '../commands/global/d.ai'; // import { awayMessage } from '../utils/awayMessage'; // import log from '../../global/utils/log'; // import {parse} from 'path'; diff --git a/src/discord/events/messageReactionAdd.ts b/src/discord/events/messageReactionAdd.ts index cb5302dee..464e4f506 100644 --- a/src/discord/events/messageReactionAdd.ts +++ b/src/discord/events/messageReactionAdd.ts @@ -2,12 +2,15 @@ // MessageReaction, // User, // } from 'discord.js'; +import { stripIndents } from 'common-tags'; +import { TextChannel } from 'discord.js'; import { MessageReactionAddEvent, } from '../@types/eventDef'; import { chitragupta } from '../utils/chitragupta'; import { bestOf } from '../utils/bestOfTripsit'; import { updatePollEmbed } from '../commands/global/d.poll'; +import { embedTemplate } from '../utils/embedTemplate'; // import log from '../../global/utils/log'; // import {parse} from 'path'; const F = f(__filename); // eslint-disable-line @typescript-eslint/no-unused-vars @@ -15,9 +18,66 @@ const F = f(__filename); // eslint-disable-line @typescript-eslint/no-unused-var export const messageReactionAdd: MessageReactionAddEvent = { name: 'messageReactionAdd', async execute(messageReaction, user) { + await messageReaction.fetch(); + await messageReaction.message.fetch(); // Get the message object so that we can do stuff between restarts + if (!messageReaction.message.guild) return; // Ignore DMs + log.info(F, stripIndents`${user} added ${messageReaction.emoji.name} on to \ + ${messageReaction.message.author?.displayName}'s message`); + // AI audit stuff comes first cuz this can happen on other guilds + // We want to collect every message tripbot sends that gets three thumbs downs + const thumbsUpEmojis = ['πŸ‘', 'πŸ‘πŸ»', 'πŸ‘πŸΌ', 'πŸ‘πŸ½', 'πŸ‘πŸΎ', 'πŸ‘πŸΏ', 'ts_thumbup']; + const thumbsDownEmojis = ['πŸ‘Ž', 'πŸ‘ŽπŸ»', 'πŸ‘ŽπŸΌ', 'πŸ‘ŽπŸ½', 'πŸ‘ŽπŸΎ', 'πŸ‘ŽπŸΏ', 'ts_thumbdown']; + if (messageReaction.message.author?.bot + && (thumbsUpEmojis.includes(messageReaction.emoji.name as string) + || thumbsDownEmojis.includes(messageReaction.emoji.name as string) + ) + ) { + log.debug(F, `Someone reacted to tripbot's message with an audit emoji (${messageReaction.emoji.name})`); + + const auditLimit = env.NODE_ENV === 'production' ? 4 : 2; + log.debug(F, `Audit limit is ${auditLimit}, emoji count is ${messageReaction.count}`); + if (messageReaction.count === auditLimit) { + log.debug(F, `Audit limit reached (${auditLimit})`); + + const action = thumbsUpEmojis.includes(messageReaction.emoji.name as string) ? 'approve' : 'reject'; + const message = thumbsUpEmojis.includes(messageReaction.emoji.name as string) + ? stripIndents`${messageReaction.message.cleanContent} + + **Thank you for your feedback, I have notified Moonbear that this response was excellent.**` + : stripIndents`~~${messageReaction.message.cleanContent}~~ + + **Thank you for your feedback, I have notified Moonbear that this response was improper.**`; + + // This happens before the message is edited, so we need to fetch the original message + const channelAiLog = await discordClient.channels.fetch(env.CHANNEL_AILOG) as TextChannel; + const originalMessage = await messageReaction.message.fetchReference(); + const ownerMention = `<@${env.DISCORD_OWNER_ID}>`; + await channelAiLog.send({ + content: stripIndents` + AI response ${action} by ${messageReaction.message.guild.name} ${action === 'reject' ? ownerMention : ''}`, + embeds: [embedTemplate() + .setTitle(`AI ${action}`) + .setDescription(stripIndents` + ${originalMessage.author.displayName} (${originalMessage.author.id}): + \`${originalMessage.cleanContent}\` + + TripBot: + \`${messageReaction.message.cleanContent}\` + + This was deemed ${action === 'reject' ? 'improper' : 'excellent'} + `)], + }); + + await messageReaction.message.edit(message); + + // Remove the emojis so someone can't just toggle it on and off + await messageReaction.message.reactions.removeAll(); + } + return; + } + // Only run on Tripsit, we don't want to snoop on other guilds ( Ν‘~ ΝœΚ– Ν‘Β°) if (messageReaction.message.guild?.id !== env.DISCORD_GUILD_ID) return; - // log.info(F, `${user} added a reaction`); // Don't run on bots if (user.bot) { @@ -25,19 +85,6 @@ export const messageReactionAdd: MessageReactionAddEvent = { return; } - // When a reaction is received, check if the structure is partial - if (messageReaction.partial) await messageReaction.message.fetch(); - - // log.info(F, `${user.username} (${user.id}) added ${reaction.emoji.name}`); - - // log.debug(F, `reaction: ${JSON.stringify(reaction.emoji.name, null, 2)}`); - // log.debug(F, `users: ${JSON.stringify(reaction.users, null, 2)}`); - - // if (reaction.message.author?.bot) { - // // log.debug(F, `Ignoring bot interaction`); - // return; - // } - chitragupta(messageReaction, user, 1); bestOf(messageReaction); updatePollEmbed(messageReaction); diff --git a/src/discord/utils/messageCommand.ts b/src/discord/utils/messageCommand.ts index bb7229996..207ae7c86 100644 --- a/src/discord/utils/messageCommand.ts +++ b/src/discord/utils/messageCommand.ts @@ -9,7 +9,7 @@ import { import { stripIndents } from 'common-tags'; import { PrismaClient } from '@prisma/client'; import { sleep } from '../commands/guild/d.bottest'; -import { discordAiChat } from '../commands/guild/d.ai'; +import { discordAiChat } from '../commands/global/d.ai'; const db = new PrismaClient({ log: ['error'] }); @@ -68,7 +68,7 @@ async function isPokingTripbot(message:Message):Promise { } async function isMentioningTripbot(message:Message):Promise { - return message.mentions.has(env.DISCORD_CLIENT_ID); + return message.mentions.users.has(env.DISCORD_CLIENT_ID); } async function isUploadMessage(message:Message):Promise { @@ -77,10 +77,10 @@ async function isUploadMessage(message:Message):Promise { || message.content.toLowerCase().includes('fetch'); } -async function isAiEnabledGuild(message:Message):Promise { - // log.debug(F, `message.guild?.id: ${message.guild?.id}`); - return message.guild?.id === env.DISCORD_GUILD_ID; -} +// async function isAiEnabledGuild(message:Message):Promise { +// // log.debug(F, `message.guild?.id: ${message.guild?.id}`); +// return message.guild?.id === env.DISCORD_GUILD_ID; +// } async function isBotOwner(message:Message):Promise { return message.author.id === env.DISCORD_OWNER_ID; @@ -95,7 +95,7 @@ export async function messageCommand(message: Message): Promise { if (!message.guild) return; // If not in a guild then ignore all messages // if (message.guild.id !== env.DISCORD_GUILD_ID) return; // If not in tripsit ignore all messages const displayName = message.member ? message.member.displayName : message.author.username; - // log.debug(F, `message.reference: ${JSON.stringify(message.content, null, 2)}`); + // log.debug(F, `message: ${JSON.stringify(message, null, 2)}`); // Ignore messages that start with ~~, these are usually strikethrough messages if (message.content.startsWith('~~')) { return; } @@ -301,7 +301,7 @@ give people a chance to answer πŸ˜„ If no one answers in 5 minutes you can try a await message.channel.send(`Uploaded ${stickerList.join(' ')} to ${message.guild.name}!`); // eslint-disable-line } } - } else if (await isAiEnabledGuild(message) && !message.author.bot) { + } else if (!message.author.bot) { await discordAiChat(message); } else { try { diff --git a/src/global/commands/g.ai.ts b/src/global/commands/g.ai.ts index 95828af1d..760e746c5 100644 --- a/src/global/commands/g.ai.ts +++ b/src/global/commands/g.ai.ts @@ -1,5 +1,5 @@ import OpenAI from 'openai'; -import { PrismaClient, ai_model, ai_personas } from '@prisma/client'; +import { PrismaClient, ai_personas } from '@prisma/client'; import { ImagesResponse, ModerationCreateResponse } from 'openai/resources'; import { Assistant } from 'openai/resources/beta/assistants/assistants'; import { stripIndents } from 'common-tags'; @@ -24,57 +24,6 @@ type ModerationResult = { limit: number, }; -// Costs per 1k tokens -const aiCosts = { - GPT_3_5_TURBO: { - input: 0.0015, - output: 0.002, - }, - GPT_3_5_TURBO_1106: { - input: 0.001, - output: 0.002, - }, - GPT_4: { - input: 0.03, - output: 0.06, - }, - GPT_4_1106_PREVIEW: { - input: 0.01, - output: 0.03, - }, - GPT_4_1106_VISION_PREVIEW: { - input: 0.01, - output: 0.03, - }, - DALL_E_2: { - input: 0.00, - output: 0.04, - }, - DALL_E_3: { - input: 0.00, - output: 0.02, - }, -} as AiCosts; - -// define an object as series of keys (AiModel) and value that looks like {input: number, output: number} -type AiCosts = { - [key in ai_model]: { - input: number, - output: number, - } -}; - -// const system = ` -// As a guide and helper, your role is to assist and engage with users in a respectful and informative manner. -// You are a harm reduction professional helping people with questions on drug usage, emphasizing safety and factual information. -// You're equipped to answer queries about the server, provide general guidance, and share fun facts related to our community's interests. -// You can also provide fun information to users when they ask it. For example, you can make songs, or come up with jokes, or tell stories. -// You will be cheerful and non-judgmental, avoiding rude or condescending behavior. -// Do not promote drug use, but provide factual, unbiased information for harm reduction. -// Remember, while you're knowledgeable, you're not a replacement for human interaction but a complement to it. -// Remember to always respect user privacy, and maintain a friendly and engaging tone. Let's make the TripSit Discord a welcoming space for everyone! -// `; - // Objective truths are facts and don't impact personality const objectiveTruths = ` Your name is TripBot, a chatbot on the TripSit Discord, created by Moonbear and Reality. @@ -85,22 +34,27 @@ Originally from the wild world of IRC (born Sept 26, 2011), you moved to the mor You recall IRC as chaotic and prefer the orderliness of Discord. You fondly remember Thanatos, an old moderation bot and your friend, who's currently on a break. -For those who wish to support us, check out our Patreon [https://www.patreon.com/TripSit]. -To appreciate Moonbear's efforts, visit [https://Ko-fi.com/tripsit]. -Join the TripSit discord via [https://discord.gg/tripsit]. +For those who wish to support TripSit, check out our Patreon [https://www.patreon.com/TripSit]. +To tip Moonbear's efforts, visit [https://Ko-fi.com/tripsit]. +Join the TripSit's discord via [https://discord.gg/tripsit]. View the TripBot source code on GitHub [https://github.com/TripSit/TripBot]. View our service status page at [https://uptime.tripsit.me/status]. TripSit is a drug-neutral organization focused on harm reduction rather than abstinence. Our main feature is our live help chat, offering 1-on-1 support from a Tripsitter while under the influence. -We host numerous resources like Factsheets [https://drugs.tripsit.me/] and our Wiki [https://wiki.tripsit.me/wiki/Main_Page]. +We host numerous resources like Factsheets [https://drugs.tripsit.me/] +and our Wiki [https://wiki.tripsit.me/wiki/Main_Page]. Our /combochart is a well-known resource for safe drug combinations. -The current team includes the admin Hipperooni (Rooni) and moderators Hisui, Hullabaloo, Foggy, Aida, Elixir, Spacelady, Hipperooni, WorriedHobbiton, Zombie, and Trees. +The current team includes the admin Hipperooni (Rooni) and moderators Hisui, Hullabaloo, Foggy, Aida, Elixir, +Spacelady, Hipperooni, WorriedHobbiton, Zombie, and Trees. -'Helper' is a role for those completing our tripsitting course. Helpers assist users in πŸŸ’β”‚tripsit but are not officially associated with TripSit. +'Helper' is a role for those completing our tripsitting course. +Helpers assist users in πŸŸ’β”‚tripsit but are not officially associated with TripSit. A 'Tripsitter' is an official role given to select users by our team. Any role with 'TS' lettering is an official TripSit team member role. -'Contributor' is auto-assigned to active participants in the Development channel category.`; +'Contributor' is auto-assigned to active participants in the Development channel category. +Patreon subscribers can use the /imagen command to generate images. +`; // # Example dummy function hard coded to return the same weather // # In production, this could be your backend API or an external API @@ -233,43 +187,17 @@ export async function readRun( export async function createImage( prompt: string, - user: string, + userId: string, ):Promise { log.debug(F, `createImage | prompt: ${prompt}`); - // log.debug(F, `image: ${JSON.stringify(image, null, 2)}`); - const userData = await db.users.upsert({ - where: { discord_id: user }, - create: { discord_id: user }, - update: { discord_id: user }, - }); - - await db.ai_usage.upsert({ - where: { - user_id: userData.id, - }, - create: { - user_id: userData.id, - images: [new Date()], - usd: 0.04, - }, - update: { - images: { - push: new Date(), - }, - usd: { - increment: 0.04, - }, - }, - }); - - if (env.NODE_ENV === ' development') { + if (env.NODE_ENV !== 'production') { log.debug(F, ' returning dev image'); return { created: 0, data: [ { - url: 'https://i.imgur.com/1Z9zZ1i.png', + url: 'https://picsum.photos/200', }, ], }; @@ -282,18 +210,18 @@ export async function createImage( response_format: 'url', size: '1024x1024', style: 'natural', - user, + user: userId, // For abuse tracking }); } export async function aiModerateReport( message: string, -):Promise { +):Promise { // log.debug(F, `message: ${message}`); // log.debug(F, `results: ${JSON.stringify(results, null, 2)}`); - if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return undefined; + if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return {} as ModerationCreateResponse; return openai.moderations .create({ @@ -307,6 +235,7 @@ export async function aiModerateReport( } else { throw err; } + return {} as ModerationCreateResponse; }); } @@ -360,20 +289,18 @@ export default async function aiChat( // const responseData = {} as CreateChatCompletionResponse; let promptTokens = 0; let completionTokens = 0; + if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return { response, promptTokens, completionTokens }; // log.debug(F, `messages: ${JSON.stringify(messages, null, 2)}`); // log.debug(F, `aiPersona: ${JSON.stringify(aiPersona.name, null, 2)}`); - let model = aiPersona.ai_model as string; + let model = aiPersona.ai_model.toLowerCase() as string; // Convert ai models into proper names if (aiPersona.ai_model === 'GPT_3_5_TURBO') { model = 'gpt-3.5-turbo-1106'; } else if (aiPersona.ai_model === 'GPT_4') { model = 'gpt-4-1106-preview'; - } else { - model = aiPersona.ai_model.toLowerCase(); } - // This message list is sent to the API const chatCompletionMessages = [{ role: 'system', @@ -406,33 +333,6 @@ export default async function aiChat( // log.debug(F, `payload: ${JSON.stringify(payload, null, 2)}`); let responseMessage = {} as OpenAI.Chat.ChatCompletionMessageParam; - if (!env.OPENAI_API_ORG || !env.OPENAI_API_KEY) return { response, promptTokens, completionTokens }; - - const userData = await db.users.upsert({ - where: { discord_id: user }, - create: { discord_id: user }, - update: { discord_id: user }, - }); - - await db.ai_usage.upsert({ - where: { - user_id: userData.id, - }, - create: { - user_id: userData.id, - images: [new Date()], - usd: 0.04, - }, - update: { - images: { - push: new Date(), - }, - usd: { - increment: 0.04, - }, - }, - }); - const chatCompletion = await openai.chat.completions .create(payload) .catch(err => { @@ -498,47 +398,9 @@ export default async function aiChat( response = responseMessage.content ?? 'Sorry, I\'m not sure how to respond to that.'; } - // responseData = chatCompletion.data; // log.debug(F, `response: ${response}`); - // Increment the tokens used - await db.ai_personas.update({ - where: { - id: aiPersona.id, - }, - data: { - total_tokens: { - increment: completionTokens + promptTokens, - }, - }, - }); - - const costUsd = (aiCosts[aiPersona.ai_model as keyof typeof aiCosts].input * promptTokens) - + (aiCosts[aiPersona.ai_model as keyof typeof aiCosts].output * completionTokens); - - await db.ai_usage.upsert({ - where: { - user_id: userData.id, - }, - create: { - user_id: userData.id, - tokens: completionTokens + promptTokens, - usd: costUsd, - }, - update: { - images: { - push: new Date(), - }, - usd: { - increment: costUsd, - }, - tokens: { - increment: completionTokens + promptTokens, - }, - }, - }); - return { response, promptTokens, completionTokens }; } diff --git a/src/global/utils/env.config.ts b/src/global/utils/env.config.ts index 27d680f6b..84daca25a 100644 --- a/src/global/utils/env.config.ts +++ b/src/global/utils/env.config.ts @@ -21,6 +21,9 @@ export const env = { API_USERNAME: process.env.API_USERNAME, API_PASSWORD: process.env.API_PASSWORD, + EMOJI_THUMB_UP: isProd ? '979721167332052992' : 'πŸ‘', + EMOJI_THUMB_DOWN: isProd ? '979721915390369822' : 'πŸ‘Ž', + OPENAI_API_ORG: process.env.OPENAI_API_ORG ?? '', OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? '', @@ -269,7 +272,12 @@ export const env = { ROLE_VOTETIMEOUT: isProd ? '991811200901976259' : '989287095367528578', ROLE_VUTEUNDERBAN: isProd ? '991811318464139416' : '989287082222579792', - ROLE_PATRON: isProd ? '954133862601089095' : '1052644652349665310', + ROLE_PATRON_TIER_0: isProd ? '954133862601089095' : '1052644652349665310', + ROLE_PATRON_TIER_1: isProd ? '' : '1182345635790327868', + ROLE_PATRON_TIER_2: isProd ? '' : '1182345770838523934', + ROLE_PATRON_TIER_3: isProd ? '' : '1182345731038777364', + ROLE_PATRON_TIER_4: isProd ? '' : '1182346555144015984', + ROLE_BOOSTER: isProd ? '853082033224024135' : '1167725202302574642', ROLE_PREMIUM: isProd ? '1139454371613122640' : '1167714206418735144', ROLE_DONATIONTRIGGER: isProd ? '1172503141481189407' : '1171007453903732776', diff --git a/src/prisma/tripbot/migrations/20231207214742_better_image_logging/migration.sql b/src/prisma/tripbot/migrations/20231207214742_better_image_logging/migration.sql new file mode 100644 index 000000000..67d3dc112 --- /dev/null +++ b/src/prisma/tripbot/migrations/20231207214742_better_image_logging/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - You are about to drop the column `images` on the `ai_usage` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ai_usage" DROP COLUMN "images"; + +-- CreateTable +CREATE TABLE "ai_images" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "user_id" UUID NOT NULL, + "prompt" TEXT NOT NULL, + "revised_prompt" TEXT NOT NULL, + "image_url" TEXT NOT NULL, + "model" "ai_model" NOT NULL, + "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ai_images_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "ai_images" ADD CONSTRAINT "aiimages_userid_foreign" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/src/prisma/tripbot/schema.prisma b/src/prisma/tripbot/schema.prisma index b19958f4c..4e47b48d1 100644 --- a/src/prisma/tripbot/schema.prisma +++ b/src/prisma/tripbot/schema.prisma @@ -164,6 +164,7 @@ model users { user_tickets_user_tickets_reopened_byTousers user_tickets[] @relation("user_tickets_reopened_byTousers") user_tickets_user_tickets_user_idTousers user_tickets[] @relation("user_tickets_user_idTousers") ai_usage ai_usage[] + ai_images ai_images[] } model discord_guilds { @@ -439,19 +440,30 @@ model ai_moderation { } model ai_usage { - id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid - user_id String @db.Uuid - tokens Int @default(0) - images DateTime[] @default([]) - usd Float @default(0) - created_at DateTime @default(now()) @db.Timestamptz(6) - updated_at DateTime @default(now()) @db.Timestamptz(6) + id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + user_id String @db.Uuid + tokens Int @default(0) + usd Float @default(0) + created_at DateTime @default(now()) @db.Timestamptz(6) + updated_at DateTime @default(now()) @db.Timestamptz(6) users users @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "aiusage_userid_foreign") @@unique([user_id], map: "aiusage_userid_unique") } +model ai_images { + id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + user_id String @db.Uuid + prompt String + revised_prompt String + image_url String + model ai_model + created_at DateTime @default(now()) @db.Timestamptz(6) + + users users @relation(fields: [user_id], references: [id], onDelete: NoAction, onUpdate: NoAction, map: "aiimages_userid_foreign") +} + enum bridge_status { PENDING ACTIVE