diff --git a/.gitguardian.yml b/.gitguardian.yml new file mode 100644 index 0000000..59d3b73 --- /dev/null +++ b/.gitguardian.yml @@ -0,0 +1,26 @@ +# Example here : https://github.com/GitGuardian/ggshield/blob/main/.gitguardian.example.yml +# Required, otherwise ggshield considers the file to use the deprecated v1 format +version: 2 +# +# Set to true if the desired exit code for the CLI is always 0, otherwise the +# exit code will be 1 if incidents are found. +# The environment variable GITGUARDIAN_EXIT_ZERO=true can also be used toggle this behavior. +exit-zero: false # default: false +# +verbose: false # default: false +# +# The dashboard URL of the instance +instance: https://dashboard.gitguardian.com # default: https://dashboard.gitguardian.com +# +# Maximum commits to scan in a hook. +max-commits-for-hook: 50 # default: 50 +# +# Accept self-signed certificates for the API. +allow-self-signed: false # default: false +# +secret: + # Exclude files and paths by globbing + ignored-paths: + - "**/README.md" + - "back_node/tests/**" + - "clientDiscord/tests/**" diff --git a/.github/workflows/client_discord-dockerPush.yml b/.github/workflows/client_discord-dockerPush.yml index 09b2430..3bcc7d8 100644 --- a/.github/workflows/client_discord-dockerPush.yml +++ b/.github/workflows/client_discord-dockerPush.yml @@ -3,13 +3,20 @@ run-name: "[CLIENT_DISCORD] DockerPush : ${{ github.actor }} push '${{ github.ev on: push: paths: - - clientDiscord/** + - clientDiscord/** + pull_request: + paths: + - clientDiscord/** +permissions: + checks: write + pull-requests: write jobs: Check: runs-on: ubuntu-latest outputs: RUN_BUILD: ${{ steps.define_docker_data.outputs.run_build }} VERSION: ${{ steps.define_docker_data.outputs.version }} + if: github.event_name == 'push' steps: - name: Check out repository code uses: actions/checkout@v3 @@ -51,6 +58,12 @@ jobs: working-directory: ./clientDiscord run: | export NODE_OPTIONS=--no-experimental-fetch && npm test 2>> /tmp/results.txt >> /tmp/coverage0.txt + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: | + clientDiscord/reports/report.xml - name: Prepare data id: data run: | @@ -68,20 +81,20 @@ jobs: - name: Echo errors if: steps.tests.outcome == 'failure' run: | - sed '$ d' /tmp/coverage1.txt > $GITHUB_STEP_SUMMARY - echo -e "\n" >> $GITHUB_STEP_SUMMARY - cat /tmp/results.txt >> $GITHUB_STEP_SUMMARY - echo -e "\n" >> $GITHUB_STEP_SUMMARY - echo "❌ Errors with the test" >> $GITHUB_STEP_SUMMARY + # sed '$ d' /tmp/coverage1.txt > $GITHUB_STEP_SUMMARY + # echo -e "\n" >> $GITHUB_STEP_SUMMARY + # cat /tmp/results.txt >> $GITHUB_STEP_SUMMARY + # echo -e "\n" >> $GITHUB_STEP_SUMMARY + # echo "❌ Errors with the test" >> $GITHUB_STEP_SUMMARY exit 1 - - name: Echo success - if: steps.tests.outcome == 'success' - run: | - echo -e "\n" >> $GITHUB_STEP_SUMMARY - sed '$ d' /tmp/coverage1.txt > $GITHUB_STEP_SUMMARY - cat /tmp/results.txt >> $GITHUB_STEP_SUMMARY - echo -e "\n" >> $GITHUB_STEP_SUMMARY - echo "✅ All test are checked" >> $GITHUB_STEP_SUMMARY + # - name: Echo success + # if: steps.tests.outcome == 'success' + # run: | + # echo -e "\n" >> $GITHUB_STEP_SUMMARY + # sed '$ d' /tmp/coverage1.txt > $GITHUB_STEP_SUMMARY + # cat /tmp/results.txt >> $GITHUB_STEP_SUMMARY + # echo -e "\n" >> $GITHUB_STEP_SUMMARY + # echo "✅ All test are checked" >> $GITHUB_STEP_SUMMARY Build_Push: runs-on: ubuntu-latest @@ -89,7 +102,7 @@ jobs: needs: - Check - Tests - if: ${{ needs.Check.outputs.RUN_BUILD == 'yes' && needs.Tests.result == 'success' }} + if: ${{ github.event_name == 'push' && needs.Check.outputs.RUN_BUILD == 'yes' && needs.Tests.result == 'success' }} steps: - name: Check out repository code uses: actions/checkout@v3 diff --git a/clientDiscord/.gitignore b/clientDiscord/.gitignore index 91489c1..6a93561 100644 --- a/clientDiscord/.gitignore +++ b/clientDiscord/.gitignore @@ -2,4 +2,6 @@ node_modules/ data/ logs/ coverage/ -.env \ No newline at end of file +reports/ +.env +.vscode/ \ No newline at end of file diff --git a/clientDiscord/docker-compose.yml b/clientDiscord/docker-compose.yml index d4230a4..739ce12 100644 --- a/clientDiscord/docker-compose.yml +++ b/clientDiscord/docker-compose.yml @@ -3,7 +3,7 @@ version: "3" services: karassistant-client-discord: #build: . - image: codyisthesenate/karassistant-client-discord:1.1.1 + image: codyisthesenate/karassistant-client-discord:1.3.1 restart: always container_name: karassistant-client-discord volumes: diff --git a/clientDiscord/events/interactionCreate.js b/clientDiscord/events/interactionCreate.js new file mode 100644 index 0000000..6b23800 --- /dev/null +++ b/clientDiscord/events/interactionCreate.js @@ -0,0 +1,14 @@ +const { Events } = require("discord.js"); +const listCommands = require("../utils/loadCommands").listCommands; + +module.exports.run = async (client) => { + client.on(Events.InteractionCreate, (interaction) => { + if (interaction.isChatInputCommand()) { + try { + listCommands[interaction.commandName](interaction); + } catch (error) { + interaction.reply("There is an error with the command"); + } + } + }); +}; diff --git a/clientDiscord/index.js b/clientDiscord/index.js index 92b53a8..8840264 100644 --- a/clientDiscord/index.js +++ b/clientDiscord/index.js @@ -24,7 +24,9 @@ async function start() { }); }); - client.login(TOKEN).then(() => {}); + client.login(TOKEN).then(() => { + require("./utils/loadCommands").run(client); + }); } start(); diff --git a/clientDiscord/interactions/commands/setTimeZone.js b/clientDiscord/interactions/commands/setTimeZone.js new file mode 100644 index 0000000..c6b3c96 --- /dev/null +++ b/clientDiscord/interactions/commands/setTimeZone.js @@ -0,0 +1,62 @@ +const { ApplicationCommandOptionType } = require("discord.js"); +const updateUser = require("../../utils/updateUser").updateUser; + +const listOfTimezones = [ + "Pacific/Kiritimati", + "Pacific/Tongatapu", + "Pacific/Apia", + "Asia/Anadyr", + "Asia/Kamchatka", + "Asia/Tokyo", + "Asia/Hong_Kong", + "Asia/Kolkata", + "Asia/Dubai", + "Europe/Moscow", + "Europe/Paris", + "Europe/London", + "Africa/Cairo", + "Africa/Nairobi", + "Asia/Kuwait", + "Europe/Athens", + "Asia/Baghdad", + "Europe/Istanbul", + "Asia/Tehran", + "Asia/Kabul", + "Asia/Baku", + "Asia/Ashgabat", + "Asia/Muscat", + "Asia/Tashkent", + "Asia/Almaty", +]; + +const choices = []; +for (const timezone of listOfTimezones) { + choices.push({ name: timezone, value: timezone }); +} + +module.exports = { + data: { + name: "settimezone", + description: "Set the time zone, to get the time right", + options: [ + { + type: ApplicationCommandOptionType.String, + name: "timezone", + description: "Value for the new time zone", + required: true, + choices, + }, + ], + }, + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + const timeZone = interaction.options.get("timezone").value; + const userName = interaction.user.userName; + const userId = interaction.user.id; + + const result = await updateUser({ userName: userName, userId: userId, data: { timeZone } }); + return interaction.followUp( + result ? "Time zone has been updated" : "ERROR: Could not update time zone. Try again later." + ); + }, +}; diff --git a/clientDiscord/jest.config.js b/clientDiscord/jest.config.js index 331f977..ee4f8d5 100644 --- a/clientDiscord/jest.config.js +++ b/clientDiscord/jest.config.js @@ -99,7 +99,7 @@ module.exports = { // projects: undefined, // Use this configuration option to add custom reporters to Jest - // reporters: undefined, + reporters: ["default", ["jest-junit", { outputDirectory: "reports", outputName: "report.xml" }]], // Automatically reset mock state before every test // resetMocks: false, diff --git a/clientDiscord/package-lock.json b/clientDiscord/package-lock.json index 802bd2f..d2ba418 100644 --- a/clientDiscord/package-lock.json +++ b/clientDiscord/package-lock.json @@ -1,17 +1,19 @@ { "name": "clientdiscord", - "version": "1.0.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "clientdiscord", - "version": "1.0.0", + "version": "1.3.0", "license": "ISC", "dependencies": { "axios": "^1.5.0", "discord.js": "^14.13.0", "dotenv": "^16.3.1", + "jest-junit": "^16.0.0", + "node-forge": "^1.3.1", "node-rsa": "^1.1.1", "qs": "^6.11.2" }, @@ -1292,7 +1294,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -2733,6 +2734,20 @@ "fsevents": "^2.3.2" } }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/jest-leak-detector": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.6.3.tgz", @@ -3303,6 +3318,17 @@ "node": "*" } }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3315,6 +3341,14 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3994,7 +4028,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4187,6 +4220,14 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-to-istanbul": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", @@ -4287,6 +4328,11 @@ } } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/clientDiscord/package.json b/clientDiscord/package.json index 0770dc4..6e07f00 100644 --- a/clientDiscord/package.json +++ b/clientDiscord/package.json @@ -1,6 +1,6 @@ { "name": "clientdiscord", - "version": "1.1.1", + "version": "1.3.1", "description": "A discord bot for KarAssistant", "main": "index.js", "scripts": { @@ -24,6 +24,8 @@ "axios": "^1.5.0", "discord.js": "^14.13.0", "dotenv": "^16.3.1", + "jest-junit": "^16.0.0", + "node-forge": "^1.3.1", "node-rsa": "^1.1.1", "qs": "^6.11.2" }, diff --git a/clientDiscord/tests/utils/encryption.test.js b/clientDiscord/tests/utils/encryption.test.js new file mode 100644 index 0000000..74bde1b --- /dev/null +++ b/clientDiscord/tests/utils/encryption.test.js @@ -0,0 +1,227 @@ +const NodeRSA = require("node-rsa"); + +describe("Test rsa loadKey", () => { + test("Load public", async () => { + //Prepare + const key = + "-----BEGIN PUBLIC KEY-----\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCGYAv73I12ZEBEqgJERAImH9p/\n" + + "/6sSkUP3vXG2S9O3R4WDP3PBqiaLJFEKn1aGIhc6+hs2wjXc6RduVj/CZjppOzW1\n" + + "Ni8q4S+PL1cYOimxX4PkwTGQkmg6VoPwELhOzuSEqsjalmzHg16FMY0JZTnKlyIw\n" + + "CICvRL9MmmoW1RP1TwIDAQAB\n" + + "-----END PUBLIC KEY-----"; + + //Execute + const result = require("../../utils/encryption").rsa.loadKey({ key }); + + //Test + expect(!!result.keyPair).toBe(true); + }); + + test("Load private", async () => { + //Prepare + const key = + "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIICWwIBAAKBgQCZJ+rDmeiJzGMpOe4MNI+9DhZ+VnqplL6xUIQICexjU+02/F5H\n" + + "2vkXzeAiaAb5DZghaKdf4GGckUbBcwyHc/PceuzkAAatKPPATB8ACQ38RXqdGLak\n" + + "A7mo74GxUD0eKqbGeuue4nxmFUDDhcne0QPv4YLLlCYtOwWBsKikdKf8WQIDAQAB\n" + + "AoGAM6REAqRYxm4GWZZQ8AihFuwzJXJfdeLT0dIGUveVn4BjEhFScQQizaX7l15g\n" + + "S4YL7+fr1+Y+w54wr3XtmZa9eOawxdGY3YAknJpfSOK1mb1IJ714dF53KmJs7V8c\n" + + "MlO/bkihQNLrJIetYLi31V+l4LOStviboNVRkNVcinmFK6kCQQD+c8cGgXmX03W7\n" + + "SHdgW+Gv3bWR7v+5S+lvSqbIfWz6SYUEVZaQQbJuARd9Bodl2uYcvONW/TLgnhi3\n" + + "Yk4hgT1HAkEAmhZntxvKY4b4TXZdohwlQV6ow7yRjyG4EXMTYW5xc0HqUmx+8nhW\n" + + "zbYQSaGbCINfP1LLcnFpjkyOCMJdk39JXwJAZa1F/meGexDYnrnaWfrdODVT9LiY\n" + + "HyciZIJkGwFjpq/yI0VAIOzfq+1rwV32hNDv2tPv1DbhObhzD/SMW/8UyQJAMjwX\n" + + "uBSxWN1J2kc6o301kChCMP4rHlTJ47Z2nQ8aoY7dy91fTcF52zr9+GNdXdsmlEhz\n" + + "122uEhxXOffT9iBLVQJAa7pXvb7gmXoXV0MMGwzBl+fucjnl5TRIp5CbILETWS9F\n" + + "volGKmguKpLIchj+QYSisyXHsZ9a+k3ldBX/BVxXyg==\n" + + "-----END RSA PRIVATE KEY-----"; + + //Execute + const result = require("../../utils/encryption").rsa.loadKey({ key }); + + //Test + expect(!!result.keyPair).toBe(true); + }); + + test("Load error", async () => { + //Prepare + const key = "Not a key"; + + //Execute + const result = require("../../utils/encryption").rsa.loadKey({ key }); + + //Test + expect(result).toBe(null); + }); +}); + +describe("Test rsa encryption/decrytion", () => { + test("encryption/decrytion", async () => { + //Prepare + const keyPublicString = + "-----BEGIN PUBLIC KEY-----\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwEbJyN+BDA4YrpNXNeCgEeYDb\n" + + "QYdiEl6FGmU+cyR8mH3jgbkw9SIjl19jpyaQL3rQYu6XJ8E8fV+izyauaUfGqA0X\n" + + "RbkGGEpZxkkAk8e+RpOjPOt8F6oaryTqg1SZassHTvWs9nfC/kYH/MLVncveHGVC\n" + + "bwLxzpkr1wfeUd9DAwIDAQAB\n" + + "-----END PUBLIC KEY-----"; + const keyPublic = new NodeRSA(keyPublicString); + const keyPrivateString = + "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIICXAIBAAKBgQCwEbJyN+BDA4YrpNXNeCgEeYDbQYdiEl6FGmU+cyR8mH3jgbkw\n" + + "9SIjl19jpyaQL3rQYu6XJ8E8fV+izyauaUfGqA0XRbkGGEpZxkkAk8e+RpOjPOt8\n" + + "F6oaryTqg1SZassHTvWs9nfC/kYH/MLVncveHGVCbwLxzpkr1wfeUd9DAwIDAQAB\n" + + "AoGALzuAIG3m5nNSkfC1PlqGebTSoX7xv5hn7NMI5/jhh98snlSVhpGsJ9oax9P2\n" + + "X2WtT6vKj5glmYGUn0ts+ArcKvfPjcsZCLjUjkEjNmFAAdRsh/unqqtMHk0Zex20\n" + + "U/yT+a5RSkTGEA1EtqDvla4GN147Ir7Qz54k2q8hVUxeSvkCQQDUcKTBCkeNuZn+\n" + + "kZ8FaHaVmALQc3nHUxtAdBgNtPE+FgQi9fnDnxxH1LUBjmx1kO6YNxyhP7LWrZui\n" + + "Bm8jzfylAkEA1Cvhc0yPmRfrVkrkjmkFMuC9pTCNebnNGxVoVb4nDCnVOiNdyCbY\n" + + "5nRLzsaF74uKYekVmQ7EumC14IV7vGxohwJBALBwifWmcv1bvHG5Qmj8dRkTsqqs\n" + + "beVFuemTQnMH6CFXqcHbp8B4gsWJ/Xe4cY5HfFLB2y51uDQi5pLwYxhKud0CQGw7\n" + + "AiOFv46x4+u+An8e1XcRq8wTS2f3vsf9EJ8Eg/ixckLY/aL3JhfQ5UbSgEok3W96\n" + + "rfjIztPgN4cTsH36swsCQA4QJak5Omkb0KTnk6Q9e8qm0kLo2VOe3LPXFY73Rk+2\n" + + "CVm3BqS1NehnqgSxN98UgXRsT0z0NnFP8dQhh3qcbvQ=\n" + + "-----END RSA PRIVATE KEY-----"; + const keyPrivate = new NodeRSA(keyPrivateString); + const initData = { test: "test" }; + + //Execute encryption + const { encryptedData } = await require("../../utils/encryption").rsa.encrypt({ key: keyPublic, data: initData }); + + //Test encryption + expect(!!encryptedData).toBe(true); + + //Execute decryption + const { decryptedData } = await require("../../utils/encryption").rsa.decrypt({ + key: keyPrivate, + data: encryptedData, + }); + + //Test decryption + expect(JSON.stringify(decryptedData)).toBe(JSON.stringify(initData)); + }); +}); + +describe("Test aes encryption", () => { + test("Encrypt message and create key", async () => { + //Prepare + const message = "Test encrypt"; + + //Execute + const result = require("../../utils/encryption").aes.encrypt({ message }); + + //Test + expect(!!result.messageHex).toBe(true); + expect(!!result.keyBase64).toBe(true); + expect(!!result.ivBase64).toBe(true); + }); + + test("Encrypt message with specific keys", async () => { + //Prepare + const message = "Test encrypt"; + const keyBase64 = "SENyKJ2V1cL7nTNH0k1/Mg=="; + const ivBase64 = "hMraShc9/XkTn2m0Iwtw6w=="; + + //Execute + const result = require("../../utils/encryption").aes.encrypt({ message, keyBase64, ivBase64 }); + + //Test + expect(result.messageHex).toBe("2ccec94d3149368cf52a9e97589b6432"); + expect(result.keyBase64).toBe(keyBase64); + expect(result.ivBase64).toBe(ivBase64); + }); +}); + +describe("Test aes decryption", () => { + test("Decrypt message", async () => { + //Prepare + const messageHex = "2ccec94d3149368cf52a9e97589b6432"; + const keyBase64 = "SENyKJ2V1cL7nTNH0k1/Mg=="; + const ivBase64 = "hMraShc9/XkTn2m0Iwtw6w=="; + + //Execute + const result = require("../../utils/encryption").aes.decrypt({ messageHex, keyBase64, ivBase64 }); + + //Test + expect(result).toBe("Test encrypt"); + }); +}); + +describe("Test aes encryption and decryption", () => { + test("Encrypt and decrypt", async () => { + //Prepare + const message = JSON.stringify({ query: "Kara fait des tests" }); + + //Execute + const { messageHex, keyBase64, ivBase64 } = require("../../utils/encryption").aes.encrypt({ message }); + const result = require("../../utils/encryption").aes.decrypt({ messageHex, keyBase64, ivBase64 }); + + //Test + expect(result).toBe(message); + }); +}); + +describe("Test encryption for request", () => { + test("Encryption", async () => { + //Prepare + const keyPublicString = + "-----BEGIN PUBLIC KEY-----\n" + + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCwEbJyN+BDA4YrpNXNeCgEeYDb\n" + + "QYdiEl6FGmU+cyR8mH3jgbkw9SIjl19jpyaQL3rQYu6XJ8E8fV+izyauaUfGqA0X\n" + + "RbkGGEpZxkkAk8e+RpOjPOt8F6oaryTqg1SZassHTvWs9nfC/kYH/MLVncveHGVC\n" + + "bwLxzpkr1wfeUd9DAwIDAQAB\n" + + "-----END PUBLIC KEY-----"; + const query = "This is a test message. Test avec des charactères très spéciaux"; + + //Execute + const result = await require("../../utils/encryption").encryptionForRequest({ + query, + backPublicKey: keyPublicString, + }); + + //Test + expect(!!result.aes).toBe(true); + expect(!!result.data).toBe(true); + }); +}); + +describe("Test decryption for result", () => { + test("Decryption", async () => { + //Prepare + const keyPrivateString = + "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIICXgIBAAKBgQDKBVEb0V14yZe824p++J4WrldboXRXoBJZbFmJukFaRXqMhu2i\n" + + "o9Z+8onfbeMMfkbD21YLPivZYLx52v7qocq+znVtU/F77LrnsP6tUMLQKaUDMOM6\n" + + "J1i+lLjsFnxJO0IdQNOk6hHPcre88pGDlY8gUKGXzbYEu7v46szU9skpGwIDAQAB\n" + + "AoGAfVVNG7gJiI1xQS7nPpzZ33JsKiIBvvdFSwtIhYTzVKD0RcjUF2oUAhBQ7zgK\n" + + "e86/8nTabgE1TRiR9fui2UhlMl8iojR9agFJmjH6cqdXf/T6hRfe/mq7Kj561FFc\n" + + "zNoTGsIxQcCXhI+Vdf6dLl2oiw4kTPWfTxFrI2XtuCZfsNECQQDrqIAKc5JKU6a/\n" + + "0ahk4PkYMXkGNUXxSJVZb/c4P/yMk1xYQPrlQUsI97kEdBvwKcKSyxwNqpQEUUm+\n" + + "ibFYtPS5AkEA23WB4nF2OCgBLrLuOyHiRcOXC+JHyiKXJDimhCD/YKc7BdNFP3tA\n" + + "kFiW1Y7tPcdhysMG/IGN0DD2EyOwU3AKcwJBANz9rOUgMCXHgG4NnI7Ncoq/ijDK\n" + + "MIbufC/dEccMKjdh0Y1pkl7+9fC47iZBBBoZ7z9dfTdLqXbLDA7EbS00tPECQQC4\n" + + "849DB9xZ910HvkSIEUZhBTWHDmzyLbSzEgtDz4tqKYXUovj5RyZigEaeNJY8Ooxw\n" + + "FW0N4SFjE+BOwQUZTJOBAkEA6Ch5im1xvBW3iDo2JVKMzjNZu/FvOMxdgrox+JmU\n" + + "GJa5L3R9piU4RzntMb04KG3BLZ9jTUzIUafLlusDhI6mdw==\n" + + "-----END RSA PRIVATE KEY-----"; + const data = { + aes: "a9eTCsLtKXNFzsKAdAOpOpfczmTu7Eh1o9yecGtWlmvxhlBeqV6KFljK7YYSWkD5SnaaAa76yPknhlpsCY8kyjpPayJw2B68QSAqZEDmjS3OTDZunC/Z4NRSG5a0h2fy9UKr9KYLvw4xDjxlPEj/yA9q0VSQGOxn3O+4m7r2h6o=", + data: "330c28589035ff92e6f86a786ddb98d6cc4a15f91b4b6c2601d2e891d405dacd22db25d4ed9e3037f0937fdda4835dcd68942cd3d1fd4a7eabc4e40b8d214861855ac9c618e495d8c5c7fe808c22bfa8c40ef99080193de7a03f91c7fb85bbda97136e78399b282eac6995dff0ec4948937e1391f6320c245bd05b28e3c4edb30935525e4652646ec576f97e11c28e4580b960ee772ca0f2663f54281cfaa3872d56b5ff64a4e959c41add36d38ebba495690973a16a626deabc4f9750673a7d661e27c9de610477084d2053de8c9bd8901699c2ab509b06e67e9398bd52676486287cc0df7610eab268cadcfbc0a1c2", + }; + + //Executeawait decryptionForResult({ clientPrivateKey, data: dataRequest }); + const result = await require("../../utils/encryption").decryptionForResult({ + clientPrivateKey: keyPrivateString, + data: data, + }); + + //Test + expect(result.result).toBe("Quelle est la ville, dont vous souhaitez connaître la météo ?"); + expect(!!result.similarity).toBe(true); + expect(!!result.bestPhrase).toBe(true); + expect(result.shortAnswerExpected).toBe(true); + expect(!!result.lang).toBe(true); + expect(!!result.skill).toBe(true); + }); +}); diff --git a/clientDiscord/tests/utils/getPassPhrase.test.js b/clientDiscord/tests/utils/getPassPhrase.test.js deleted file mode 100644 index 16720da..0000000 --- a/clientDiscord/tests/utils/getPassPhrase.test.js +++ /dev/null @@ -1,5 +0,0 @@ -describe("Get pass phrase", () => { - test("Example", async () => { - expect(true).toBe(true); - }); -}); diff --git a/clientDiscord/utils/RSA.js b/clientDiscord/utils/RSA.js index db72ef5..6cd59a3 100644 --- a/clientDiscord/utils/RSA.js +++ b/clientDiscord/utils/RSA.js @@ -1,10 +1,9 @@ const NodeRSA = require("node-rsa"); module.exports.encryptData = encryptData; -async function encryptData({ data, publicKey }) { +async function encryptData({ data, key }) { try { - const keyPublic = new NodeRSA(publicKey); - data.date = new Date().toUTCString(); + const keyPublic = new NodeRSA(key); const passPhraseEncrypted = keyPublic.encrypt(JSON.stringify(data), "base64"); return passPhraseEncrypted; } catch { diff --git a/clientDiscord/utils/encryption.js b/clientDiscord/utils/encryption.js new file mode 100644 index 0000000..865d8c6 --- /dev/null +++ b/clientDiscord/utils/encryption.js @@ -0,0 +1,140 @@ +const NodeRSA = require("node-rsa"); +const forge = require("node-forge"); + +const regexSpectialChar = /[^\x00-\x7F]/; +function convertExceptionToHex(text) { + let result = ""; + for (let i = 0; i < text.length; i++) { + const char = text.charAt(i); + const codeAsciiHex = char.charCodeAt(0).toString(16); // Convertit le code ASCII en hexadécimal + if (regexSpectialChar.test(char)) result = result + "\\x" + codeAsciiHex; + else result = result + char; + } + return result; +} + +function convertExceptionToString(text) { + const decodedString = eval(`"${text}"`); + return decodedString; +} + +function loadKeyRSA({ key }) { + try { + const privateKeyObject = new NodeRSA(key); + return privateKeyObject; + } catch { + return null; + } +} + +async function encryptRSA({ key, data }) { + return await new Promise((resolve, reject) => { + try { + const encryptedData = key.encrypt(JSON.stringify(data), "base64"); + resolve({ encryptedData }); + /* c8 ignore start */ + } catch { + resolve({ encryptError: 403 }); + } + /* c8 ignore stop */ + }); +} + +async function decryptRSA({ key, data }) { + return await new Promise((resolve, reject) => { + try { + const decryptedData = key.decrypt(data, "utf8"); + resolve({ decryptedData: JSON.parse(decryptedData) }); + /* c8 ignore start */ + } catch (e) { + console.log(e); + resolve({ decryptError: 406 }); + } + /* c8 ignore stop */ + }); +} + +function encryptAES({ message, keyBase64, ivBase64 }) { + const keyDecoded = keyBase64 ? forge.util.decode64(keyBase64) : forge.random.getBytesSync(16); + const ivDecoded = ivBase64 ? forge.util.decode64(ivBase64) : forge.random.getBytesSync(16); + + const keyBase64Export = forge.util.encode64(keyDecoded); + const ivBase64Export = forge.util.encode64(ivDecoded); + + const cipher = forge.cipher.createCipher("AES-CBC", keyDecoded); + cipher.start({ iv: ivDecoded }); + cipher.update(forge.util.createBuffer(message)); + cipher.finish(); + const messageHex = cipher.output.toHex(); + + return { messageHex, keyBase64: keyBase64Export, ivBase64: ivBase64Export }; +} + +function decryptAES({ keyBase64, ivBase64, messageHex }) { + try { + const messageBuff = Buffer.from(messageHex, "hex"); + const key = forge.util.decode64(keyBase64); + const iv = forge.util.decode64(ivBase64); + + const decipher = forge.cipher.createDecipher("AES-CBC", key); + decipher.start({ iv }); + decipher.update(forge.util.createBuffer(messageBuff)); + decipher.finish(); + + return decipher.output.toString("utf8"); + /* c8 ignore start */ + } catch (error) { + return JSON.stringify({ result: "ERROR FOR DECRYPT AES" }); + } + /* c8 ignore stop */ +} + +async function encryptionForRequest({ query, backPublicKey }) { + const { messageHex, keyBase64, ivBase64 } = encryptAES({ + message: JSON.stringify({ query: convertExceptionToHex(query), date: new Date() }), + }); + + const key = loadKeyRSA({ key: backPublicKey }); + const { encryptedData } = await encryptRSA({ + data: { key: keyBase64, iv: ivBase64 }, + key, + }); + const data = { aes: encryptedData, data: messageHex }; + + return data; +} + +async function decryptionForResult({ data, clientPrivateKey }) { + const clientPrivateKeyLoaded = loadKeyRSA({ key: clientPrivateKey }); + const { decryptClientAesError, decryptedData: dataAes } = await decryptRSA({ + key: clientPrivateKeyLoaded, + data: data.aes, + }); + /* c8 ignore start */ + if (decryptClientAesError) throw decryptClientAesError; + /* c8 ignore stop */ + + const resultDecryptedString = decryptAES({ + messageHex: data.data, + keyBase64: dataAes.key, + ivBase64: dataAes.iv, + }); + const resultDecrypted = JSON.parse(resultDecryptedString); + if (resultDecrypted.bestPhrase) resultDecrypted.bestPhrase = convertExceptionToString(resultDecrypted.bestPhrase); + if (resultDecrypted.result) resultDecrypted.result = convertExceptionToString(resultDecrypted.result); + return resultDecrypted; +} + +module.exports = { + rsa: { + loadKey: loadKeyRSA, + encrypt: encryptRSA, + decrypt: decryptRSA, + }, + aes: { + encrypt: encryptAES, + decrypt: decryptAES, + }, + encryptionForRequest, + decryptionForResult, +}; diff --git a/clientDiscord/utils/heyKara.js b/clientDiscord/utils/heyKara.js index 46c8057..c2d8afd 100644 --- a/clientDiscord/utils/heyKara.js +++ b/clientDiscord/utils/heyKara.js @@ -7,17 +7,15 @@ const api = axios.create({ }, }); const prepareRequest = require("./prepareRequest").prepareRequest; -const decryptResult = require("./prepareRequest").decryptResult; +const decryptionForResult = require("../utils/encryption").decryptionForResult; module.exports.makeRequest = makeRequest; -async function makeRequest({ query, clientToken, data }) { +async function makeRequest({ clientToken, data }) { return await new Promise((resolve, reject) => { api({ method: "GET", headers: { "Content-Type": "application/json", karaeatcookies: clientToken }, - params: { - data, - }, + params: data, url: process.env.BACK_URL + "/api/heyKara", }) .then(function (response) { @@ -27,15 +25,15 @@ async function makeRequest({ query, clientToken, data }) { }) .catch(function (error) { if (error.code === "ECONNREFUSED") - resolve({ err: "Access to the Kara server cannot be established", status: 420 }); - if (error.response && error.response.status === 403) - resolve({ err: "Access to the Kara server is forbidden", status: error.response.status }); + return resolve({ err: "Access to the Kara server cannot be established", status: 420 }); + else if (error.response && error.response.status === 403) + return resolve({ err: "Access to the Kara server is forbidden", status: error.response.status }); else if (error.response && error.response.status === 404) return resolve({ err: "User does not exist", status: error.response.status }); - if (error.response && error.response.status === 500) - resolve({ err: "Kara as an internal error", status: error.response.status }); + else if (error.response && error.response.status === 500) + return resolve({ err: "Kara as an internal error", status: error.response.status }); else { - resolve({ err: error.code, status: error.response ? error.response.status : 420 }); + return resolve({ err: error.code, status: error.response ? error.response.status : 420 }); } }); }); @@ -43,21 +41,25 @@ async function makeRequest({ query, clientToken, data }) { module.exports.heyKara = heyKara; async function heyKara({ userName, userId, messageContent, avatarUrl, retry = 0 }) { - const { err, clientToken, data, publicKey } = await prepareRequest({ userId, userName, avatarUrl, messageContent }); + const { err, clientToken, data, clientPrivateKey } = await prepareRequest({ + userId, + userName, + avatarUrl, + messageContent, + }); if (err) return err; - const dataRequest = data ? await makeRequest({ query: messageContent, clientToken, data }) : { clientExist: false }; + + const dataRequest = data ? await makeRequest({ clientToken, data }) : { clientExist: false }; if (dataRequest.err && retry != 0) return dataRequest.err; if (!dataRequest || dataRequest.err) { - if (dataRequest && dataRequest.status === 404) fs.unlinkSync(__dirname + "/../data/clients/" + userId + ".json"); + if (dataRequest && (dataRequest.status === 404 || dataRequest.status === 406)) + fs.unlinkSync(__dirname + "/../data/clients/" + userId + ".json"); retry++; return heyKara({ userName, userId, messageContent, avatarUrl, retry }); } - const resultDecrypted = data - ? await decryptResult({ data: dataRequest, publicKey }) - : { result: "Error with the request" }; - + const resultDecrypted = await decryptionForResult({ clientPrivateKey, data: dataRequest }); const phraseResult = resultDecrypted.result; return phraseResult; } diff --git a/clientDiscord/utils/loadCommands.js b/clientDiscord/utils/loadCommands.js new file mode 100644 index 0000000..a76d9f7 --- /dev/null +++ b/clientDiscord/utils/loadCommands.js @@ -0,0 +1,40 @@ +const { REST, Routes } = require("discord.js"); +const fs = require("fs"); +require("dotenv").config(); + +const GUILD_ID = process.env.GUILD_ID; +const TOKEN = process.env.DISCORD_TOKEN; + +const listCommands = {}; +module.exports.listCommands = listCommands; + +module.exports.run = async (client) => { + // Create all commands + const commands = []; + const commandFolders = fs.readdirSync(__dirname + "/../interactions/commands/"); + + for (const folder of commandFolders) { + // Grab all the command files from the commands directory you created earlier + const commandsPath = __dirname + "/../interactions/commands/" + folder; + + const command = require(commandsPath); + if ("data" in command && "execute" in command) { + const data = command.data; + listCommands[data.name] = command.execute; + commands.push(data); + } else { + console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`); + } + } + + // Construct and prepare an instance of the REST module + const rest = new REST().setToken(TOKEN); + + // and deploy your commands! + try { + client.application.commands.set(commands, GUILD_ID); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } +}; diff --git a/clientDiscord/utils/prepareRequest.js b/clientDiscord/utils/prepareRequest.js index 7e5740f..f76a460 100644 --- a/clientDiscord/utils/prepareRequest.js +++ b/clientDiscord/utils/prepareRequest.js @@ -1,5 +1,5 @@ const fs = require("fs"); -const encryptData = require("./RSA").encryptData; +const encryptionForRequest = require("./encryption").encryptionForRequest; const updateUser = require("./updateUser").updateUser; const { stringify } = require("qs"); const axios = require("axios"); @@ -44,10 +44,10 @@ async function createNewClient({ authautifierTag }) { } module.exports.decryptResult = decryptResult; -async function decryptResult({ data, publicKey }) { +async function decryptResult({ data, key }) { try { - const keyPublic = new NodeRSA(publicKey); - const resultDecrypted = keyPublic.decryptPublic(data, "utf8"); + const keyPublic = new NodeRSA(key); + const resultDecrypted = keyPublic.decrypt(data, "utf8"); return JSON.parse(resultDecrypted); } catch { return null; @@ -58,10 +58,14 @@ async function decryptResult({ data, publicKey }) { module.exports.prepareRequest = async ({ userId, userName, avatarUrl, messageContent }) => { const userExist = fs.existsSync(__dirname + "/../data/clients/" + userId + ".json"); - const { err, clientToken, publicKey } = await new Promise(async (resolve, reject) => { + const { err, clientToken, backPublicKey, clientPrivateKey } = await new Promise(async (resolve, reject) => { if (userExist) { const userData = JSON.parse(fs.readFileSync(__dirname + "/../data/clients/" + userId + ".json", "utf8")); - resolve({ clientToken: userData.clientToken, publicKey: userData.publicKey }); + resolve({ + clientToken: userData.clientToken, + backPublicKey: userData.backPublicKey, + clientPrivateKey: userData.clientPrivateKey, + }); } else { const newClient = await createNewClient({ authautifierTag: userId }); if (newClient.err) return resolve({ err: newClient.err }); @@ -69,7 +73,8 @@ module.exports.prepareRequest = async ({ userId, userName, avatarUrl, messageCon userName, avatarUrl, clientToken: newClient.clientToken, - publicKey: newClient.publicKey, + backPublicKey: newClient.backPublicKey, + clientPrivateKey: newClient.clientPrivateKey, }; fs.writeFileSync(__dirname + "/../data/clients/" + userId + ".json", JSON.stringify(userData)); const res = await updateUser({ @@ -82,10 +87,7 @@ module.exports.prepareRequest = async ({ userId, userName, avatarUrl, messageCon }); if (err) return { err }; - const data = await encryptData({ - data: { query: messageContent }, - publicKey, - }); + const data = await encryptionForRequest({ query: messageContent, backPublicKey }); - return { clientToken, data, publicKey }; + return { clientToken, data, backPublicKey, clientPrivateKey }; }; diff --git a/clientDiscord/utils/updateUser.js b/clientDiscord/utils/updateUser.js index a036a98..16d8d04 100644 --- a/clientDiscord/utils/updateUser.js +++ b/clientDiscord/utils/updateUser.js @@ -26,6 +26,8 @@ async function updateUserRequest({ data, clientToken }) { .catch(function (error) { if (error.code === "ECONNREFUSED") return resolve({ err: "Access to the Kara server cannot be established", status: 420 }); + else if (error.response && error.response.status === 400) + return resolve({ err: "Bad request for PUT /api/user", status: error.response.status }); else if (error.response && error.response.status === 403) return resolve({ err: "Access to the Kara server is forbidden", status: error.response.status }); else if (error.response && error.response.status === 404) @@ -49,7 +51,7 @@ async function updateUser({ userName, userId, data, retry = 0 }) { const clientToken = userData.clientToken; const dataEncrypted = await encryptData({ data, - publicKey: userData.publicKey, + key: userData.backPublicKey, }); const dataRequest = await updateUserRequest({ data: dataEncrypted, clientToken });