diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..7df42831 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +# Version control +.git/ +.github/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.suo +*.user + +# Build artifacts +**/bin/ +**/obj/ + +# Tests +DiscordBot.Tests/ + +# Docs & misc +LICENSE +README.md +README_CASINO.md +UpgradeLog.htm + +# Runtime data (mounted at runtime, not baked into image) +Settings/ +DiscordBot/SERVER/ diff --git a/.github/workflows/AddCommentOnPR.yml b/.github/workflows/AddCommentOnPR.yml deleted file mode 100644 index ecf50f11..00000000 --- a/.github/workflows/AddCommentOnPR.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Add Deployment Comment to PR -"on": - pull_request_target: - types: [opened] - -jobs: - add_deploy_comment: - runs-on: ubuntu-latest - steps: - - name: Add deployment comment - uses: thollander/actions-comment-pull-request@v3 - with: - message: | - ### 🚀 Deploy this PR to an environment - - You can deploy this PR to either development or staging environment: - - - Comment `/deploy_dev` to deploy to the **development** environment - - Alternatively, you can: - 1. Go to Actions tab - 2. Click on "Manual Deploy to Firebase" workflow - 3. Click the "Run workflow" button - 4. Select branch: `${{ github.event.pull_request.head.ref }}` - 5. Choose environment: DEV - 6. Enter a deployment message - 7. Click "Run workflow" diff --git a/.github/workflows/Build&Deploy.yml b/.github/workflows/Build&Deploy.yml deleted file mode 100644 index b8a60ef0..00000000 --- a/.github/workflows/Build&Deploy.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Build & Deploy - -on: - push: - branches: [dev] - workflow_call: - inputs: - env: - required: true - type: string - workflow_dispatch: - inputs: - env: - description: "Environment to deploy to" - required: true - default: "dev" - type: choice - options: - - dev - -jobs: - push_to_registry: - name: Push Docker image to GitHub Packages - runs-on: ubuntu-latest - - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v2 - with: - push: true - tags: ghcr.io/unity-developer-community/udc-bot-dev:latest - - restart: - name: Restart Bot - needs: push_to_registry - runs-on: ubuntu-latest - - environment: - name: ${{ inputs.env }} - - steps: - - name: Run commands in SSH - uses: appleboy/ssh-action@master - with: - script: | - cd ${{ vars.SERVER_BUILD_DIR }} - docker compose pull - docker compose up -d - host: ${{ vars.SERVER_IP }} - port: ${{ vars.SERVER_PORT }} - username: ${{ vars.SERVER_USER }} - password: ${{ secrets.SERVER_PASSWORD }} - - - name: Discord notification - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} - uses: Ilshidur/action-discord@master - with: - args: Bot has been deployment to Test Server successfully. diff --git a/.github/workflows/DeployDev.yml b/.github/workflows/DeployDev.yml deleted file mode 100644 index fa4bc494..00000000 --- a/.github/workflows/DeployDev.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Deploy to Test Server - -on: - push: - branches: [dev] - workflow_dispatch: - -jobs: - push_to_registry: - name: Push Docker image to GitHub Packages - runs-on: ubuntu-latest - - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v2 - with: - push: true - tags: ghcr.io/unity-developer-community/udc-bot-dev:latest - - restart: - name: Restart Bot - needs: push_to_registry - runs-on: ubuntu-latest - - environment: - name: dev - - steps: - - name: Run commands in SSH - uses: appleboy/ssh-action@master - with: - script: | - cd ${{ vars.SERVER_BUILD_DIR }} - docker compose pull - docker compose up -d - host: ${{ vars.SERVER_IP }} - port: ${{ vars.SERVER_PORT }} - username: ${{ vars.SERVER_USER }} - password: ${{ secrets.SERVER_PASSWORD }} - - - name: Discord notification - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} - uses: Ilshidur/action-discord@master - with: - args: Bot has been deployment to Test Server successfully. diff --git a/.github/workflows/DeployProd.yml b/.github/workflows/DeployProd.yml deleted file mode 100644 index 7550ae2f..00000000 --- a/.github/workflows/DeployProd.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Deploy to UDC Server - -on: - push: - branches: [master] - workflow_dispatch: - -jobs: - push_to_registry: - name: Push Docker image to GitHub Packages - runs-on: ubuntu-latest - - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v2 - with: - push: true - tags: ghcr.io/unity-developer-community/udc-bot:latest - - restart: - name: Restart Bot - needs: push_to_registry - runs-on: ubuntu-latest - - environment: - name: prod - - steps: - - name: Run commands in SSH - uses: appleboy/ssh-action@master - with: - script: | - cd ${{ vars.SERVER_BUILD_DIR }} - docker compose pull - docker compose up -d - host: ${{ vars.SERVER_IP }} - port: ${{ vars.SERVER_PORT }} - username: ${{ vars.SERVER_USER }} - password: ${{ secrets.SERVER_PASSWORD }} - - - name: Discord notification - env: - DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} - uses: Ilshidur/action-discord@master - with: - args: Bot has been deployment to UDC Server successfully. diff --git a/.github/workflows/Dotnet.yml b/.github/workflows/Dotnet.yml index 0f500dc7..45d91c75 100644 --- a/.github/workflows/Dotnet.yml +++ b/.github/workflows/Dotnet.yml @@ -4,16 +4,19 @@ on: pull_request: branches: [master, dev] +permissions: + contents: read + jobs: build: name: Build & Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x diff --git a/.github/workflows/HandleDeployCommand.yml b/.github/workflows/HandleDeployCommand.yml deleted file mode 100644 index 6af20bad..00000000 --- a/.github/workflows/HandleDeployCommand.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: Handle Deploy Command -"on": - issue_comment: - types: [created] - -jobs: - process_comment: - if: github.event.issue.pull_request && (github.event.comment.body == '/deploy_dev') - runs-on: ubuntu-latest - steps: - - name: Determine deployment environment - id: deployment_env - run: | - if [[ "${{ github.event.comment.body }}" == "/deploy_dev" ]]; then - echo "env=dev" >> $GITHUB_OUTPUT - echo "env_name=development" >> $GITHUB_OUTPUT - fi - - - name: Get PR information - id: pr_info - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { owner, repo, number } = context.issue; - const { data: pull } = await github.rest.pulls.get({ - owner, - repo, - pull_number: number - }); - - console.log("PR head branch:", pull.head.ref); - console.log("PR head SHA:", pull.head.sha); - - core.setOutput("branch", pull.head.ref); - core.setOutput("sha", pull.head.sha); - core.setOutput("repo_name", pull.head.repo.full_name); - return pull.head.ref; - - - name: Add reaction to comment - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.reactions.createForIssueComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: context.payload.comment.id, - content: 'rocket' - }); - - - name: Add deployment comment - uses: actions/github-script@v6 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: `🚀 Starting deployment of \`${{ steps.pr_info.outputs.repo_name }}:${{ steps.pr_info.outputs.branch }}\` to ${{ steps.deployment_env.outputs.env_name }}...` - }); - - - name: Generate unique branch name - id: branch_name - run: | - TIMESTAMP=$(date +%s) - UNIQUE_BRANCH="deploy-branch-${{ steps.pr_info.outputs.branch }}-$TIMESTAMP" - echo "name=$UNIQUE_BRANCH" >> $GITHUB_OUTPUT - - - name: Checkout PR branch - uses: actions/checkout@v3 - with: - ref: ${{ steps.pr_info.outputs.sha }} - repository: ${{ github.event.issue.pull_request.head.repo.full_name }} - fetch-depth: 0 - - - name: Create temporary branch - run: | - git checkout -b ${{ steps.branch_name.outputs.name }} - git push origin ${{ steps.branch_name.outputs.name }} - - - name: Trigger deployment workflow - uses: benc-uk/workflow-dispatch@v1 - with: - workflow: Build & Deploy - token: ${{ secrets.GITHUB_TOKEN }} - ref: ${{ steps.branch_name.outputs.name }} - inputs: | - { - "env": "${{ steps.deployment_env.outputs.env }}" - } - - - name: Wait for deployment to start - run: sleep 60 # Wait 60 seconds to ensure workflow has started - - - name: Clean up temporary branch - if: always() - run: | - git push origin --delete ${{ steps.branch_name.outputs.name }} || true diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 00000000..a32a01ef --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,95 @@ +name: Build & Deploy + +on: + workflow_call: + inputs: + image_name: + description: "GHCR image name (without registry prefix)" + required: true + type: string + manifest_path: + description: "Path to the k8s manifest to update" + required: true + type: string + checkout_ref: + description: "Git ref to checkout and build from" + required: true + type: string + manifest_ref: + description: "Branch where the manifest lives (for commit+push)" + required: false + type: string + commit_message: + description: "Commit message for the manifest update" + required: true + type: string + +permissions: + contents: write + packages: write + +jobs: + build-and-deploy: + name: Build & Deploy + runs-on: ubuntu-latest + outputs: + short_sha: ${{ steps.sha.outputs.short }} + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + ref: ${{ inputs.checkout_ref }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract short SHA + id: sha + run: echo "short=$(git rev-parse --short=7 HEAD)" >> "$GITHUB_OUTPUT" + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/${{ inputs.image_name }}:${{ steps.sha.outputs.short }} + ghcr.io/${{ github.repository_owner }}/${{ inputs.image_name }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Checkout manifest branch + if: inputs.manifest_ref != '' + uses: actions/checkout@v4 + with: + ref: ${{ inputs.manifest_ref }} + + - name: Update manifest image tag + env: + IMAGE_NAME: ${{ inputs.image_name }} + SHORT_SHA: ${{ steps.sha.outputs.short }} + MANIFEST_PATH: ${{ inputs.manifest_path }} + run: | + OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]') + IMAGE=$(echo "${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]') + sed -i "s|image: ghcr.io/${OWNER}/${IMAGE}:.*|image: ghcr.io/${OWNER}/${IMAGE}:${SHORT_SHA}|" "${MANIFEST_PATH}" + + - name: Commit and push manifest update + env: + COMMIT_MSG: ${{ inputs.commit_message }} + SHORT_SHA: ${{ steps.sha.outputs.short }} + MANIFEST_PATH: ${{ inputs.manifest_path }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add "${MANIFEST_PATH}" + FINAL_MSG=$(echo "${COMMIT_MSG}" | sed "s/{sha}/${SHORT_SHA}/g") + git diff --cached --quiet || git commit -m "${FINAL_MSG}" + git push diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 00000000..854724ea --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,26 @@ +name: Deploy Dev + +on: + push: + branches: [dev] + paths-ignore: + - "k8s/**" + - "**/*.md" + - "docs/**" + workflow_dispatch: + +concurrency: + group: deploy-dev + cancel-in-progress: true + +jobs: + deploy: + uses: ./.github/workflows/build-and-deploy.yml + with: + image_name: udc-bot-dev + manifest_path: k8s/dev/bot.yaml + checkout_ref: dev + commit_message: "chore(k8s): update dev image to {sha}" + permissions: + contents: write + packages: write diff --git a/.github/workflows/deploy-pr-to-dev.yml b/.github/workflows/deploy-pr-to-dev.yml new file mode 100644 index 00000000..3e0b6254 --- /dev/null +++ b/.github/workflows/deploy-pr-to-dev.yml @@ -0,0 +1,99 @@ +name: Deploy PR to Dev + +on: + workflow_dispatch: + inputs: + pr_number: + description: "PR number to deploy" + required: true + type: number + +permissions: + contents: write + packages: write + pull-requests: read + issues: write + +concurrency: + group: deploy-dev + cancel-in-progress: false + +jobs: + validate: + name: Validate PR + runs-on: ubuntu-latest + outputs: + sha: ${{ steps.pr.outputs.sha }} + short_sha: ${{ steps.pr.outputs.short_sha }} + steps: + - name: Get PR info + id: pr + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ inputs.pr_number }} + with: + script: | + const prNumber = Number(process.env.PR_NUMBER); + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + if (pr.state !== 'open') { + core.setFailed(`PR #${prNumber} is not open (state: ${pr.state})`); + return; + } + core.setOutput('sha', pr.head.sha); + core.setOutput('short_sha', pr.head.sha.substring(0, 7)); + + deploy: + needs: validate + uses: ./.github/workflows/build-and-deploy.yml + with: + image_name: udc-bot-dev + manifest_path: k8s/dev/bot.yaml + checkout_ref: ${{ needs.validate.outputs.sha }} + manifest_ref: dev + commit_message: "chore(k8s): deploy PR #${{ inputs.pr_number }} to dev ({sha})" + permissions: + contents: write + packages: write + + notify: + needs: [validate, deploy] + if: always() + name: Notify PR + runs-on: ubuntu-latest + steps: + - name: Post success comment + if: needs.deploy.result == 'success' + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ inputs.pr_number }} + SHORT_SHA: ${{ needs.validate.outputs.short_sha }} + REPO_OWNER: ${{ github.repository_owner }} + with: + script: | + const { PR_NUMBER, SHORT_SHA, REPO_OWNER } = process.env; + const owner = REPO_OWNER.toLowerCase(); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: Number(PR_NUMBER), + body: `✅ PR deployed to dev!\n\nImage: \`ghcr.io/${owner}/udc-bot-dev:${SHORT_SHA}\`\nArgoCD will sync shortly.`, + }); + + - name: Post failure comment + if: needs.deploy.result == 'failure' + uses: actions/github-script@v7 + env: + PR_NUMBER: ${{ inputs.pr_number }} + with: + script: | + const { PR_NUMBER } = process.env; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: Number(PR_NUMBER), + body: `❌ Deploy to dev failed. Check the [workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.`, + }); diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml new file mode 100644 index 00000000..7c2c688e --- /dev/null +++ b/.github/workflows/deploy-prod.yml @@ -0,0 +1,26 @@ +name: Deploy Prod + +on: + push: + branches: [master] + paths-ignore: + - "k8s/**" + - "**/*.md" + - "docs/**" + workflow_dispatch: + +concurrency: + group: deploy-prod + cancel-in-progress: false + +jobs: + deploy: + uses: ./.github/workflows/build-and-deploy.yml + with: + image_name: udc-bot + manifest_path: k8s/prod/bot.yaml + checkout_ref: master + commit_message: "chore(k8s): update prod image to {sha}" + permissions: + contents: write + packages: write diff --git a/.github/workflows/pr-deploy-instructions.yml b/.github/workflows/pr-deploy-instructions.yml new file mode 100644 index 00000000..4cdc627a --- /dev/null +++ b/.github/workflows/pr-deploy-instructions.yml @@ -0,0 +1,31 @@ +name: PR Deploy Instructions + +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + issues: write + +jobs: + add-comment: + name: Add deployment instructions + runs-on: ubuntu-latest + steps: + - name: Add deploy comment + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + '### 🚀 Deploy this PR', + '', + `To deploy this branch to the **dev** environment, go to the [Deploy PR to Dev](${context.payload.repository.html_url}/actions/workflows/deploy-pr-to-dev.yml) workflow and click **Run workflow** with this PR number.`, + '', + 'This will build a Docker image from your branch, push it to GHCR, and trigger an ArgoCD sync.', + ].join('\n'), + }); diff --git a/.gitignore b/.gitignore index f0faf3b6..9908e11e 100644 --- a/.gitignore +++ b/.gitignore @@ -452,3 +452,6 @@ fabric.properties .ionide # End of https://www.toptal.com/developers/gitignore/api/csharp,visualstudiocode,rider + +# Bot runtime-generated data (SERVER/ is created by the bot at runtime) +DiscordBot/SERVER/ diff --git a/DiscordBot/SERVER/fonts/Consolas.ttf b/DiscordBot/Assets/fonts/Consolas.ttf similarity index 100% rename from DiscordBot/SERVER/fonts/Consolas.ttf rename to DiscordBot/Assets/fonts/Consolas.ttf diff --git a/DiscordBot/SERVER/fonts/ConsolasBold.ttf b/DiscordBot/Assets/fonts/ConsolasBold.ttf similarity index 100% rename from DiscordBot/SERVER/fonts/ConsolasBold.ttf rename to DiscordBot/Assets/fonts/ConsolasBold.ttf diff --git a/DiscordBot/SERVER/fonts/OpenSans-Regular.ttf b/DiscordBot/Assets/fonts/OpenSans-Regular.ttf similarity index 100% rename from DiscordBot/SERVER/fonts/OpenSans-Regular.ttf rename to DiscordBot/Assets/fonts/OpenSans-Regular.ttf diff --git a/DiscordBot/SERVER/fonts/OpenSansEmoji.ttf b/DiscordBot/Assets/fonts/OpenSansEmoji.ttf similarity index 100% rename from DiscordBot/SERVER/fonts/OpenSansEmoji.ttf rename to DiscordBot/Assets/fonts/OpenSansEmoji.ttf diff --git a/DiscordBot/SERVER/fonts/georgia.ttf b/DiscordBot/Assets/fonts/georgia.ttf similarity index 100% rename from DiscordBot/SERVER/fonts/georgia.ttf rename to DiscordBot/Assets/fonts/georgia.ttf diff --git a/DiscordBot/SERVER/images/ExampleExport.png b/DiscordBot/Assets/images/ExampleExport.png similarity index 100% rename from DiscordBot/SERVER/images/ExampleExport.png rename to DiscordBot/Assets/images/ExampleExport.png diff --git a/DiscordBot/Assets/images/Layout.txt b/DiscordBot/Assets/images/Layout.txt new file mode 100644 index 00000000..f484290d --- /dev/null +++ b/DiscordBot/Assets/images/Layout.txt @@ -0,0 +1,19 @@ +XP Bar: +- Back_Rect: (104, 39, 232, 16) (X,Y,W,H) +- Fore_Rect: (105, 40, 126, 14) (X,Y,W,H) +- Background:(#3F3F3F) +- Foreground:(#00F0FF) + +Rank Triangle: +- Rect: (346, 12, 54, 104) (X,Y,W,H) +- Color:(Based on UDH Rank Color) + +Fonts: +- Name: Consolas (24pt, #3C3C3C) +- Level: Consolas (59pt, #3C3C3C) +- Meta: Opens Sans (16pt, #3C3C3C) + +Effects (If Possible): +- Card: Drop-Shadow (16px, 50% Opacity), Alpha-Blend (75% Opacity) +- XP Bar: Drop-Shadow (4px, 50% Opacity) +- Level: Drop-Shadow (4px, 25% Opacity) diff --git a/DiscordBot/SERVER/images/background.png b/DiscordBot/Assets/images/background.png similarity index 100% rename from DiscordBot/SERVER/images/background.png rename to DiscordBot/Assets/images/background.png diff --git a/DiscordBot/SERVER/images/background_old.png b/DiscordBot/Assets/images/background_old.png similarity index 100% rename from DiscordBot/SERVER/images/background_old.png rename to DiscordBot/Assets/images/background_old.png diff --git a/DiscordBot/SERVER/images/default.png b/DiscordBot/Assets/images/default.png similarity index 100% rename from DiscordBot/SERVER/images/default.png rename to DiscordBot/Assets/images/default.png diff --git a/DiscordBot/SERVER/images/foreground.png b/DiscordBot/Assets/images/foreground.png similarity index 100% rename from DiscordBot/SERVER/images/foreground.png rename to DiscordBot/Assets/images/foreground.png diff --git a/DiscordBot/SERVER/images/levelupcard.png b/DiscordBot/Assets/images/levelupcard.png similarity index 100% rename from DiscordBot/SERVER/images/levelupcard.png rename to DiscordBot/Assets/images/levelupcard.png diff --git a/DiscordBot/SERVER/images/levelupcardbackground.png b/DiscordBot/Assets/images/levelupcardbackground.png similarity index 100% rename from DiscordBot/SERVER/images/levelupcardbackground.png rename to DiscordBot/Assets/images/levelupcardbackground.png diff --git a/DiscordBot/SERVER/images/triangle.png b/DiscordBot/Assets/images/triangle.png similarity index 100% rename from DiscordBot/SERVER/images/triangle.png rename to DiscordBot/Assets/images/triangle.png diff --git a/DiscordBot/Assets/skins/alpha.png b/DiscordBot/Assets/skins/alpha.png new file mode 100644 index 00000000..2926b123 Binary files /dev/null and b/DiscordBot/Assets/skins/alpha.png differ diff --git a/DiscordBot/Assets/skins/background.png b/DiscordBot/Assets/skins/background.png new file mode 100644 index 00000000..3b6e30c1 Binary files /dev/null and b/DiscordBot/Assets/skins/background.png differ diff --git a/DiscordBot/SERVER/skins/foreground.png b/DiscordBot/Assets/skins/foreground.png similarity index 100% rename from DiscordBot/SERVER/skins/foreground.png rename to DiscordBot/Assets/skins/foreground.png diff --git a/DiscordBot/Assets/skins/resources/background.backup.png b/DiscordBot/Assets/skins/resources/background.backup.png new file mode 100644 index 00000000..1f0e094b Binary files /dev/null and b/DiscordBot/Assets/skins/resources/background.backup.png differ diff --git a/DiscordBot/Assets/skins/resources/background.png b/DiscordBot/Assets/skins/resources/background.png new file mode 100644 index 00000000..fa85f332 Binary files /dev/null and b/DiscordBot/Assets/skins/resources/background.png differ diff --git a/DiscordBot/Assets/skins/resources/foreground_sleek.png b/DiscordBot/Assets/skins/resources/foreground_sleek.png new file mode 100644 index 00000000..e6a537f4 Binary files /dev/null and b/DiscordBot/Assets/skins/resources/foreground_sleek.png differ diff --git a/DiscordBot/SERVER/skins/skin.json b/DiscordBot/Assets/skins/skin.alaanor.json similarity index 87% rename from DiscordBot/SERVER/skins/skin.json rename to DiscordBot/Assets/skins/skin.alaanor.json index dd461397..0791f6e7 100644 --- a/DiscordBot/SERVER/skins/skin.json +++ b/DiscordBot/Assets/skins/skin.alaanor.json @@ -18,8 +18,8 @@ "StartY": 0, "Width": 640, "Height": 209, - "WhiteFix": true, - "DefaultColor": "#1B2631FF" + "WhiteFix" : true, + "DefaultColor" : "#1B2631CC" }, { "Type": "Username", @@ -87,7 +87,7 @@ "StartY": 162, "FontPointSize": 15, "FillColor": "#FFFF", - "TextAlignmnent": "Right" + "TextAlignmnent" : "Right" }, { "Type": "CustomText", @@ -103,7 +103,16 @@ "StartY": 183, "FontPointSize": 15, "FillColor": "#FFF", - "TextAlignmnent": "Right" + "TextAlignmnent" : "Right" + } + , + { + "Type": "TotalXp", + "StartX": 175, + "StartY": 200, + "FontPointSize": 15, + "FillColor": "#FFF", + "TextAlignmnent" : "Center" } ] }, diff --git a/DiscordBot/SERVER/skins/skin.default.json b/DiscordBot/Assets/skins/skin.default.json similarity index 81% rename from DiscordBot/SERVER/skins/skin.default.json rename to DiscordBot/Assets/skins/skin.default.json index 4d5fda93..110c01fd 100644 --- a/DiscordBot/SERVER/skins/skin.default.json +++ b/DiscordBot/Assets/skins/skin.default.json @@ -3,6 +3,8 @@ "Codename": "default", "Description": "Default avatar", "AvatarSize": 128, + "AvatarX": 36, + "AvatarY": 40, "Background": "background.png", "Layers": [ { @@ -37,8 +39,7 @@ "Font": "Consolas", "FillColor": "#000000FF", "StrokeColor": "#00000000", - "FontPointSize": 17, - "TextAlignment": "Center" + "FontPointSize": 17 }, { "Type": "Username", @@ -57,8 +58,7 @@ "StartY": 83, "FontPointSize": 17, "Font": "Consolas", - "FillColor": "#000000FF", - "TextAlignment": "Right" + "FillColor": "#000000FF" }, { "Type": "XpRank", @@ -66,8 +66,7 @@ "StartY": 108, "FontPointSize": 17, "Font": "Consolas", - "FillColor": "#000000FF", - "TextAlignment": "Right" + "FillColor": "#000000FF" }, { "Type": "KarmaRank", @@ -75,8 +74,7 @@ "StartY": 153, "FontPointSize": 17, "Font": "Consolas", - "FillColor": "#000000FF", - "TextAlignment": "Right" + "FillColor": "#000000FF" }, { "Type": "KarmaPoints", @@ -84,23 +82,16 @@ "StartY": 130, "FontPointSize": 17, "Font": "Consolas", - "FillColor": "#000000FF", - "TextAlignment": "Right" + "FillColor": "#000000FF" }, { "Type": "Level", "StartX": 220, "StartY": 140, "FontPointSize": 50, - "Font": "Consolas", - "TextAlignment": "Center" + "Font": "Consolas" } ] - }, - { - "Image": "avatar", - "StartX": 36, - "StartY": 40 } ] } \ No newline at end of file diff --git a/DiscordBot/Assets/skins/skin.json b/DiscordBot/Assets/skins/skin.json new file mode 100644 index 00000000..8d73efb9 --- /dev/null +++ b/DiscordBot/Assets/skins/skin.json @@ -0,0 +1,174 @@ +{ + "Name":"Sleek", + "Codename":"nomnom", + "Description": "Sleek, minimalist profile card", + "AvatarSize": 160, + "Background": "resources/background.png", + "Layers": [ + { + "Image": "resources/foreground_sleek.png", + "StartX": 0, + "StartY": 0, + "Width": 500, + "Height": 200, + "Modules": [ + { + "Type": "AvatarBorder", + "StartX": 14, + "StartY": 7, + "Size": 162 + }, + { + "Type": "XpBar", + "StartX": 12, + "StartY": 175, + "Width": 165, + "Height": 20, + "StrokeWidth": 1, + "OutsideStrokeColor": "#b2b2b2FF", + "OutsideFillColor": "#FFFFFFFF", + "InsideStrokeColor": "#b2b2b2FF", + "InsideFillColor": "#b2b2b2FF" + }, + { + "Type": "XpBarInfo", + "StartX": 95, + "StartY": 190, + "Width": 164, + "Height": 20, + "Font": "Roboto Mono", + "FontPointSize": 10, + "TextAlignment": "Center", + "FillColor": "#404040FF", + }, + { + "Type": "Username", + "StartX": 189, + "StartY": 115, + "Font": "Roboto Mono", + "FontPointSize": 20, + "FillColor": "#FFFFFFFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "CustomText", + "StartX": 231, + "StartY": 157, + "Text": "LEVEL", + "Font": "Roboto Mono", + "FontPointSize": 24, + "TextAlignment": "Center", + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00, + "TextKerning": 2, + }, + { + "Type": "CustomText", + "StartX": 284, + "StartY": 139, + "Text": "Total XP", + "Font": "Roboto Mono", + "FontPointSize": 16, + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "CustomText", + "StartX": 284, + "StartY": 157, + "Text": "Server Rank", + "Font": "Roboto Mono", + "FontPointSize": 16, + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "CustomText", + "StartX": 284, + "StartY": 175, + "Text": "Karma", + "Font": "Roboto Mono", + "FontPointSize": 16, + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "CustomText", + "StartX": 284, + "StartY": 193, + "Text": "Karma Rank", + "Font": "Roboto Mono", + "FontPointSize": 16, + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "Level", + "StartX": 231, + "StartY": 180, + "Font": "Roboto Mono", + "FontPointSize": 24, + "TextAlignment": "Center", + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "TotalXp", + "StartX": 490, + "StartY": 139, + "Font": "Roboto Mono", + "FontPointSize": 16, + "TextAlignment": "Right", + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "XpRank", + "StartX": 490, + "StartY": 157, + "Font": "Roboto Mono", + "FontPointSize": 16, + "TextAlignment": "Right", + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "KarmaPoints", + "StartX": 490, + "StartY": 175, + "Font": "Roboto Mono", + "FontPointSize": 16, + "TextAlignment": "Right", + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + { + "Type": "KarmaRank", + "StartX": 490, + "StartY": 193, + "Font": "Roboto Mono", + "FontPointSize": 16, + "TextAlignment": "Right", + "FillColor": "#4d4d4dFF", + "StrokeColor": "#00000000", + "StrokeWidth": 00 + }, + ] + }, + { + "Image": "avatar", + "StartX": 15, + "StartY": 8 + } + ] +} \ No newline at end of file diff --git a/DiscordBot/Attributes/ThreadAttributes.cs b/DiscordBot/Attributes/ThreadAttributes.cs deleted file mode 100644 index 8cd6583b..00000000 --- a/DiscordBot/Attributes/ThreadAttributes.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Discord.Commands; -using Discord.WebSocket; -using DiscordBot.Settings; -using Microsoft.Extensions.DependencyInjection; - -namespace DiscordBot.Attributes; - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class RequireThreadAttribute : PreconditionAttribute -{ - protected SocketThreadChannel _currentThread; - - public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - this._currentThread = context.Message.Channel as SocketThreadChannel; - if (this._currentThread != null) return await Task.FromResult(PreconditionResult.FromSuccess()); - - Task task = context.Message.DeleteAfterSeconds(seconds: 10); - return await Task.FromResult(PreconditionResult.FromError("This command can only be used in a thread.")); - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class RequireAutoThreadAttribute : RequireThreadAttribute -{ - protected AutoThreadChannel _autoThreadChannel; - - public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var res = await base.CheckPermissionsAsync(context, command, services); - if (!res.IsSuccess) return res; - - var settings = services.GetRequiredService(); - this._autoThreadChannel = settings.AutoThreadChannels.Find(x => this._currentThread.ParentChannel.Id == x.Id); - if (this._autoThreadChannel != null) return await Task.FromResult(PreconditionResult.FromSuccess()); - - Task task = context.Message.DeleteAfterSeconds(seconds: 10); - return await Task.FromResult(PreconditionResult.FromError("This command can only be used in a thread created automatically.")); - - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class RequireArchivableAutoThreadAttribute : RequireAutoThreadAttribute -{ - public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var res = await base.CheckPermissionsAsync(context, command, services); - if (!res.IsSuccess) return res; - - if (this._autoThreadChannel.CanArchive) return await Task.FromResult(PreconditionResult.FromSuccess()); - - Task task = context.Message.DeleteAfterSeconds(seconds: 10); - return await Task.FromResult(PreconditionResult.FromError("This command cannot be used in a this thread.")); - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class RequireDeletableAutoThreadAttribute : RequireAutoThreadAttribute -{ - public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var res = await base.CheckPermissionsAsync(context, command, services); - if (!res.IsSuccess) return res; - - if (this._autoThreadChannel.CanDelete) return await Task.FromResult(PreconditionResult.FromSuccess()); - - Task task = context.Message.DeleteAfterSeconds(seconds: 10); - return await Task.FromResult(PreconditionResult.FromError("This command cannot be used in a this thread.")); - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class RequireAutoThreadAuthorAttribute : RequireAutoThreadAttribute -{ - public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) - { - var res = await base.CheckPermissionsAsync(context, command, services); - if (!res.IsSuccess) return res; - - var messages = await this._currentThread.GetPinnedMessagesAsync(); - var firstMessage = messages.LastOrDefault(); - if (firstMessage != null) - { - var user = (SocketGuildUser)context.Message.Author; - if (firstMessage.MentionedUsers.Any(x => x.Id == context.User.Id)) - return await Task.FromResult(PreconditionResult.FromSuccess()); - } - - Task task = context.Message.DeleteAfterSeconds(seconds: 10); - return await Task.FromResult(PreconditionResult.FromError("This command can only be used by the thread author.")); - } -} \ No newline at end of file diff --git a/DiscordBot/DiscordBot.csproj b/DiscordBot/DiscordBot.csproj index 22a99c92..dc5c4509 100644 --- a/DiscordBot/DiscordBot.csproj +++ b/DiscordBot/DiscordBot.csproj @@ -10,14 +10,13 @@ + - - \ No newline at end of file diff --git a/DiscordBot/Extensions/ReactMessageExtensions.cs b/DiscordBot/Extensions/ReactMessageExtensions.cs deleted file mode 100644 index 940304fc..00000000 --- a/DiscordBot/Extensions/ReactMessageExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using DiscordBot.Settings; - -namespace DiscordBot.Extensions; - -public static class ReactMessageExtensions -{ - public static string MessageLinkBack(this UserReactMessage message, ulong guildId) - { - if (message == null) return ""; - return $"https://discordapp.com/channels/{guildId.ToString()}/{message.ChannelId.ToString()}/{message.MessageId.ToString()}"; - } -} \ No newline at end of file diff --git a/DiscordBot/Modules/ReactionRoleModule.cs b/DiscordBot/Modules/ReactionRoleModule.cs deleted file mode 100644 index 33fb26a7..00000000 --- a/DiscordBot/Modules/ReactionRoleModule.cs +++ /dev/null @@ -1,292 +0,0 @@ -using Discord.Commands; -using DiscordBot.Services; -using DiscordBot.Settings; - -namespace DiscordBot.Modules; - -[Group("ReactRole")] -public class ReactionRoleModule : ModuleBase -{ - #region Dependency Injection - - public CommandHandlingService CommandHandlingService { get; set; } - public ILoggingService LoggingService { get; set; } - public ReactRoleService ReactRoleService { get; set; } - - #endregion - - #region Config - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Duration between bulk role changes in ms. Default: 5000")] - [Command("Delay")] - [Priority(99)] - public async Task SetReactRoleDelay(uint delay) - { - if (ReactRoleService.SetReactRoleDelay(delay)) - await Context.Message.AddReactionAsync(new Emoji("👍")); - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Log all updates, gets spammy. Default: false")] - [Command("Log")] - [Priority(98)] - public async Task SetReactLogState(bool state) - { - if (ReactRoleService.SetReactLogState(state)) - await Context.Message.AddReactionAsync(new Emoji("👍")); - } - - #endregion - - #region MessageMaking - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Begins message setup.")] - [Command("NewMessage")] - [Priority(0)] - public async Task NewMessageSetup(IMessageChannel channel, ulong messageId, string description = "") - { - if (ReactRoleService.IsPreparingMessage) - await ReplyAsync("A message is already being prepared, please finish."); - else - { - var linkedMessage = await channel.GetMessageAsync(messageId); - if (linkedMessage == null) - { - await ReplyAsync($"Message ID \"{messageId}\" passed in does not exist"); - return; - } - - ReactRoleService.NewMessage = new UserReactMessage - { - ChannelId = channel.Id, - Description = description, - MessageId = messageId - }; - await ReplyAsync( - $"Setup began, Reaction roles will be attached to {linkedMessage.GetJumpUrl()}"); - await ReplyAsync( - "Use `ReactRole Emote` \"Role\" \"Emoji\" \"Name (optional)\" to add emotes."); - } - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Allows making changes to previously saved messages.")] - [Command("EditMessage")] - [Priority(1)] - public async Task EditMessageSetup(ulong messageId, string description = "") - { - if (ReactRoleService.IsPreparingMessage) - await ReplyAsync("A message is already being prepared, please finish."); - else - { - var foundMessage = - ReactRoleService.ReactSettings.UserReactRoleList.Find(reactMessage => - reactMessage.MessageId == messageId); - if (foundMessage == null) - { - await ReplyAsync("No message with that ID exists"); - return; - } - - ReactRoleService.NewMessage = foundMessage; - await ReplyAsync( - $"Message linked, future changes will be made to {foundMessage.MessageLinkBack(Context.Guild.Id)}"); - } - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Adds/Removes Roles/Emotes to the prepared message. Removes role if emoteId is 0")] - [Command("Emote")] - [Priority(2)] - public async Task AddEmoteRoles(IRole role, string emoteString = "", string name = "") - { - if (!ReactRoleService.IsPreparingMessage) - { - await ReplyAsync("No message is being prepared, use NewMessage first!"); - return; - } - - // If empty we check the message for the role passed in, if it exists we remove that role from the message. - if (emoteString == string.Empty) - { - var reactionRemove = - ReactRoleService.NewMessage.Reactions?.Find(reactRole => reactRole.RoleId == role.Id); - if (reactionRemove == null) - { - await ReplyAsync($"Role {role.Name} was not attached to this message."); - return; - } - - ReactRoleService.NewMessage.Reactions.Remove(reactionRemove); - await ReplyAsync($"Removed Role {role.Name} from message."); - return; - } - - // Try pull the Emote ID from the emote used. - Emote tempEmote; - if (!Emote.TryParse(emoteString, out tempEmote)) - { - await ReplyAsync($"Emote ({emoteString}) does not exist in this server."); - return; - } - - // We make sure we have access to it on this server - var emote = await Context.Guild.GetEmoteAsync(tempEmote.Id); - if (emote == null) - { - await ReplyAsync($"Failed to use ({emoteString}), unknown error."); - return; - } - - if (name == string.Empty) - name = emote.Name; - - // Add our Reaction Role - ReactRoleService.NewMessage.Reactions ??= new List(); - var newRole = new ReactRole(name, role.Id, emote.Id); - ReactRoleService.NewMessage.Reactions.Add(newRole); - - await Context.Message.AddReactionAsync(emote); - await Context.Message.AddReactionAsync(new Emoji("👍")); - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Preview the message being prepared.")] - [Command("Preview")] - [Priority(3)] - public async Task PreviewNewReactionMessage() - { - if (!ReactRoleService.IsPreparingMessage) - { - await ReplyAsync("No message is being prepared, use NewMessage first!"); - return; - } - - var config = ReactRoleService.NewMessage; - foreach (var configReaction in config.Reactions) - { - var emote = await Context.Guild.GetEmoteAsync(configReaction.EmojiId); - await Context.Message.AddReactionAsync(emote); - } - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Cancel the current reaction roles.")] - [Command("Cancel")] - [Priority(4)] - public async Task CancelNewReactionMessage() - { - if (!ReactRoleService.IsPreparingMessage) - { - await ReplyAsync("No message is being prepared!"); - return; - } - - ReactRoleService.NewMessage = null; - await Context.Message.AddReactionAsync(new Emoji("👍")); - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Saves the message being prepared.")] - [Command("Save")] - [Priority(10)] - public async Task SaveNewReactionMessage() - { - if (!ReactRoleService.IsPreparingMessage) - { - await ReplyAsync("No message is being prepared, use NewMessage first!"); - return; - } - - await ReplyAsync("Saving Values, use ***reactrole restart*** to enable the changes."); - ReactRoleService.StoreNewMessage(); - } - - #endregion - - #region Additional Functions - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Delete a stored ReactionRole Configuration.")] - [Command("DeleteConfig")] - [Priority(11)] - public async Task DeleteReactionRoleConfig(uint messageId) - { - var foundMessage = - ReactRoleService.ReactSettings.UserReactRoleList.Find(reactMessage => - reactMessage.MessageId == messageId); - if (foundMessage == null) - { - await ReplyAsync("No message with that ID exists"); - return; - } - - ReactRoleService.ReactSettings.UserReactRoleList.Remove(foundMessage); - await ReplyAsync( - "Deleted the configuration for that ID, use \"!reactrole restart\" to enable these changes."); - await LoggingService.LogChannelAndFile( - $"{Context.User} deleted the reactionrole configuration for `{foundMessage}`."); - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Lists out all current reaction messages.")] - [Command("List")] - [Priority(80)] - public async Task ListReactionRoleConfig() - { - var messageList = ReactRoleService.ReactSettings.UserReactRoleList; - if (messageList.Count == 0) - { - await ReplyAsync("No messages currently stored."); - return; - } - - foreach (var reactMessage in messageList) - { - var linkedInfoMessage = - await ReplyAsync( - $"Linked Location: {reactMessage.MessageLinkBack(Context.Guild.Id)} which should contain {reactMessage.RoleCount()} emotes.\n"); - foreach (var reactRole in reactMessage.Reactions) - { - var emote = await Context.Guild.GetEmoteAsync(reactRole.EmojiId); - if (emote != null) - await linkedInfoMessage.AddReactionAsync(emote); - } - } - } - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Restarts the ReactRoleService.")] - [Command("Restart")] - [Priority(90)] - public async Task ReactRestartService() - { - await LoggingService.LogAction($"{Context.User} restarted the ReactionRole service."); - await ReplyAsync("Reaction role service is restarting."); - var results = await ReactRoleService.Restart(); - if (results) - await Context.Message.AddReactionAsync(new Emoji("👍")); - else - await ReplyAsync("Failed to restart reaction role service."); - } - - #endregion - - #region CommandList - - [RequireUserPermission(GuildPermission.Administrator)] - [Summary("Does what you see now.")] - [Command("Help")] - [Priority(100)] - public async Task ReactionRoleHelp() - { - foreach (var textMessage in CommandHandlingService.GetCommandListMessages("ReactRole")) - { - await ReplyAsync(textMessage); - } - } - - #endregion -} \ No newline at end of file diff --git a/DiscordBot/Modules/UserModule.cs b/DiscordBot/Modules/UserModule.cs index 4cdd353d..ced5b0c4 100644 --- a/DiscordBot/Modules/UserModule.cs +++ b/DiscordBot/Modules/UserModule.cs @@ -21,20 +21,19 @@ public class UserModule : ModuleBase public ILoggingService LoggingService { get; set; } public CurrencyService CurrencyService { get; set; } public DatabaseService DatabaseService { get; set; } - public PublisherService PublisherService { get; set; } public UpdateService UpdateService { get; set; } public CommandHandlingService CommandHandlingService { get; set; } public WeatherService WeatherService { get; set; } public UserExtendedService UserExtendedService { get; set; } public BotSettings Settings { get; set; } public Rules Rules { get; set; } - + #endregion - + private readonly Random _random = new(); private FuzzTable _slapObjects = new(); private FuzzTable _slapFails = new(); - + [Command("Help"), Priority(100)] [Summary("Does what you see now.")] [Alias("command", "commands")] @@ -322,7 +321,6 @@ await ReplyAsync( "!role add/remove XR-Developers - If you're a VR, AR or MR sorcerer. \n" + "!role add/remove Writers - If you like writing lore, scenarios, characters and stories. \n" + "```"); - await ReplyAsync("```To get the publisher role type **!pinfo** and follow the instructions.```\n"); } } #region All Rules @@ -524,14 +522,14 @@ public async Task DisplayProfile(IUser user) try { await Context.Message.DeleteAsync(); - + var profileCard = await UserService.GenerateProfileCard(user); if (string.IsNullOrEmpty(profileCard)) { await ReplyAsync("Failed to generate profile card.").DeleteAfterSeconds(seconds: 10); return; } - + var profile = await Context.Channel.SendFileAsync(profileCard); await profile.DeleteAfterTime(minutes: 3); } @@ -551,7 +549,7 @@ public async Task JoinDate() await ReplyAsync($"{Context.User.Mention} you joined **{joinDate:dddd dd/MM/yyy HH:mm:ss}**"); await Context.Message.DeleteAsync(); } - + [Command("SetCity"), Priority(100)] [Alias("SetDefaultCity")] [Summary("Set 'Default City' which can be used by various commands.")] @@ -690,14 +688,14 @@ public async Task CoinFlip() await ReplyAsync($"**{uname}** flipped a coin and got **{coin[_random.Next() % 2]}**!"); await Context.Message.DeleteAfterSeconds(seconds: 1); } - + [Command("Roll"), Priority(23)] [Summary("Roll a dice. Syntax: !roll [sides]")] - public async Task RollDice(int sides=20) + public async Task RollDice(int sides = 20) { await RollDice(sides, 0); } - + [Command("Roll"), Priority(23)] [Summary("Roll a dice. Syntax: !roll [sides] [minimum]")] public async Task RollDice(int sides, int number) @@ -718,69 +716,20 @@ public async Task RollDice(int sides, int number) message = " :white_check_mark: " + message + " [Needed: " + number + "]"; else message = " :x: " + message + " [Needed: " + number + "]"; - + await ReplyAsync(message); await Context.Message.DeleteAfterSeconds(seconds: 1); } [Command("D20"), Priority(23)] [Summary("Roll a D20 dice. Syntax: !d20 [minimum]")] - public async Task RollD20(int number=0) + public async Task RollD20(int number = 0) { await RollDice(20, number); } #endregion - #region Publisher - - [Command("PInfo"), BotCommandChannel, Priority(11)] - [Summary("Information on how to get publisher role.")] - [Alias("publisherinfo")] - public async Task PublisherInfo() - { - var builder = new EmbedBuilder() - .WithTitle("Publisher Commands") - .WithDescription("Use these commands to get the **Asset-Publisher** role.") - .AddField("1️⃣ `!publisher `", "Example: `!publisher 12345`.\nReceive a code on the email associated with your publisher account.\nTo get your ID: assetstore.unity.com/publishers/**YourID**.") - .AddField("2️⃣ `!verify `", "Example: `!publisher 12345 6789`.\nVerify your ID with the code sent to your email."); - var embed = builder.Build(); - - await ReplyAsync(embed: embed); - await Context.Message.DeleteAfterSeconds(seconds: 2); - } - - [Command("Publisher"), BotCommandChannel, HideFromHelp] - [Summary("Get the Asset-Publisher role by verifying who you are. Syntax: !publisher publisherID")] - public async Task Publisher(uint publisherId) - { - if (((SocketGuildUser)Context.Message.Author).Roles.Any(x => x.Id == Settings.PublisherRoleId)) - { - await ReplyAsync($"{Context.Message.Author.Mention} you already have the `Asset-Publisher` role."); - } - else if (Settings.Email == string.Empty) - { - await ReplyAsync("The `Asset-Publisher` role is currently disabled."); - } - else - { - var verify = await PublisherService.VerifyPublisher(publisherId, Context.User.Username); - await ReplyAsync(verify.Item2); - } - await Context.Message.DeleteAfterSeconds(seconds: 1); - } - - [Command("Verify"), BotCommandChannel, HideFromHelp] - [Summary("Verify a publisher with the code received by email. Syntax : !verify publisherId code")] - public async Task VerifyPackage(uint packageId, string code) - { - await Context.Message.DeleteAfterSeconds(seconds: 0); - var verif = await PublisherService.ValidatePublisherWithCode(Context.Message.Author, packageId, code); - await ReplyAsync(verif); - } - - #endregion - #region Search [Command("Search"), Priority(25)] [Summary("Searches DuckDuckGo for results. Syntax: !search c# lambda help")] @@ -867,7 +816,7 @@ public async Task SearchManual(params string[] queries) { var curScore = CalculateScore(p[1], query); if (!(curScore < minimumScore)) continue; - + minimumScore = curScore; mostSimilarPage = p; } @@ -881,7 +830,7 @@ public async Task SearchManual(params string[] queries) embedBuilder.Color = new Color(81, 50, 169); embedBuilder.Footer = new EmbedFooterBuilder().WithText("Results sourced from Unity3D Docs."); var message = await ReplyAsync(embed: embedBuilder.Build()); - + var doc = new HtmlWeb().Load($"https://docs.unity3d.com/Manual/{mostSimilarPage[0]}.html"); // Get first Header as this'll contain the main part we need var descriptionNode = doc.DocumentNode.SelectSingleNode("//h1"); @@ -914,11 +863,11 @@ public async Task SearchApi(params string[] queries) { var curScore = CalculateScore(p[1], query); if (!(curScore < minimumScore)) continue; - + minimumScore = curScore; mostSimilarPage = p; } - + // If a page has been found (should be), return the message, else return information if (mostSimilarPage != null) { @@ -928,7 +877,7 @@ public async Task SearchApi(params string[] queries) embedBuilder.Color = new Color(81, 50, 169); embedBuilder.Footer = new EmbedFooterBuilder().WithText("Results sourced from Unity3D Docs."); var message = await ReplyAsync(embed: embedBuilder.Build()); - + // Load the page, and look for a

Description

tag, and then get the next

tag var doc = new HtmlWeb().Load($"https://docs.unity3d.com/ScriptReference/{mostSimilarPage[0]}.html"); var descriptionNode = doc.DocumentNode.SelectSingleNode("//h3[contains(text(), 'Description')]"); @@ -1109,7 +1058,7 @@ public async Task Birthday() { // URL to cell C15/"Next birthday" cell from Corn's google sheet const string nextBirthday = "https://docs.google.com/spreadsheets/d/10iGiKcrBl1fjoBNTzdtjEVYEgOfTveRXdI5cybRTnj4/gviz/tq?tqx=out:html&range=C15:C15"; - + var tableText = await WebUtil.GetHtmlNodeInnerText(nextBirthday, "/html/body/table/tr[2]/td"); var message = $"**{tableText}**"; @@ -1126,7 +1075,7 @@ public async Task Birthday(IUser user) // URL to columns B to D of Corn's google sheet const string birthdayTable = "https://docs.google.com/spreadsheets/d/10iGiKcrBl1fjoBNTzdtjEVYEgOfTveRXdI5cybRTnj4/gviz/tq?tqx=out:html&gid=318080247&range=B:D"; var relevantNodes = await WebUtil.GetHtmlNodes(birthdayTable, "/html/body/table/tr"); - + var birthdate = default(DateTime); HtmlNode matchedNode = null; @@ -1138,10 +1087,10 @@ public async Task Birthday(IUser user) // XPath to the name column (C) var nameNode = row.SelectSingleNode("td[2]"); var name = nameNode.InnerText; - + if (!name.ToLower().Contains(searchName.ToLower()) || name.Length >= matchedLength) continue; - + // Check for a "Closer" match matchedNode = row; matchedLength = name.Length; @@ -1238,8 +1187,8 @@ public async Task Translate(string text, string language = "en") #endregion #region Currency - - [Command("CurrencyName") , Priority(29)] + + [Command("CurrencyName"), Priority(29)] [Summary("Get the name of a currency. Syntax : !currname USD")] [Alias("currname")] public async Task CurrencyName(string currency) @@ -1262,7 +1211,7 @@ public async Task ConvertCurrency(string from, string to = "usd") { await ConvertCurrency(1, from, to); } - + [Command("Currency"), Priority(29)] [Summary("Converts a currency. Syntax : !currency amount fromCurrency toCurrency")] [Alias("curr")] @@ -1284,7 +1233,7 @@ public async Task ConvertCurrency(double amount, string from, string to = "usd") // We check if both currencies are valid bool fromValid = await CurrencyService.IsCurrency(from.ToLower()); bool toValid = await CurrencyService.IsCurrency(to.ToLower()); - + // Check if valid if (!fromValid || !toValid) { @@ -1304,43 +1253,4 @@ public async Task ConvertCurrency(double amount, string from, string to = "usd") } #endregion - - #region AutoThread - - [Command("Autothread close")] - [Alias("Autothread archive", "Att close", "Att archive")] - [Summary("Archive an auto-thread and rename it automatically according to channel-specific settings.")] - [RequireArchivableAutoThread] - [RequireAutoThreadAuthor(Group = "AuthorOrMod")] - [RequireModerator(Group = "AuthorOrMod")] - public async Task CloseAutoThread() - { - var currentThread = Context.Message.Channel as SocketThreadChannel; - var autoTheadConfig = Settings.AutoThreadChannels.Find(x => currentThread.ParentChannel.Id == x.Id); - - var newName = autoTheadConfig.GenerateTitleArchived(Context.User); - if (currentThread.Name.Equals(newName)) return; - await currentThread.ModifyAsync(x => - { - x.Archived = true; - x.Locked = true; - x.Name = newName; - }); - } - - [Command("Autothread delete")] - [Alias("Att delete")] - [Summary("Delete an auto-thread.")] - [RequireDeletableAutoThread] - [RequireAutoThreadAuthor(Group = "AuthorOrMod")] - [RequireModerator(Group = "AuthorOrMod")] - public async Task DeleteAutoThread() - { - var currentThread = Context.Message.Channel as SocketThreadChannel; - var autoTheadConfig = Settings.AutoThreadChannels.Find(x => currentThread.ParentChannel.Id == x.Id); - - await currentThread.DeleteAsync(); - } } - -#endregion diff --git a/DiscordBot/Program.cs b/DiscordBot/Program.cs index 6e78ffa8..03989537 100644 --- a/DiscordBot/Program.cs +++ b/DiscordBot/Program.cs @@ -96,13 +96,11 @@ private IServiceProvider ConfigureServices() => .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/DiscordBot/SERVER/images/Layout.txt b/DiscordBot/SERVER/images/Layout.txt deleted file mode 100644 index 5564391d..00000000 --- a/DiscordBot/SERVER/images/Layout.txt +++ /dev/null @@ -1,61 +0,0 @@ -XP Bar: -- Back_Rect: (104, 39, 232, 16) (X,Y,W,H) -- Fore_Rect: (105, 40, 126, 14) (X,Y,W,H) -- Background:(#3F3F3F) -- Foreground:(#00F0FF) - -Rank Triangle: -- Rect: (346, 12, 54, 104) (X,Y,W,H) -- Color:(Based on UDH Rank Color) - -Fonts: -- Name: Consolas (24pt, #3C3C3C) -- Level: Consolas (59pt, #3C3C3C) -- Meta: Opens Sans (16pt, #3C3C3C) - -Effects (If Possible): -- Card: Drop-Shadow (16px, 50% Opacity), Alpha-Blend (75% Opacity) -- XP Bar: Drop-Shadow (4px, 50% Opacity) -- Level: Drop-Shadow (4px, 25% Opacity) - - -NEW -========================================== -Image: -- Role_Rect: (11, 11, 113, 113) (X,Y,W,H) -- Picture_Rect: (27, 17, 102, 102) (X,Y,W,H) - -Name: -- Rect: (131, 18, 272, 26) (X,Y,W,H) -- Font: Consolas - Regular (24pt, #3a3a3a) - -XP-Bar: -- Back_Rect: (131, 44, 275, 15) (X,Y,W,H) -- Front_Rect: (133, 46, 271, 11) (X,Y,W,H) <- filled -- Text: (131, 45, 272, 10) (X,Y,W,H) -- Font: Consolas - Regular (11pt, #3a3a3a) -- Color: #c5c5c7 - -LEVEL Text -- Rect: (131, 63, 90, 18) (X,Y,W,H) -- Font: Consolas - Bold (22pt, #3a3a3a) - Tracking: 40 - Center-align - -Level # Text -- Rect: (131, 89, 90, 30) (X,Y,W,H) -- Font: Consolas - Bold (30pt, #3a3a3a) - Center-align - -Server Rank Text -- Rect: (235, 67, 109, 18) (X,Y,W,H) -- Font: Consolas - Regular (16pt, #3a3a3a) - -Server Rank Value Text -- Rect: (344, 67, 45, 18) (X,Y,W,H) -- Font: Consolas - Regular (16pt, #3a3a3a) - Right-align - -Karma Points Text -- Rect: (235, 95, 109, 18) (X,Y,W,H) -- Font: Consolas - Regular (16pt, #3a3a3a) - -Server Rank Value Text -- Rect: (344, 95, 45, 18) (X,Y,W,H) -- Font: Consolas - Regular (16pt, #3a3a3a) - Right-align \ No newline at end of file diff --git a/DiscordBot/SERVER/images/background.psd b/DiscordBot/SERVER/images/background.psd deleted file mode 100644 index 84975adf..00000000 Binary files a/DiscordBot/SERVER/images/background.psd and /dev/null differ diff --git a/DiscordBot/SERVER/images/foreground.psd b/DiscordBot/SERVER/images/foreground.psd deleted file mode 100644 index d476dd7e..00000000 Binary files a/DiscordBot/SERVER/images/foreground.psd and /dev/null differ diff --git a/DiscordBot/SERVER/images/levelupcard.psd b/DiscordBot/SERVER/images/levelupcard.psd deleted file mode 100644 index 8003bcf4..00000000 Binary files a/DiscordBot/SERVER/images/levelupcard.psd and /dev/null differ diff --git a/DiscordBot/SERVER/skins/background.png b/DiscordBot/SERVER/skins/background.png deleted file mode 100644 index 3838fcde..00000000 Binary files a/DiscordBot/SERVER/skins/background.png and /dev/null differ diff --git a/DiscordBot/Services/PublisherService.cs b/DiscordBot/Services/PublisherService.cs deleted file mode 100644 index 3d147e51..00000000 --- a/DiscordBot/Services/PublisherService.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Net; -using System.Security.Cryptography; -using System.Text.RegularExpressions; -using Discord.WebSocket; -using DiscordBot.Settings; -using MailKit.Net.Smtp; -using MimeKit; - -namespace DiscordBot.Services; - -public class PublisherService -{ - private readonly BotSettings _settings; - private readonly Dictionary _verificationCodes; - - public PublisherService(BotSettings settings) - { - _verificationCodes = new Dictionary(); - _settings = settings; - } - - /* - blogfeed (xml) => https://blogs.unity3d.com/feed/ - */ - - // Attempts to get a publishers email from the unity asset store and emails them with confirmation codes to verify their account. - public async Task<(bool, string)> VerifyPublisher(uint publisherId, string name) - { - // Unity is 1, we probably don't want to email them. - if (publisherId < 2) - return (false, "Invalid publisher ID."); - - using (var webClient = new WebClient()) - { - // For the record, this is a terrible way of pulling this information. - var content = await webClient.DownloadStringTaskAsync($"https://assetstore.unity.com/publishers/{publisherId}"); - if (!content.Contains("Error 404")) - { - var email = string.Empty; - var emailMatch = new Regex("mailto:([^\"]+)").Match(content); - if (emailMatch.Success) - email = emailMatch.Groups[1].Value; - - if (email.Length > 2) - { - // No easy way to take their name, so we pass their discord name in. - await SendVerificationCode(name, email, publisherId); - return (true, "An email with a validation code was sent.\nPlease type `!verify ` to verify your publisher account.\nThis code will be valid for 30 minutes."); - } - } - return (false, "We failed to confirm this Publisher ID, double check and try again in a few minutes."); - } - } - - public async Task SendVerificationCode(string name, string email, uint packageId) - { - var random = new byte[9]; - var rand = RandomNumberGenerator.Create(); - rand.GetBytes(random); - - var code = Convert.ToBase64String(random); - - var body = string.Join('\n', new string[] { - $"Somebody named @{name} on the Unity Developer Community Discord server " + - "has requested the Publisher role on that server.", - "", - $"Here's your validation code: {code}", - "", - $"If this is you, enter the command '!verify {packageId} {code}' to complete the process.", - "", - "If this is NOT you, feel free to join the UDC Discord server " + - $"to report @{name} to the @Administrators (and join our community if you like).", - "https://discord.gg/bu3bbby" - }); - - _verificationCodes[packageId] = code; - var message = new MimeMessage(); - message.From.Add(new MailboxAddress("Unity Developer Community", _settings.Email)); - message.To.Add(new MailboxAddress(name, email)); - message.Subject = "Unity Developer Community Package Validation"; - message.Body = new TextPart("plain") { Text = body }; - - using (var client = new SmtpClient()) - { - client.CheckCertificateRevocation = false; - await client.ConnectAsync(_settings.EmailSMTPServer, _settings.EmailSMTPPort, MailKit.Security.SecureSocketOptions.SslOnConnect); - - client.AuthenticationMechanisms.Remove("XOAUTH2"); - await client.AuthenticateAsync(_settings.EmailUsername, _settings.EmailPassword); - - await client.SendAsync(message); - await client.DisconnectAsync(true); - } - - //TODO Delete code after 30min - } - - // User is verified if they have a code that matches the one in _verificationCodes and given `Asset-Publisher` role if so. - public async Task ValidatePublisherWithCode(IUser user, uint packageId, string code) - { - if (!_verificationCodes.TryGetValue(packageId, out string c)) - return "An error occurred while trying to verify your publisher account. Please check your ID is valid."; - if (c != code) - return "The verification code is not valid. Please check and try again."; - - // Give the user the publisher role. - await ((SocketGuildUser)user) - .AddRoleAsync(((SocketGuildUser)user) - .Guild.GetRole(_settings.PublisherRoleId)); - // Remove this code since it is now used. - _verificationCodes.Remove(packageId); - - return "Your publisher account has been verified and you now have the `Asset-Publisher` role!"; - } -} diff --git a/DiscordBot/Services/ReactRoleService.cs b/DiscordBot/Services/ReactRoleService.cs deleted file mode 100644 index 38d129ba..00000000 --- a/DiscordBot/Services/ReactRoleService.cs +++ /dev/null @@ -1,299 +0,0 @@ -using System.IO; -using Discord.WebSocket; -using DiscordBot.Settings; -using Newtonsoft.Json; - -namespace DiscordBot.Services; - -public class ReactRoleService -{ - private const string ServiceName = "ReactRoleService"; - - private const string ReactionSettingsPath = @"Settings/ReactionRoles.json"; - private readonly DiscordSocketClient _client; - private readonly Dictionary _guildEmotes = new Dictionary(); - // GuildRoles uses EmojiID as Key - private readonly Dictionary _guildRoles = new Dictionary(); - private readonly ILoggingService _loggingService; - - private readonly Dictionary _pendingUserUpdate = new Dictionary(); - - // Dictionaries to simplify lookup - private readonly Dictionary _reactMessages = new Dictionary(); - - private readonly BotSettings _settings; - - private bool _isRunning; - - public UserReactMessage NewMessage; - - public ReactRoleSettings ReactSettings; - - public ReactRoleService(DiscordSocketClient client, ILoggingService logging, BotSettings settings) - { - _loggingService = logging; - _settings = settings; - - _client = client; - - if (!_settings.ReactRoleServiceEnabled) - { - LoggingService.LogServiceDisabled(ServiceName, nameof(_settings.ReactRoleServiceEnabled)); - return; - } - LoggingService.LogServiceEnabled(ServiceName); - - _client.ReactionAdded += ReactionAdded; - _client.ReactionRemoved += ReactionRemoved; - - Task.Run(async () => _isRunning = await StartService()); - } - - // These are for the Modules to reference if/when setting up new message roles. - public bool IsPreparingMessage => NewMessage != null; - - ///

- /// Loads settings, this should just be message ids, and emotes/role ids - /// - private void LoadSettings() - { - try - { - // If file doesn't exist, we make an empty file with the default values. - if (!File.Exists(ReactionSettingsPath)) - { - var reactSettings = new ReactRoleSettings(); - var settingsContent = JsonConvert.SerializeObject(reactSettings, Formatting.Indented); - File.WriteAllText(ReactionSettingsPath, settingsContent); - } - else - { - using var file = File.OpenText(ReactionSettingsPath); - ReactSettings = JsonConvert.DeserializeObject(file.ReadToEnd()); - } - } - catch (Exception ex) - { - LoggingService.LogToConsole($"[{ServiceName}] Failed to Deserialize 'ReactionRoles.Json' err: {ex.Message}", LogSeverity.Error); - _isRunning = false; - } - } - - private void SaveSettings() - { - try - { - var settingsContent = JsonConvert.SerializeObject(ReactSettings, Formatting.Indented); - File.WriteAllText(ReactionSettingsPath, settingsContent); - } - catch (Exception ex) - { - LoggingService.LogToConsole($"[{ServiceName}] Failed to Serialize 'ReactionRoles.Json' err: {ex.Message}", LogSeverity.Error); - _isRunning = false; - } - } - - private async Task StartService() - { - LoadSettings(); - - // Escape early since we have nothing to process - if (ReactSettings.UserReactRoleList == null) - { - _isRunning = true; - return true; - } - - // Get our Emotes - var serverGuild = _client.GetGuild(_settings.GuildId); - if (serverGuild == null) - { - await _loggingService.LogAction($"[{ServiceName}] ReactRoleService failed to start, could not return guild information.", ExtendedLogSeverity.Warning); - return false; - } - - for (var messageIndex = 0; messageIndex < ReactSettings.UserReactRoleList.Count; messageIndex++) - { - var reactMessage = ReactSettings.UserReactRoleList[messageIndex]; - // Channel used for message - var messageChannel = _client.GetChannel(reactMessage.ChannelId) as IMessageChannel; - if (messageChannel == null) - { - LoggingService.LogToConsole($"[{ServiceName}] ReactRoleService: Channel {reactMessage.ChannelId} does not exist.", LogSeverity.Warning); - continue; - } - - // Get The Message for this group of reactions - if (!_reactMessages.ContainsKey(reactMessage.MessageId)) _reactMessages.Add(reactMessage.MessageId, await messageChannel.GetMessageAsync(reactMessage.MessageId) as IUserMessage); - for (var i = 0; i < reactMessage.RoleCount(); i++) - { - // We check if emote exists - var emote = serverGuild.Emotes.First(guildEmote => guildEmote.Id == reactMessage.Reactions[i].EmojiId); - - // Add a Reference to our Roles to simplify lookup - if (!_guildRoles.ContainsKey(reactMessage.Reactions[i].EmojiId)) _guildRoles.Add(reactMessage.Reactions[i].EmojiId, serverGuild.GetRole(reactMessage.Reactions[i].RoleId)); - // Same for the Emojis, saves look-arounds - if (!_guildEmotes.ContainsKey(reactMessage.Reactions[i].EmojiId)) _guildEmotes.Add(reactMessage.Reactions[i].EmojiId, emote); - // If our message doesn't have the emote, we add it. - if (!_reactMessages[reactMessage.MessageId].Reactions.ContainsKey(_guildEmotes[reactMessage.Reactions[i].EmojiId])) - { - LoggingService.LogToConsole($"[{ServiceName}] Added Reaction to Message {reactMessage.MessageId} which was missing.", LogSeverity.Info); - // We could add these in bulk, but that'd require a bit more setup - await _reactMessages[reactMessage.MessageId].AddReactionAsync(emote); - } - } - } - - _isRunning = true; - return true; - } - - private async Task ReactionAdded(Cacheable message, Cacheable channel, SocketReaction reaction) - { - if (!_isRunning) - return; - if (_reactMessages.ContainsKey(message.Id)) await ReactionChangedAsync(reaction.User.Value as IGuildUser, reaction.Emote as Emote, true); - } - - private async Task ReactionRemoved(Cacheable message, Cacheable channel, SocketReaction reaction) - { - if (!_isRunning) - return; - if (_reactMessages.ContainsKey(message.Id)) - if (_reactMessages.ContainsKey(message.Id)) - await ReactionChangedAsync(reaction.User.Value as IGuildUser, reaction.Emote as Emote, false); - } - - private async Task ReactionChangedAsync(IGuildUser user, Emote emote, bool state) - { - if (!IsUserValid(user)) return; - - if (_guildRoles.TryGetValue(emote.Id, out var targetRole)) await UpdatePendingRolesAsync(user, targetRole, state); - } - - /// - /// Updates users roles in bulk, this prevents discord from crying. - /// We check the last time they tried to change the role (if they've clicked multiple) to save API calls, Discord will - /// quickly complain if we hit them with to many requests. - /// - private async Task UpdateUserRoles(IGuildUser user) - { - //TODO Implement a watcher, prevent people from changing their roles every few minutes. Not super important. - if (!_pendingUserUpdate.TryGetValue(user, out var userData)) return; - - // Wait for a bit to give user to choose all their roles. - // we add a bit more to the end just so we don't always hit a second delay if they only selected 1 emote. - await Task.Delay((int)ReactSettings.RoleAddDelay + 250); - while ((DateTime.Now - userData.LastChange).TotalMilliseconds < ReactSettings.RoleAddDelay) await Task.Delay(2000); - - // Strip out any changes we don't need to prevent additional calls - // If changes were made to either add or remove, we make those changes. - if (userData.RolesToAdd.Count > 0) - { - for (var i = userData.RolesToAdd.Count - 1; i >= 0; i--) - if (user.RoleIds.Contains(userData.RolesToAdd[i].Id)) - userData.RolesToAdd.RemoveAt(i); - await user.AddRolesAsync(userData.RolesToAdd); - } - - if (userData.RolesToRemove.Count > 0) - { - for (var i = userData.RolesToRemove.Count - 1; i >= 0; i--) - if (!user.RoleIds.Contains(userData.RolesToRemove[i].Id)) - userData.RolesToRemove.RemoveAt(i); - await user.RemoveRolesAsync(userData.RolesToRemove); - } - - if (ReactSettings.LogUpdates) - await _loggingService.Log(LogBehaviour.Channel, $"[{ServiceName}] {user.Username} Updated Roles.", ExtendedLogSeverity.Info); - - _pendingUserUpdate.Remove(user); - } - - /// - /// If a user is reacting to messages, they're added to a pending list of updates. Any time they react within the - /// timeframe, it resets, and updates what roles need to be set. - /// - private async Task UpdatePendingRolesAsync(IGuildUser user, IRole role, bool state) - { - // We check if the user has pending updates, if they don't we add them - if (!_pendingUserUpdate.ContainsKey(user)) - { - _pendingUserUpdate.Add(user, new ReactRoleUserData()); - await UpdateUserRoles(user); - } - - var userData = _pendingUserUpdate[user]; - userData.LastChange = DateTime.Now; - // Add our change, make sure it isn't in our RemoveList - if (state) - { - userData.RolesToAdd.Add(role); - userData.RolesToRemove.Remove(role); - } - else - { - userData.RolesToAdd.Remove(role); - userData.RolesToRemove.Add(role); - } - } - - private bool IsUserValid(IUser user) => !user.IsBot; - - private class ReactRoleUserData - { - public readonly List RolesToAdd = new List(); - public readonly List RolesToRemove = new List(); - public DateTime LastChange = DateTime.Now; - } - - #region ModuleCommands - - public bool SetReactRoleDelay(uint delay) - { - if (ReactSettings == null) return false; - ReactSettings.RoleAddDelay = delay; - SaveSettings(); - return true; - } - - public bool SetReactLogState(bool state) - { - ReactSettings.LogUpdates = state; - SaveSettings(); - return true; - } - - /// Saves the prepared message if one is being prepared. - public bool StoreNewMessage() - { - if (!IsPreparingMessage) - return false; - - // Make sure it isn't null (New config) - ReactSettings.UserReactRoleList ??= new List(); - - ReactSettings.UserReactRoleList.Add(NewMessage); - SaveSettings(); - - NewMessage = null; - - return true; - } - - /// Restarts the Service by clearing all containers and restoring them from the configuration file. - public async Task Restart() - { - _isRunning = false; - _reactMessages.Clear(); - _guildRoles.Clear(); - _guildEmotes.Clear(); - //TODO This may have users in it, we could push changes before the restart? - _pendingUserUpdate.Clear(); - - await StartService(); - return _isRunning; - } - - #endregion -} \ No newline at end of file diff --git a/DiscordBot/Services/ReminderService.cs b/DiscordBot/Services/ReminderService.cs index ad34d2a7..6c25ad32 100644 --- a/DiscordBot/Services/ReminderService.cs +++ b/DiscordBot/Services/ReminderService.cs @@ -4,7 +4,8 @@ namespace DiscordBot.Services; [Serializable] -public class ReminderItem { +public class ReminderItem +{ public ulong ChannelId { get; set; } public ulong MessageId { get; set; } public ulong UserId { get; set; } @@ -15,19 +16,20 @@ public class ReminderItem { public class ReminderService { private const string ServiceName = "ReminderService"; - + // Bot responds to reminder request, any users who also use this emoji on the message will be pinged when the reminder is triggered. public static readonly Emoji BotResponseEmoji = new("✅"); - + public bool IsRunning { get; private set; } - + private DateTime _nearestReminder = DateTime.Now; - + private readonly DiscordSocketClient _client; private readonly ILoggingService _loggingService; private List _reminders = new List(); - + private readonly ChannelInfo _botCommandsChannel; + private readonly string _serverRootPath; private bool _hasChangedSinceLastSave = false; private const int _maxUserReminders = 10; @@ -37,6 +39,7 @@ public ReminderService(DiscordSocketClient client, ILoggingService loggingServic _client = client; _loggingService = loggingService; _botCommandsChannel = settings.BotCommandsChannel; + _serverRootPath = settings.ServerRootPath; Initialize(); } @@ -44,7 +47,7 @@ public ReminderService(DiscordSocketClient client, ILoggingService loggingServic private void Initialize() { if (IsRunning) return; - + LoadReminders(); if (_reminders == null) { @@ -58,27 +61,27 @@ private void Initialize() // Serialize Reminders to file public void SaveReminders() { - Utils.SerializeUtil.SerializeFile(@"Settings/reminders.json", _reminders); + Utils.SerializeUtil.SerializeFile($"{_serverRootPath}/reminders.json", _reminders); } private void LoadReminders() { - _reminders = Utils.SerializeUtil.DeserializeFile>(@"Settings/reminders.json"); + _reminders = Utils.SerializeUtil.DeserializeFile>($"{_serverRootPath}/reminders.json"); } public void AddReminder(ReminderItem reminder) { _reminders.Add(reminder); _hasChangedSinceLastSave = true; - + // We check if this reminder is sooner than the next one if (_nearestReminder > reminder.When) _nearestReminder = reminder.When; } - + public bool UserHasTooManyReminders(ulong userId) { return _reminders.FindAll(x => x.UserId == userId).Count >= _maxUserReminders; } - + public List GetUserReminders(ulong userId) { return _reminders.FindAll(x => x.UserId == userId); @@ -103,7 +106,7 @@ public int RemoveReminders(IUser user, int index = 0) _hasChangedSinceLastSave = true; return count; } - + // Check if reminders are due in an async task that loops from the constructor private async Task CheckReminders() { @@ -119,11 +122,11 @@ private async Task CheckReminders() } await Task.Delay(1000); - + var now = DateTime.Now; // We wait until we know at least one reminder needs to be checked if (now <= _nearestReminder || _reminders.Count <= 0) continue; - + List remindersToCheck = _reminders.Where(r => r.When <= now).ToList(); _hasChangedSinceLastSave = true; @@ -157,7 +160,7 @@ private async Task CheckReminders() // If there are any extra users, we add them to the bot response if (extraUsers != string.Empty) botResponse += $"\n({extraUsers} also signed on {BotResponseEmoji})"; - + await message.ReplyAsync(botResponse); continue; } @@ -165,7 +168,7 @@ private async Task CheckReminders() channel ??= _client.GetChannel(_botCommandsChannel.Id) as SocketTextChannel; var user = _client.GetUser(reminder.UserId); if (user == null) continue; - + if (channel != null) await channel.SendMessageAsync( $"{user.Mention} reminder: \"{reminder.Message}\""); @@ -183,7 +186,7 @@ await channel.SendMessageAsync( IsRunning = false; } } - + public bool RestartService() { Initialize(); diff --git a/DiscordBot/Services/UpdateService.cs b/DiscordBot/Services/UpdateService.cs index 62be1ea4..d2d802a4 100644 --- a/DiscordBot/Services/UpdateService.cs +++ b/DiscordBot/Services/UpdateService.cs @@ -122,7 +122,7 @@ await Task.Run(async () => } }, _token); - _faqData = SerializeUtil.DeserializeFile>($"{_settings.ServerRootPath}/FAQs.json"); + _faqData = SerializeUtil.DeserializeFile>("Settings/FAQs.json"); _feedData = SerializeUtil.DeserializeFile($"{_settings.ServerRootPath}/feeds.json"); } @@ -186,9 +186,9 @@ private async Task DownloadDocDatabase() _apiDatabase = ConvertJsToArray(apiInput, false); if (!SerializeUtil.SerializeFile($"{_settings.ServerRootPath}/unitymanual.json", _manualDatabase)) - await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to save unitymanual.json", ExtendedLogSeverity.Warning); + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile, $"{ServiceName}: Failed to save unitymanual.json", ExtendedLogSeverity.Warning); if (!SerializeUtil.SerializeFile($"{_settings.ServerRootPath}/unityapi.json", _apiDatabase)) - await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to save unityapi.json", ExtendedLogSeverity.Warning); + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile, $"{ServiceName}: Failed to save unityapi.json", ExtendedLogSeverity.Warning); string[][] ConvertJsToArray(string data, bool isManual) { @@ -217,7 +217,7 @@ string[][] ConvertJsToArray(string data, bool isManual) } catch (Exception e) { - await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to download manual/api file\nEx:{e.ToString()}", ExtendedLogSeverity.Warning); + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile, $"{ServiceName}: Failed to download manual/api file\nEx:{e.ToString()}", ExtendedLogSeverity.Warning); } } @@ -260,7 +260,7 @@ private async Task UpdateRssFeeds() } catch (Exception e) { - await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile,$"{ServiceName}: Failed to update RSS feeds, attempting to continue.", ExtendedLogSeverity.Error); + await _loggingService.Log(LogBehaviour.ConsoleChannelAndFile, $"{ServiceName}: Failed to update RSS feeds, attempting to continue.", ExtendedLogSeverity.Error); } await Task.Delay(TimeSpan.FromSeconds(30d), _token); diff --git a/DiscordBot/Services/UserService.cs b/DiscordBot/Services/UserService.cs index 6943923e..f865581a 100644 --- a/DiscordBot/Services/UserService.cs +++ b/DiscordBot/Services/UserService.cs @@ -16,7 +16,7 @@ namespace DiscordBot.Services; public class UserService { private const string ServiceName = "UserService"; - + private readonly HashSet _canEditThanks; //Doesn't need to be saved private readonly DiscordSocketClient _client; public readonly string CodeFormattingExample; @@ -48,7 +48,7 @@ public class UserService private readonly TimeSpan _mikuCooldownTime; private readonly string _mikuRegex; private readonly string _mikuReply; - + private readonly UpdateService _updateService; private readonly Dictionary _xpCooldown; @@ -163,7 +163,6 @@ Event subscriptions //_client.MessageReceived += MikuCheck; _client.MessageReceived += CodeCheck; _client.MessageReceived += ScoldForAtEveryoneUsage; - _client.MessageReceived += AutoCreateThread; _client.UserJoined += UserJoined; _client.GuildMemberUpdated += UserUpdated; _client.UserLeft += UserLeft; @@ -173,7 +172,7 @@ Event subscriptions LoadData(); UpdateLoop(); - + Task.Run(DelayedWelcomeService); } @@ -239,7 +238,7 @@ public async Task UpdateXp(SocketMessage messageParam) var userId = messageParam.Author.Id; if (_xpCooldown.HasUser(userId)) return; - + var waitTime = _rand.Next(_xpMinCooldown, _xpMaxCooldown); float baseXp = _rand.Next(_xpMinPerMessage, _xpMaxPerMessage); float bonusXp = 0; @@ -251,7 +250,7 @@ public async Task UpdateXp(SocketMessage messageParam) var user = await _databaseService.GetOrAddUser((SocketGuildUser)messageParam.Author); if (user == null) return; - + bonusXp += baseXp * (1f + user.Karma / 100f); //Reduce XP for members with no role @@ -306,7 +305,7 @@ private async Task LevelUp(SocketMessage messageParam, ulong userId) private double GetXpHigh(uint level) => 70d - 139.5d * (level + 2d) + 69.5 * Math.Pow(level + 2d, 2d); private SkinData GetSkinData() => - JsonConvert.DeserializeObject(File.ReadAllText($"{_settings.ServerRootPath}/skins/skin.json"), + JsonConvert.DeserializeObject(File.ReadAllText($"{_settings.AssetsRootPath}/skins/skin.json"), new SkinModuleJsonConverter()); /// @@ -323,7 +322,7 @@ public async Task GenerateProfileCard(IUser user) var dbRepo = _databaseService.Query; if (dbRepo == null) return profileCardPath; - + var userData = await dbRepo.GetUser(user.Id.ToString()); var xpTotal = userData.Exp; @@ -371,11 +370,11 @@ public async Task GenerateProfileCard(IUser user) XpTotal = (uint)xpTotal }; - var background = new MagickImage($"{_settings.ServerRootPath}/skins/{skin.Background}"); + var background = new MagickImage($"{_settings.AssetsRootPath}/skins/{skin.Background}"); var avatarUrl = user.GetAvatarUrl(ImageFormat.Auto, 256); if (string.IsNullOrEmpty(avatarUrl)) - profile.Picture = new MagickImage($"{_settings.ServerRootPath}/images/default.png"); + profile.Picture = new MagickImage($"{_settings.AssetsRootPath}/images/default.png"); else try { @@ -393,7 +392,7 @@ public async Task GenerateProfileCard(IUser user) LoggingService.LogToConsole( $"Failed to download user profile image for ProfileCard.\nEx:{e.Message}", LogSeverity.Warning); - profile.Picture = new MagickImage($"{_settings.ServerRootPath}/images/default.png"); + profile.Picture = new MagickImage($"{_settings.AssetsRootPath}/images/default.png"); } profile.Picture.Resize(skin.AvatarSize, skin.AvatarSize); @@ -405,7 +404,7 @@ public async Task GenerateProfileCard(IUser user) { var image = layer.Image.ToLower() == "avatar" ? profile.Picture - : new MagickImage($"{_settings.ServerRootPath}/skins/{layer.Image}"); + : new MagickImage($"{_settings.AssetsRootPath}/skins/{layer.Image}"); background.Composite(image, (int)layer.StartX, (int)layer.StartY, CompositeOperator.Over); } @@ -425,7 +424,7 @@ public async Task GenerateProfileCard(IUser user) { await _loggingService.LogChannelAndFile($"Failed to generate profile card for {user.Username}.\nEx:{e.Message}", ExtendedLogSeverity.LowWarning); } - + if (!string.IsNullOrEmpty(profileCardPath)) await Task.Delay(100); @@ -436,7 +435,7 @@ public Embed WelcomeMessage(SocketGuildUser user) { string icon = user.GetAvatarUrl(); icon = string.IsNullOrEmpty(icon) ? "https://cdn.discordapp.com/embed/avatars/0.png" : icon; - + string welcomeString = $"Welcome to Unity Developer Community, {user.GetPreferredAndUsername()}!"; var builder = new EmbedBuilder() .WithDescription(welcomeString) @@ -457,7 +456,7 @@ public async Task ThanksEdited(Cacheable cachedMessage, SocketM { if (_canEditThanks.Contains(messageParam.Id)) await Thanks(messageParam); } - + public async Task Thanks(SocketMessage messageParam) { //Get guild id @@ -558,7 +557,7 @@ public async Task CodeCheck(SocketMessage messageParam) // We just ignore anything if it is under 200 characters if (messageParam.Content.Length < 200) return; - + var userId = messageParam.Author.Id; //Simple check to cover most large code posting cases without being an issue for most non-code messages @@ -654,16 +653,16 @@ private async Task UserIsTyping(Cacheable user, Cacheable u.id == user.Id)) { _welcomeNoticeUsers.RemoveAll(u => u.id == user.Id); @@ -723,10 +722,10 @@ private async Task DelayedWelcomeService() { currentlyProcessedUserId = userData.id; await ProcessWelcomeUser(userData.id, null); - + toRemove.Add(userData.id); } - + // Remove all the users we've welcomed from the list if (toRemove.Count > 0) { @@ -748,7 +747,7 @@ private async Task DelayedWelcomeService() { // Catch and show exception await _loggingService.LogChannelAndFile($"{ServiceName} Exception during welcome message `{currentlyProcessedUserId}`.\n{e.Message}.", ExtendedLogSeverity.Warning); - + // Remove the offending user from the dictionary and run the service again. _welcomeNoticeUsers.RemoveAll(u => u.id == currentlyProcessedUserId); if (_welcomeNoticeUsers.Count > 200) @@ -836,45 +835,5 @@ await _loggingService.LogChannelAndFile( } } - private async Task AutoCreateThread(SocketMessage messageParam) - { - if (messageParam.Author.IsBot) return; - - foreach (var prefix in _settings.AutoThreadExclusionPrefixes) - if (messageParam.Content.StartsWith(prefix)) - return; - - foreach (var AutoThreadChannel in _settings.AutoThreadChannels) - { - var channel = messageParam.Channel as SocketTextChannel; - if (channel.Id.Equals(AutoThreadChannel.Id)) - { - try - { - ThreadArchiveDuration wantedDuration; - if (!Enum.TryParse(AutoThreadChannel.Duration, out wantedDuration)) - wantedDuration = ThreadArchiveDuration.ThreeDays; - Discord.ThreadArchiveDuration duration = - Utils.Utils.GetMaxThreadDuration(wantedDuration, _client.GetGuild(_settings.GuildId)); - var title = AutoThreadChannel.GenerateTitle(messageParam.Author); - var thread = await channel.CreateThreadAsync(title, Discord.ThreadType.PublicThread, duration, - messageParam); - - if (!String.IsNullOrEmpty(AutoThreadChannel.FirstMessage)) - { - var message = - await thread.SendMessageAsync(AutoThreadChannel.GenerateFirstMessage(messageParam.Author)); - await message.PinAsync(); - } - } - catch (Exception err) - { - LoggingService.LogToConsole($"Failed to CreateThread.\nEx: {err.ToString()}", LogSeverity.Error); - } - } - } - - } - #endregion } diff --git a/DiscordBot/Settings/Deserialized/ReactionRole.cs b/DiscordBot/Settings/Deserialized/ReactionRole.cs deleted file mode 100644 index 75014b25..00000000 --- a/DiscordBot/Settings/Deserialized/ReactionRole.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace DiscordBot.Settings; - -public class ReactRoleSettings -{ - public bool LogUpdates = false; - public uint RoleAddDelay = 5000; // Delay in ms - public List UserReactRoleList; -} - -public class UserReactMessage -{ - public ulong ChannelId; - public ulong MessageId; - public List Reactions; - public string Description { get; set; } - - public int RoleCount() => Reactions?.Count ?? 0; -} - -public class ReactRole -{ - public string Name; - - public ReactRole(string name, ulong roleId, ulong emojiId) - { - Name = name; - RoleId = roleId; - EmojiId = emojiId; - } - - public ulong RoleId { get; set; } - public ulong EmojiId { get; set; } -} \ No newline at end of file diff --git a/DiscordBot/Settings/Deserialized/Settings.cs b/DiscordBot/Settings/Deserialized/Settings.cs index 19dbff60..c751ccff 100644 --- a/DiscordBot/Settings/Deserialized/Settings.cs +++ b/DiscordBot/Settings/Deserialized/Settings.cs @@ -6,15 +6,16 @@ public class BotSettings public string Token { get; set; } public string Invite { get; set; } - + public string DbConnectionString { get; set; } public string ServerRootPath { get; set; } + public string AssetsRootPath { get; set; } = "./Assets"; public char Prefix { get; set; } public ulong GuildId { get; set; } public bool LogCommandExecutions { get; set; } = true; #endregion // Important - + #region Configuration public int WelcomeMessageDelaySeconds { get; set; } = 300; @@ -26,70 +27,55 @@ public class BotSettings #region Fun Commands public string UserModuleSlapObjectsTable { get; set; } = null; - // = "udc-slap.txt" //NOTE: Deserializer will not override a List from the json if a default one is made here. public List UserModuleSlapChoices { get; set; } - // = { "trout", "duck", "truck", "paddle", "magikarp", "sausage", "student loan", - // "life choice", "bug report", "unhandled exception", "null pointer", "keyboard", - // "cheese wheel", "banana peel", "unresolved bug", "low poly donut" }; + // = { "trout", "duck", "truck", "paddle", "magikarp", "sausage", "student loan", + // "life choice", "bug report", "unhandled exception", "null pointer", "keyboard", + // "cheese wheel", "banana peel", "unresolved bug", "low poly donut" }; public List UserModuleSlapFails { get; set; } - // = { "hurting themselves" }; - + // = { "hurting themselves" }; + #endregion // Fun Commands #region Service Enabling // Used for enabling/disabling services in the bot - + public bool RecruitmentServiceEnabled { get; set; } = false; public bool UnityHelpBabySitterEnabled { get; set; } = false; - public bool ReactRoleServiceEnabled { get; set; } = false; public bool IntroductionWatcherServiceEnabled { get; set; } = false; #endregion // Service Enabling #region Birthday Announcements - + public bool BirthdayAnnouncementEnabled { get; set; } = true; public int BirthdayCheckIntervalMinutes { get; set; } = 240; // Check every 4 hours by default public ChannelInfo BirthdayAnnouncementChannel { get; set; } - + #endregion // Birthday Announcements #endregion // Configuration - #region Asset Publisher - - // Used for Asset Publisher - - public string Email { get; set; } - public string EmailUsername { get; set; } - public string EmailPassword { get; set; } - public string EmailSMTPServer { get; set; } - public int EmailSMTPPort { get; set; } - - #endregion // Asset Publisher - #region Channels - + public ChannelInfo IntroductionChannel { get; set; } public ChannelInfo GeneralChannel { get; set; } public ChannelInfo GenericHelpChannel { get; set; } - + public ChannelInfo BotAnnouncementChannel { get; set; } - public ChannelInfo AnnouncementsChannel { get; set; } public ChannelInfo BotCommandsChannel { get; set; } public ChannelInfo UnityNewsChannel { get; set; } public ChannelInfo UnityReleasesChannel { get; set; } public ChannelInfo RulesChannel { get; set; } // Recruitment Channels - + public ChannelInfo RecruitmentChannel { get; set; } public ChannelInfo ReportedMessageChannel { get; set; } - + public ChannelInfo MemeChannel { get; set; } - + #region Complaint Channel public ulong ComplaintCategoryId { get; set; } @@ -99,13 +85,6 @@ public class BotSettings #endregion // Complaint Channel - #region Auto-Threads - - public List AutoThreadChannels { get; set; } = new List(); - public List AutoThreadExclusionPrefixes { get; set; } = new List(); - - #endregion // Auto-Threads - #endregion // Channels #region User Roles @@ -114,34 +93,32 @@ public class BotSettings public ulong MutedRoleId { get; set; } public ulong SubsReleasesRoleId { get; set; } public ulong SubsNewsRoleId { get; set; } - public ulong PublisherRoleId { get; set; } public ulong ModeratorRoleId { get; set; } public ulong TipsUserRoleId { get; set; } // e.g., Helpers - public ulong TipsAuthorRoleId { get; set; } // e.g., Moderators #endregion // User Roles #region Recruitment Thread - + public string TagLookingToHire { get; set; } public string TagLookingForWork { get; set; } public string TagUnpaidCollab { get; set; } public string TagPositionFilled { get; set; } - + public int EditPermissionAccessTimeMin { get; set; } = 3; #endregion // Recruitment Thread Tags #region Unity Help Threads - + #region Tips - + public string TipImageDirectory { get; set; } public int TipMaxImageFileSize { get; set; } = 1024 * 1024 * 10; // 10MB // Unlikely, but we prevent exploitation by limiting the max directory size to avoid VPS disk space issues public int TipMaxDirectoryFileSize { get; set; } = 1024 * 1024 * 1024; // 1GB - + #endregion // Tips public string TagUnitHelpResolvedTag { get; set; } @@ -154,7 +131,7 @@ public class BotSettings public string FlightAPIKey { get; set; } public string FlightAPISecret { get; set; } - public string FlightAPIId { get; set; } + public string AirLabAPIKey { get; set; } #endregion // API Keys @@ -174,11 +151,10 @@ public class BotSettings #region Other - public string AssetStoreFrontPage { get; set; } public string WikipediaSearchPage { get; set; } #endregion // Other - + } #region Role Group Collections @@ -201,35 +177,4 @@ public class ChannelInfo public ulong Id { get; set; } } -public class AutoThreadChannel -{ - public string Title { get; set; } - public ulong Id { get; set; } - public bool CanArchive { get; set; } = false; - public bool CanDelete { get; set; } = false; - public string TitleArchived { get; set; } - public string FirstMessage { get; set; } - public string Duration { get; set; } - - private static string AuthorName(IUser author) - { - return ((IGuildUser)author).Nickname ?? author.Username; - } - - public string GenerateTitle(IUser author) - { - return String.Format(this.Title, AuthorName(author)); - } - - public string GenerateTitleArchived(IUser author) - { - return String.Format(this.TitleArchived, AuthorName(author)); - } - - public string GenerateFirstMessage(IUser author) - { - return String.Format(this.FirstMessage, author.Mention); - } -} - #endregion diff --git a/DiscordBot/SERVER/FAQs.json b/DiscordBot/Settings/FAQs.json similarity index 100% rename from DiscordBot/SERVER/FAQs.json rename to DiscordBot/Settings/FAQs.json diff --git a/DiscordBot/Settings/ReactionRoles.json b/DiscordBot/Settings/ReactionRoles.json deleted file mode 100644 index 007678df..00000000 --- a/DiscordBot/Settings/ReactionRoles.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "RoleAddDelay": 5000, - "LogUpdates": false, - "UserReactRoleList": null -} \ No newline at end of file diff --git a/DiscordBot/Settings/Settings.example.json b/DiscordBot/Settings/Settings.example.json index 098c3fd7..44960198 100644 --- a/DiscordBot/Settings/Settings.example.json +++ b/DiscordBot/Settings/Settings.example.json @@ -3,18 +3,14 @@ /* Auth info */ "token": "Y O U R _ B O T _ T O K E N", "invite": "InviteLink", // Currently Unused - /* Gmail information for Asset Publisher Role to work. */ - "gmail": "", - "gmailUsername": "YourGmailUsername", - "gmailPassword": "YourGmailPassword", /* 'SSL MODE' and 'Allow User Variables' are only required when running on a local machine with XAMPP. This can often be removed. */ /* DB Info*/ "DbConnectionString": "server=localhost;port=3306;database=test;user id=USER;Password=USERPASSWORD;SSL Mode=None;Allow User Variables=True", /*Server Info*/ "serverRootPath": "./SERVER", + "assetsRootPath": "./Assets", /* Base info */ "prefix": "!", - "Administrator": "0", "ModeratorRoleId": "0", "guildId": "0", // Replace with your servers guild ID /* All assignable roles as of 29/04/21 */ @@ -48,10 +44,6 @@ "desc": "Bot-Announcement Channel", "id": "0" }, - "announcementsChannel": { // Not used by bot - "desc": "General Announcement Channel", - "id": "0" // Currently Unused 29/04/21 - }, "botCommandsChannel": { "desc": "Bot-Commands Channel", "id": "0" @@ -66,11 +58,8 @@ }, /* Role Ids */ "mutedRoleID": "0", - "publisherRoleID": "0", "SubsNewsRoleId": "0", "SubsReleasesRoleId": "0", - /*Publisher Stuff*/ - "assetStoreFrontPage": "https://www.assetstore.unity3d.com/en/", /*Complaints Channels Stuff*/ "complaintCategoryId": "0", "complaintChannelPrefix": "Complaint", @@ -102,8 +91,6 @@ "desc": "Unity-Help Channel", "id": "0" }, - /* React Role Service */ - "ReactRoleServiceEnabled": false, /* Birthday Announcement Service */ "BirthdayAnnouncementEnabled": true, "BirthdayCheckIntervalMinutes": 240, // Check every 4 hours diff --git a/DiscordBot/Utils/Utils.cs b/DiscordBot/Utils/Utils.cs index c5e797ae..ad73526c 100644 --- a/DiscordBot/Utils/Utils.cs +++ b/DiscordBot/Utils/Utils.cs @@ -76,16 +76,6 @@ public static bool IsLegalXmlChar(int character) => character >= 0xE000 && character <= 0xFFFD || character >= 0x10000 && character <= 0x10FFFF; - public static ThreadArchiveDuration GetMaxThreadDuration(ThreadArchiveDuration wantedDuration, IGuild guild) - { - var maxDuration = ThreadArchiveDuration.OneDay; - if (guild.PremiumTier >= PremiumTier.Tier2) maxDuration = ThreadArchiveDuration.OneWeek; - else if (guild.PremiumTier >= PremiumTier.Tier1) maxDuration = ThreadArchiveDuration.ThreeDays; - - if (wantedDuration > maxDuration) return maxDuration; - return wantedDuration; - } - // Returns a datetime from a string using common date terms, ie; '1 year 40 days', '30 minutes 10 seconds', '10m 1d 400s', '1d 10h' public static DateTime ParseTimeFromString(string time) { @@ -149,7 +139,7 @@ public static string RemoveHtmlTags(string contents) var regex = new Regex("<.*?>"); return regex.Replace(contents, string.Empty); } - + public static string MessageLinkBack(ulong guildId, ulong channelId, ulong messageId) { return $"https://discordapp.com/channels/{guildId}/{channelId}/{messageId}"; diff --git a/Dockerfile b/Dockerfile index a5266cf6..80361dbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,34 @@ -# Builds application using dotnet's sdk +# Build stage FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR / -COPY ./DiscordBot/ ./app/ WORKDIR /app/ - +COPY ./NuGet.config ./ +COPY ./DiscordBot/DiscordBot.csproj ./ RUN dotnet restore +COPY ./DiscordBot/ ./ RUN dotnet publish --configuration Release --no-restore --output /app/publish - -# Build finale image +# Runtime stage FROM mcr.microsoft.com/dotnet/runtime:8.0 WORKDIR /app/ COPY --from=build /app/publish/ ./ -RUN echo "deb http://deb.debian.org/debian bullseye main contrib" > /etc/apt/sources.list -RUN echo "deb http://security.debian.org/ bullseye-security main contrib" >> /etc/apt/sources.list -RUN echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | debconf-set-selections -RUN apt update -RUN apt install -y ttf-mscorefonts-installer -RUN apt clean -RUN apt autoremove -y -RUN rm -rf /var/lib/apt/lists/ +# Bake immutable static assets (fonts, images, skins) into the image +COPY ./DiscordBot/Assets/fonts/ ./Assets/fonts/ +COPY ./DiscordBot/Assets/images/ ./Assets/images/ +COPY ./DiscordBot/Assets/skins/ ./Assets/skins/ + +# Add contrib repo for MS fonts, matching the base image's Debian codename +RUN . /etc/os-release && \ + echo "deb https://deb.debian.org/debian ${VERSION_CODENAME} contrib" > /etc/apt/sources.list.d/contrib.list && \ + echo "deb https://security.debian.org/debian-security ${VERSION_CODENAME}-security contrib" >> /etc/apt/sources.list.d/contrib.list && \ + echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | debconf-set-selections && \ + apt-get update && \ + apt-get install -y --no-install-recommends ttf-mscorefonts-installer && \ + apt-get autoremove -y && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* ENTRYPOINT ["./DiscordBot"] diff --git a/docker-compose.yml b/docker-compose.yml index e4c264e2..0c4aba68 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: volumes: - .\DiscordBot\Settings\:/app/Settings - .\DiscordBot\SERVER\:/app/SERVER + - .\DiscordBot\Assets\:/app/Assets:ro depends_on: - db restart: always diff --git a/docs/plans/done/directory-reorganization.md b/docs/plans/done/directory-reorganization.md new file mode 100644 index 00000000..4a28041a --- /dev/null +++ b/docs/plans/done/directory-reorganization.md @@ -0,0 +1,54 @@ +# Directory Reorganization Plan + +**Status: COMPLETED** + +## Goal + +Clean separation of directories by purpose: +- **Settings/** — Human-authored config, read-only by bot +- **SERVER/** — Bot-generated runtime data, read/write +- **Assets/** — Static visual assets (fonts, images, skins), human-authored, read-only + +## File Moves + +| File | From | To | Code Change | +|------|------|----|-------------| +| FAQs.json | `{ServerRootPath}/FAQs.json` | `Settings/FAQs.json` | UpdateService.cs L125 | +| reminders.json | `Settings/reminders.json` | `{ServerRootPath}/reminders.json` | ReminderService.cs L61, L65 | +| skins/skin.json | `{ServerRootPath}/skins/` | `{AssetsRootPath}/skins/` | UserService.cs L308, L373, L407 | +| images/default.png | `{ServerRootPath}/images/` | `{AssetsRootPath}/images/` | UserService.cs L377, L395 | +| fonts/ | SERVER/fonts/ | Assets/fonts/ | No code change (font names only) | + +## New Setting + +Add `AssetsRootPath` to `BotSettings` with default `"./Assets"`. + +## Unchanged Paths (remain in SERVER/) + +- `botdata.json`, `userdata.json`, `feeds.json` (bot-generated) +- `log.txt`, `logXP.txt`, `log_backups/` (bot-generated) +- `images/profiles/` (bot-generated profile cards) +- `unitymanual.json`, `unityapi.json` (downloaded cache) +- `{TipImageDirectory}/` (bot-generated tips) + +## Docker Changes + +- Assets/ lives inside DiscordBot/ for native + Docker compatibility +- Dockerfile: `COPY ./DiscordBot/Assets/` for baking into image +- docker-compose: `.\DiscordBot\Assets\:/app/Assets:ro` volume mount (read-only) + +## Subtasks + +- [x] Add `AssetsRootPath` to Settings.cs + Settings.example.json + Settings.json +- [x] Update UpdateService.cs: FAQs.json → `Settings/FAQs.json` (hardcoded) +- [x] Update ReminderService.cs: → `{ServerRootPath}/reminders.json` +- [x] Update UserService.cs: skins + default.png → AssetsRootPath +- [x] Move assets into DiscordBot/Assets/ (via DiscordBot/SERVER/ → Assets/) +- [x] Update Dockerfile COPY paths +- [x] Update docker-compose volume mounts (with `:ro`) +- [x] Fix AssetsRootPath/ServerRootPath null in Settings.json +- [x] Update .gitignore for SERVER/ runtime data +- [x] Remove stale static assets from DiscordBot/SERVER/ +- [x] Remove empty profiles/subtitles dirs from Assets/images/ +- [x] Build and test +- [x] Peer review diff --git a/k8s/dev/bot-config.yaml b/k8s/dev/bot-config.yaml new file mode 100644 index 00000000..db1d6292 --- /dev/null +++ b/k8s/dev/bot-config.yaml @@ -0,0 +1,130 @@ +# Settings.json template for UDC-Bot dev environment. +# Secrets are substituted at pod startup by the render-config init container via envsubst. +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-config + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: dev +data: + Settings.json: | + { + "token": "${BOT_TOKEN}", + "DbConnectionString": "server=mysql;port=3306;database=udcbot;user id=udcbot;Password=${DB_PASSWORD};SSL Mode=None;Allow User Variables=True;AllowPublicKeyRetrieval=True", + "invite": "InviteLink", // Currently Unused + /*Server Info*/ + "serverRootPath": "./SERVER", + "assetsRootPath": "./Assets", + /* Base info */ + "prefix": "!", + "Administrator": "838030241103478805", + "ModeratorRoleId": "769010537119088690", + "guildId": "566084539664039938", // Replace with your servers guild ID + /* All assignable roles as of 29/04/21 */ + "UserAssignableRoles": { + "desc": "All normal user assignable roles available", + "roles": [ + "Audio-Engineers", + "Technical-Artists", + "Animators", + "3D-Artists", + "2D-Artists", + "XR-Developers", + "Programmers", + "Writers", + "Game-Designers", + "Generalists", + "Hobbyists", + "Students" + ] + }, + /* Channel IDs for certain channels. */ + "generalChannel": { // Off-topic + "desc": "General-Chat Channel", + "id": "566084539664039944" + }, + "botAnnouncementChannel": { // Most bot logs will go here + "desc": "Bot-Announcement Channel", + "id": "567628191221547008" + }, + "announcementsChannel": { // Not used by bot + "desc": "General Announcement Channel", + "id": "838030934728376320" // Currently Unused 29/04/21 + }, + "botCommandsChannel": { + "desc": "Bot-Commands Channel", + "id": "599583999379243008" + }, + "unityNewsChannel": { + "desc": "Unity News Channel", + "id": "1022102744552710154" + }, + "UnityReleasesChannel": { + "desc": "Unity Releases Channel", + "id": "1022102744552710154" + }, + "RulesChannel": { + "desc": "The Rules", + "id": "825932695698669618" + }, + "ReportedMessageChannel": { + "desc": "Reported Message Channel", + "id": "567628191221547008" + }, + /* Role Ids */ + "mutedRoleID": "682432235445682194", + "SubsReleasesRoleId": "769870886743703584", + "TipsUserRoleId": "603187742096228374", + "TipsAuthorRoleId": "603187742096228374", + /*Complaints Channels Stuff*/ + "complaintCategoryId": "874631331810799626", + "complaintChannelPrefix": "Complaint", + /*Commands Configuration*/ + "wikipediaSearchPage": "https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts|info&generator=prefixsearch&redirects=1&converttitles=1&utf8=1&formatversion=2&exchars=750&exintro=1&explaintext=1&exsectionformat=plain&inprop=url&gpslimit=5&gpsprofile=fuzzy&gpssearch=", + "IPGeolocationAPIKey": "${IPGEO_KEY}", + "WeatherAPIKey": "${WEATHER_KEY}", + "FlightAPIKey": "${FLIGHT_KEY}", + "FlightAPISecret": "${FLIGHT_SECRET}", + "AirLabAPIKey": "${AIRLAB_KEY}", + /* Job Recruitment Service */ + "RecruitmentChannel": { + "desc": "Channel for job postings", + "id": "1134672948356202678" + }, + "RecruitmentServiceEnabled": true, + "TagLookingToHire": "1134673961779724480", + "TagLookingForWork": "1134673990993051658", + "TagUnpaidCollab": "1134674009292804117", + "TagPositionFilled": "1134674041450545283", + /* Unity Help Service */ + "UnityHelpBabySitterEnabled": true, + "genericHelpChannel": { // Unity-help + "desc": "Unity-Help Channel", + "id": "1028254982748778516" + }, + "TagUnitHelpResolvedTag": "1028255134356086784", + "IntroductionChannel": { + "desc": "Introduction Channel", + "id": "1198575542467838044" + }, + "IntroductionWatcherServiceEnabled": true, + "UserModuleSlapObjectsTable": "Settings/udc-slap.txt", + "UserModuleSlapChoices": [ + "developer manual", + "devonlepment server" + ], + "UserModuleSlapFails": [ + "developing a rash", + "developing a skin condition" + ], + "TipImageDirectory": "tips", + "BirthdayAnnouncementEnabled": true, + "BirthdayCheckIntervalMinutes": 1, + "BirthdayAnnouncementChannel": { + "desc": "Channel for birthday announcements", + "id": "566084539664039944" + } + } diff --git a/k8s/dev/bot-settings-config.yaml b/k8s/dev/bot-settings-config.yaml new file mode 100644 index 00000000..b8845016 --- /dev/null +++ b/k8s/dev/bot-settings-config.yaml @@ -0,0 +1,217 @@ +# ConfigMaps for bot Settings files (Rules, FAQs, UserSettings). +# These are rendered into an emptyDir volume by the render-config init container. +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-rules + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: dev +data: + Rules.json: | + { + "channel": [ + { + "id": 0, + "header": "", + "content": "**Global rules:**\n**General Rules**\n·Keep it friendly and civil.\n·Do not post anything illegal or illegal links (cracked software links etc.)\n·Do not look down on others just because they're less experienced than you, try to help them instead.\n·Use the appropriate channels\n·If you ask for help, try to give it back. Don't be selfish.\n·Do not link to other discords without a mod permission\n**Help Channels Rules**:\n· Make your question as descriptive as possible,\nadding all the necessary informations.\n· Do not ask if someone can help. Post your question directly.\n· Do not beg for help.\n· Do not repost your question unless it is buried under dozens of other messages.\n· Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 257542774843375616, + "header": "Ask general Unity questions in this channel. Newbie questions welcome.\n", + "content": "· Make your question as descriptive as possible,\nadding all the necessary informations.\n· Do not ask if someone can help. Post your question directly.\n· Do not beg for help.\n· Do not repost your question unless it is buried under dozens of other messages.\n· Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 234257627930951680, + "header": "Use this channel to discuss game scenario, characters, etc", + "content": "" + }, + { + "id": 206846414519664640, + "header": "Use this channel to discuss the process of creation of your game and general game design related discussions.", + "content": "\n·Do not ask for help about scripting here\n·If you want to show off your game, use #work-in-progress instead" + }, + { + "id": 206846919421460481, + "header": "This channel is for advanced C# questions and scripting related discussions (script optimization, naming convention...).", + "content": "\n· Make your question as descriptive as possible, adding all the necessary informations.\n· Do not ask if someone can help. Post your question directly.\n· Do not beg for help.\n· Do not repost your question unless it is buried under dozens of other messages.\n· Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 207821341842210816, + "header": "Use this channel to ask questions about networking (uNet or other libraries) and discuss networking in general.", + "content": "\n· Make your question as descriptive as possible, adding all the necessary informations.\n· Do not ask if someone can help. Post your question directly.\n· Do not beg for help.\n· Do not repost your question unless it is buried under dozens of other messages.\n· Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 209176044773572610, + "header": "Use this channel to show off your art and UI, ask art-related questions and discuss the different softwares and workflows.", + "content": "" + }, + { + "id": 213312462399864833, + "header": "Use this channel to show off your audio work, ask audio-related questions and discuss the different softwares and workflows.", + "content": "" + }, + { + "id": 236150813037297664, + "header": "Use this channel to show off your shaders, ask shader-related questions and discuss the different tools.", + "content": "" + }, + { + "id": 206846647315988481, + "header": "Use this channel for Virtual Reality, Augmented Reality and Mixed Reality discussions and questions.", + "content": "" + }, + { + "id": 206829511034142720, + "header": "Use this channel to share your game development through screenshots, videos and devlogs.", + "content": "\n·Please don't spam your game and let other show off their game too !" + }, + { + "id": 234305401628131329, + "header": "Use this channel to post resources and tutorials about Unity and its ecosystem.", + "content": "\n·Only post link, no discussion around them in this channel." + }, + { + "id": 207352725728526336, + "header": "Show off your latest asset published on the Unity Asset Store !", + "content": "\n·Only post once per asset, keep discussion at a minimum to not drown out other assets' posts." + }, + { + "id": 206829581041139712, + "header": "Use this channel if you're looking for a job. Please read the pins for a recommended template.", + "content": "\n·Only for **paid** jobs. Use #collaboration if you want to work for free\n·Describe your skills as best as possible\n·Portfolio recommended, don't use multiple image links as they'll generate several thumbnails, use an album instead.\n·Wait at least 7 days before reposting, and do not repost if there's less than 10 posts in between.\n·Your offer must be in a single post. Edit it if you want to add aditionnal infos.\n·Discussions about the post must be made in DM or another channel.\n·You will be temporary locked out of posting in this channel for the first offense, and definitely after further offenses." + }, + { + "id": 331674608598253568, + "header": "Use this channel if you're looking to hire someone. Please read the pins for a recommended template.", + "content": "\n·Only for **paid** jobs\n·Describe the skills needed as best as possible\n·Give as much info as possible for better chance of finding someone, don't use multiple image links as they'll generate several thumbnails, use an album instead.\n·Wait at least 7 days before reposting, and do not repost if there's less than 10 posts in between.\n·Your offer must be in a single post. Edit it if you want to add aditionnal infos.\n·Discussions about the post must be made in DM or another channel.\n·You will be temporary locked out of posting in this channel for the first offense, and definitely after further offenses." + }, + { + "id": 264335890791399425, + "header": "Use this channel if you want to collaborate for free. Please read the pins for a recommended template.", + "content": "\n·If you want to be hired : describe your skills as best as possible, portfolio recommended\n·If you want to hire : Describe the skills needed as best as possible, and your skills (and your team if you already have one)\n·Give as much infos as possible on the project on hand\n·Wait at least 7 days before reposting, and do not repost if there's less than 10 posts in between.\n·Your offer must be in a single post. Edit it if you want to add aditionnal infos.\n·Discussions about the post must be made in DM or another channel.\n·You will be temporary locked out of posting in this channel for the first offense, and definitely after further offenses." + }, + { + "id": 300307451146797067, + "header": "Post here about your finished projects along with a link to download or purchase it.", + "content": "\n·Do not repost your project multiple times\n·Please keep the discussion at a minimum to not drown out other projects" + }, + { + "id": 245250659832692736, + "header": "Use this channel to discuss all Not Safe For Work related development.", + "content": "\n·Do not post memes here\n·This is a serious development channel, it's not here to fill your fap folder." + }, + { + "id": 218397150525128708, + "header": "Use this channel to discuss marketing strategy.", + "content": "\n·Do not post your finished game links here, use #finished-projects" + }, + { + "id": 300660661464334336, + "header": "Use this channel to talk about everything anime and japan related (mangas, VN, LN, ~~your favorite hentai~~).", + "content": "\n·Memes recommended\n·Do not post anime streaming sites links" + }, + { + "id": 300641860358242304, + "header": "Post about your favorite musics.", + "content": "\n·Do not spam your music, keep it at 1 or 2 post per day maximum" + }, + { + "id": 245255786618421249, + "header": "Post your dankest memes.", + "content": "\n·NSFW and offensive images are allowed, but do not post porn or anything illegal." + }, + { + "id": 207741022262919170, + "header": "Use this channel to add a role and see all the available commands.", + "content": "\n·Type `!help` to get a list of available commands" + }, + { + "id": 206834669046595584, + "header": "Use this channel to request additional channels", + "content": "" + }, + { + "id": 294352760055529472, + "header": "Use this channel to request additional functions for the bot or to report bugs.", + "content": "" + } + ] + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-faqs + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: dev +data: + FAQs.json: | + [ + { + "question": "What is karma ?", + "answer": "Karma is tracked on your !profile, helping indicate how much you've helped others. You also earn slightly more EXP from things the higher your Karma level is. Karma may be used for more features in the future.", + "keywords": [ "karma", "xp" ] + } + ] +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-user-settings + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: dev +data: + UserSettings.json: | + { + /*Thanks parameters*/ + "thanks": [ + "thanks", + "ty", + "thx", + "thnx", + "thanx", + "thankyou", + "thank you", + "tysm", + "cheers", + "merci", + "gracias", + "danke", + "grazie", + "arigatou", + "有り難う", + "有難う", + "ありがとう", + "どうも", + "고맙습니다", + "谢谢" + ], + "thanksCooldown": 60, //In seconds + "thanksReminderCooldown": 86400, //24 hours in seconds + "thanksMinJoinTime": 600, + + /*Xp parameters*/ + "xpMinPerMessage": 10, + "xpMaxPerMessage": 30, + "xpMinCooldown": 60, + "xpMaxCooldown": 180, + + /*Code parameters*/ + "codeReminderCooldown": 86400, + + "isSomeoneThere": [ + "is anyone around?", + "can someone help?", + "can someone help me?" + ] + } diff --git a/k8s/dev/bot.yaml b/k8s/dev/bot.yaml new file mode 100644 index 00000000..f3d2a97c --- /dev/null +++ b/k8s/dev/bot.yaml @@ -0,0 +1,164 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: bot-server-data + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: udc-bot + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: udc-bot + template: + metadata: + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: dev + spec: + initContainers: + - name: render-config + image: alpine:3.20 + command: + - sh + - -c + - | + set -e + apk add --no-cache gettext >/dev/null 2>&1 + envsubst '${BOT_TOKEN} ${DB_PASSWORD} ${WEATHER_KEY} ${IPGEO_KEY} ${FLIGHT_KEY} ${FLIGHT_SECRET} ${AIRLAB_KEY}' < /config-template/Settings.json > /app-settings/Settings.json + cp /rules-template/Rules.json /app-settings/Rules.json + cp /faqs-template/FAQs.json /app-settings/FAQs.json + cp /usersettings-template/UserSettings.json /app-settings/UserSettings.json + env: + - name: BOT_TOKEN + valueFrom: + secretKeyRef: + name: discord-bot-token + key: identifiant + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-user-credentials + key: password + - name: WEATHER_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: weather-api-key + - name: IPGEO_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: ipgeo-api-key + - name: FLIGHT_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: flight-api-key + - name: FLIGHT_SECRET + valueFrom: + secretKeyRef: + name: bot-api-keys + key: flight-api-secret + - name: AIRLAB_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: airlab-api-key + volumeMounts: + - name: config-template + mountPath: /config-template + readOnly: true + - name: rules-template + mountPath: /rules-template + readOnly: true + - name: faqs-template + mountPath: /faqs-template + readOnly: true + - name: usersettings-template + mountPath: /usersettings-template + readOnly: true + - name: app-settings + mountPath: /app-settings + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - name: wait-for-mysql + image: busybox:1.37 + command: + - sh + - -c + - | + until nc -z mysql 3306; do + echo "Waiting for MySQL..." + sleep 2 + done + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + containers: + - name: bot + image: ghcr.io/unity-developer-community/udc-bot-dev:latest + volumeMounts: + - name: app-settings + mountPath: /app/Settings + - name: server-data + mountPath: /app/SERVER + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 300m + memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + add: + - DAC_OVERRIDE + volumes: + - name: config-template + configMap: + name: bot-config + - name: rules-template + configMap: + name: bot-rules + - name: faqs-template + configMap: + name: bot-faqs + - name: usersettings-template + configMap: + name: bot-user-settings + - name: app-settings + emptyDir: {} + - name: server-data + persistentVolumeClaim: + claimName: bot-server-data diff --git a/k8s/dev/external-secrets.yaml b/k8s/dev/external-secrets.yaml new file mode 100644 index 00000000..97bfd10a --- /dev/null +++ b/k8s/dev/external-secrets.yaml @@ -0,0 +1,115 @@ +--- +# MySQL root password — from 1Password "MySQL Server - Root User - Dev" +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: mysql-credentials + namespace: udc-bot-dev +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: mysql-credentials + data: + - secretKey: password + remoteRef: + key: "MySQL Server - Root User - Dev" + property: password +--- +# MySQL udcbot user password — from 1Password "MySQL Server - UDC User - Dev" +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: mysql-user-credentials + namespace: udc-bot-dev +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: mysql-user-credentials + data: + - secretKey: password + remoteRef: + key: "MySQL Server - UDC User - Dev" + property: password +--- +# Discord bot token — from 1Password "Bot Token - Dev" +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: discord-bot-token + namespace: udc-bot-dev +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: discord-bot-token + data: + - secretKey: identifiant + remoteRef: + key: "Bot Token - Dev" + property: identifiant +--- +# AWS credentials for MySQL backups to S3 — from 1Password "AWS Backup Credentials" +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: mysql-backup-credentials + namespace: udc-bot-dev +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: mysql-backup-credentials + data: + - secretKey: AWS_ACCESS_KEY_ID + remoteRef: + key: "AWS Backup Credentials" + property: access-key-id + - secretKey: AWS_SECRET_ACCESS_KEY + remoteRef: + key: "AWS Backup Credentials" + property: secret-access-key +--- +# Bot API keys — from various 1Password items +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: bot-api-keys + namespace: udc-bot-dev +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: bot-api-keys + data: + - secretKey: weather-api-key + remoteRef: + key: "OpenWeather" + property: credential + - secretKey: ipgeo-api-key + remoteRef: + key: "IP Geolocation" + property: credential + - secretKey: flight-api-key + remoteRef: + key: "FlightAPI" + property: api_key + - secretKey: flight-api-secret + remoteRef: + key: "FlightAPI" + property: api_secret + - secretKey: airlab-api-key + remoteRef: + key: "AirlabAPI" + property: api_key diff --git a/k8s/dev/mysql-backup.yaml b/k8s/dev/mysql-backup.yaml new file mode 100644 index 00000000..ad305e02 --- /dev/null +++ b/k8s/dev/mysql-backup.yaml @@ -0,0 +1,69 @@ +# MySQL backup using databack/mysql-backup — handles its own daily schedule internally. +# Dumps go to S3 bucket "udc-bot-mysql-backups" with 90-day retention via S3 lifecycle. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql-backup + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: mysql-backup + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: mysql-backup + template: + metadata: + labels: + app.kubernetes.io/name: mysql-backup + app.kubernetes.io/part-of: udc-bot + environment: dev + spec: + containers: + - name: mysql-backup + image: databack/mysql-backup:1.4.0 + args: ["dump"] + env: + - name: DB_SERVER + value: mysql + - name: DB_PORT + value: "3306" + - name: DB_USER + value: root + - name: DB_PASS + valueFrom: + secretKeyRef: + name: mysql-credentials + key: password + - name: DB_DUMP_TARGET + value: s3://udc-bot-mysql-backups/udc-bot-mysql-dev + - name: DB_DUMP_FREQ + value: "1440" + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: mysql-backup-credentials + key: AWS_ACCESS_KEY_ID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: mysql-backup-credentials + key: AWS_SECRET_ACCESS_KEY + - name: AWS_DEFAULT_REGION + value: eu-west-3 + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/k8s/dev/mysql.yaml b/k8s/dev/mysql.yaml new file mode 100644 index 00000000..8198f72f --- /dev/null +++ b/k8s/dev/mysql.yaml @@ -0,0 +1,115 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-data + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: mysql + template: + metadata: + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: dev + spec: + containers: + - name: mysql + image: mysql:8.0 + ports: + - containerPort: 3306 + protocol: TCP + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-credentials + key: password + - name: MYSQL_DATABASE + value: udcbot + - name: MYSQL_USER + value: udcbot + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-user-credentials + key: password + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + readinessProbe: + exec: + command: + - sh + - -c + - mysqladmin ping -h 127.0.0.1 -u root -p"${MYSQL_ROOT_PASSWORD}" + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + exec: + command: + - sh + - -c + - mysqladmin ping -h 127.0.0.1 -u root -p"${MYSQL_ROOT_PASSWORD}" + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + volumes: + - name: mysql-data + persistentVolumeClaim: + claimName: mysql-data +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + type: ClusterIP + ports: + - port: 3306 + targetPort: 3306 + protocol: TCP + selector: + app.kubernetes.io/name: mysql diff --git a/k8s/dev/namespace.yaml b/k8s/dev/namespace.yaml new file mode 100644 index 00000000..d8d9594b --- /dev/null +++ b/k8s/dev/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: udc-bot-dev + labels: + app.kubernetes.io/part-of: udc-bot + environment: dev diff --git a/k8s/dev/phpmyadmin.yaml b/k8s/dev/phpmyadmin.yaml new file mode 100644 index 00000000..9f3e216a --- /dev/null +++ b/k8s/dev/phpmyadmin.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: phpmyadmin + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: phpmyadmin + template: + metadata: + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: dev + spec: + containers: + - name: phpmyadmin + image: phpmyadmin:5.2.3 + ports: + - containerPort: 80 + protocol: TCP + env: + - name: PMA_HOST + value: mysql + - name: PMA_PORT + value: "3306" + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 10 + securityContext: + allowPrivilegeEscalation: false +--- +apiVersion: v1 +kind: Service +metadata: + name: phpmyadmin + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: dev +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + selector: + app.kubernetes.io/name: phpmyadmin +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: phpmyadmin + namespace: udc-bot-dev + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: dev + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: default-ip-allowlist@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - phpmyadmin.dev.bot.udc.ovh + secretName: phpmyadmin-dev-tls + rules: + - host: phpmyadmin.dev.bot.udc.ovh + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: phpmyadmin + port: + number: 80 diff --git a/k8s/prod/bot-config.yaml b/k8s/prod/bot-config.yaml new file mode 100644 index 00000000..532b73d7 --- /dev/null +++ b/k8s/prod/bot-config.yaml @@ -0,0 +1,144 @@ +# Settings.json template for UDC-Bot prod environment. +# Secrets are substituted at pod startup by the render-config init container via envsubst. +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-config + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: prod +data: + Settings.json: | + { + "token": "${BOT_TOKEN}", + "DbConnectionString": "server=mysql;port=3306;database=udcbot;user id=udcbot;Password=${DB_PASSWORD};SSL Mode=None;Allow User Variables=True;AllowPublicKeyRetrieval=True", + "invite": "https://discord.gg/bu3bbby", // Currently Unused + /* 'SSL MODE' and 'Allow User Variables' are only required when running on a local machine with XAMPP. This can often be removed. */ + /* DB Info*/ + /*Server Info*/ + "serverRootPath": "./SERVER", + "assetsRootPath": "./Assets", + /* Base info */ + "prefix": "!", + "Administrator": "493514411026153482", + "ModeratorRoleId": "493514490504019969", + "ModeratorCommandsEnabled": false, + "guildId": "493510779866316801", // Replace with your servers guild ID + /* All assignable roles as of 29/04/21 */ + "UserAssignableRoles": { + "desc": "All normal user assignable roles available", + "roles": [ + "Audio-Engineers", + "Technical-Artists", + "Animators", + "3D-Artists", + "2D-Artists", + "XR-Developers", + "Programmers", + "Writers", + "Game-Designers", + "Generalists", + "Hobbyists", + "Students" + ] + }, + /* Channel IDs for certain channels. */ + "generalChannel": { // Off-topic + "desc": "General-Chat Channel", + "id": "493511024037724180" + }, + "botAnnouncementChannel": { // Most bot logs will go here + "desc": "Bot-Announcement Channel", + "id": "493512007144833055" + }, + "announcementsChannel": { // Not used by bot + "desc": "General Announcement Channel", + "id": "493510992320528404" // Currently Unused 29/04/21 + }, + "botCommandsChannel": { + "desc": "Bot-Commands Channel", + "id": "493512044973260811" + }, + "RulesChannel": { + "desc": "The Rules", + "id": "519890141805019137" + }, + "ReportedMessageChannel": { + "desc": "Reported Message Channel", + "id": "993446104790220840" + }, + /* Role Ids */ + "mutedRoleID": "493514472942600202", + "SubsNewsRoleId": "1209260621342707772", + "SubsReleasesRoleId": "523205962279157771", + "TipsUserRoleId": "493514563736698880", + "TipsAuthorRoleId": "99999", + /*Complaints Channels Stuff*/ + "complaintCategoryId": "520853507851681797", + "complaintChannelPrefix": "Complaint", + "closedComplaintCategoryId": "662084543662129175", + "closedComplaintChannelPrefix": "Closed-", + /*Commands Configuration*/ + "wikipediaSearchPage": "https://en.wikipedia.org/w/api.php?action=query&format=json&prop=extracts|info&generator=prefixsearch&redirects=1&converttitles=1&utf8=1&formatversion=2&exchars=750&exintro=1&explaintext=1&exsectionformat=plain&inprop=url&gpslimit=5&gpsprofile=fuzzy&gpssearch=", + "IPGeolocationAPIKey": "${IPGEO_KEY}", + "WeatherAPIKey": "${WEATHER_KEY}", + "FlightAPIKey": "${FLIGHT_KEY}", + "FlightAPISecret": "${FLIGHT_SECRET}", + "AirLabAPIKey": "${AIRLAB_KEY}", + /* Feed Service */ + "UnityNewsChannel": { + "desc": "Unity News Channel", + "id": "1142423451383119975" + }, + "UnityReleasesChannel": { + "desc": "Unity Releases Channel", + "id": "1142423451383119975" + }, + /* Recruitment Service */ + "RecruitmentServiceEnabled": false, + "RecruitmentChannel": { + "desc": "Channel for job postings", + "id": "1019677109171527750" + }, + "TagLookingToHire": "1019680606067630151", + "TagLookingForWork": "1019680763756695653", + "TagUnpaidCollab": "1019680795641774110", + "TagPositionFilled": "1052258665530408991", + "EditPermissionAccessTimeMin": 3, + /* Unity Help Service */ + "UnityHelpBabySitterEnabled": false, + "GenericHelpChannel": { // Unity-help + "desc": "Unity-Help Channel", + "id": "1019663870798856212" + }, + "TagUnitHelpResolvedTag": "1019672922811551815", + "IntroductionChannel": { + "desc": "Introduction Channel", + "id": "768488410959708210" + }, + "IntroductionWatcherServiceEnabled": true, + "UserModuleSlapObjectsTable": "Settings/udc-slap.txt", + "UserModuleSlapChoices": [ + "trout", "duck", "truck", "paddle", "magikarp", "sausage", "student loan", + "life choice", "bug report", "unhandled exception", "null pointer", "keyboard", + "kinematic rigidbody", "gameobject", "trigger collider", "update cycle", "json file", + "large language model", "hosting invoice", "quality of life patch", + "game jam submission", "bucket of fried chicken", "anime waifu pillow", + "network-attached storage cabinet", "baguette", "moldy cheese", + "cup noodle", "game jam submission", "game library listing", + "cheese wheel", "banana peel", "unresolved bug", "low poly donut" + ], + "UserModuleSlapFails": [ + "hurting themselves", "making themselves look foolish", "tripping on it", + "dropping it on their toes", "breaking their screen with it" + ], + "TipImageDirectory": "tips", + "BirthdayAnnouncementEnabled": true, + "BirthdayCheckIntervalMinutes": 60, + "BirthdayAnnouncementChannel": { + "desc": "Channel for birthday announcements", + "id": "493511024037724180" + } + } diff --git a/k8s/prod/bot-settings-config.yaml b/k8s/prod/bot-settings-config.yaml new file mode 100644 index 00000000..fceed72e --- /dev/null +++ b/k8s/prod/bot-settings-config.yaml @@ -0,0 +1,153 @@ +# ConfigMaps for bot Settings files (Rules, FAQs, UserSettings). +# These are rendered into an emptyDir volume by the render-config init container. +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-rules + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: prod +data: + Rules.json: | + { + "channel": [ + { + "id": 0, + "header": "", + "content": "**Global rules:**\n**General Rules**\n- Keep it friendly and civil.\n- Do not post anything illegal content or illegal links (cracked software links etc.)\n- Do not look down on others just because they're less experienced than you, try to help them instead.\n- Use the appropriate channels\n- If you ask for help, try to give it back.\n- Do not link to other discords without a mods permission\n**Help Channels Rules**:\n- Make questions as descriptive as possible by adding all the necessary information.\n- **Do not ask if someone can help.** Post your question directly.\n- Do not beg for help.\n- Do not repost questions unless it is buried under dozens of other messages.\n- Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 493511037421879316, + "header": "Ask general Unity questions in this channel. Newbie questions welcome.\n", + "content": "\n- Make your question as descriptive as possible, adding all the necessary informations.\n- Do not ask if someone can help. Post your question directly.\n- Do not beg for help.\n- Do not repost your question unless it is buried under dozens of other messages.\n- Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 493511468134694912, + "header": "This channel is for advanced C# questions and scripting related discussions (script optimization, naming convention...).", + "content": "\n- Make your question as descriptive as possible, adding all the necessary informations.\n- Do not ask if someone can help. Post your question directly.\n- Do not beg for help.\n- Do not repost your question unless it is buried under dozens of other messages.\n- Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 493511492226908181, + "header": "Use this channel to ask questions about game networking and discuss networking in general.", + "content": "\n- Light conversation is permitted in this channel.\n- Make any questions as descriptive as possible, adding all the necessary informations.\n- Do not repost your question unless it is buried under dozens of other messages.\n- Do not post the same question in multiple channels. If you found a more appropriate channel, delete your old message first." + }, + { + "id": 493511590164037632, + "header": "Use this channel to share your game development through screenshots, videos and devlogs.", + "content": "\n- Keep posts short and sweet with a name and description of what we're looking at!" + }, + { + "id": 493511791578578944, + "header": "Use this channel to post resources and tutorials about Unity and its ecosystem.", + "content": "\n- Only post link, no discussion around them in this channel." + }, + { + "id": 493511872709001221, + "header": "Use this channel if you're looking for a job. Please read the pins for a recommended template.", + "content": "\n- Only for **paid** jobs. Use #collaboration if you want to work for free\n- Describe your skills as best as possible\n- Portfolio recommended, don't use multiple image links as they'll generate several thumbnails, use an album instead.\n- Wait **at least 7 days** before reposting, and do not repost if there's less than 10 posts in between.\n- Your offer must be in a single post. Edit it if you want to add aditionnal infos.\n- Discussions about the post must be made in DM or another channel." + }, + { + "id": 493511844904828990, + "header": "Use this channel if you're looking to hire someone. Please read the pins for a recommended template.", + "content": "\n- Only for **paid** jobs\n- Describe skills required as best as possible\n- If using links, ensure they are escaped and don't generate more than 1 thumbnail.\n- Wait **at least 7 days** before reposting, and do not repost if there's less than 10 posts in between.\n- Your offer must be in a single post. Edit it if you want to add aditionnal infos.\n·Discussions about the post must be made in DM or another channel." + }, + { + "id": 493511883651809300, + "header": "Use this channel if you want to collaborate for free. Please read the pins for a recommended template.", + "content": "\n- If you want to be hired : describe your skills as best as possible, portfolio recommended\n- If you want to hire : Describe the skills needed as best as possible, and your skills (and your team if you already have one)\n- Give as much info as possible on the project on hand\n- Wait **at least 7 days** before reposting, and do not repost if there's less than 10 posts in between.\n- Your offer must be in a single post. Edit it if you want to add aditionnal infos.\n- Discussions about the post must be made in DM or another channel." + }, + { + "id": 493511631557492736, + "header": "Post here about your finished projects along with a link to download or purchase it.", + "content": "\n- Do not repost projects multiple times\n- Keep discussion at a minimum to not drown out other projects" + }, + { + "id": 493511757822820353, + "header": "Use this channel to discuss all Not Safe For Work related development.", + "content": "\n- This is a serious development channel, do not post memes here." + }, + { + "id": 493511779499114517, + "header": "Channel to discuss marketing strategy and business.", + "content": "\n- Do not post your finished game links here, use #finished-projects" + } + ] + } +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-faqs + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: prod +data: + FAQs.json: | + [ + { + "question": "What is karma ?", + "answer": "Karma is tracked on your !profile, helping indicate how much you've helped others. You also earn slightly more EXP from things the higher your Karma level is. Karma may be used for more features in the future.", + "keywords": [ "karma", "xp" ] + } + ] +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: bot-user-settings + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: prod +data: + UserSettings.json: | + { + /*Thanks parameters*/ + "thanks": [ + "thanks", + "ty", + "thx", + "thnx", + "thanx", + "thankyou", + "thank you", + "tysm", + "cheers", + "merci", + "gracias", + "danke", + "grazie", + "arigatou", + "有り難う", + "有難う", + "ありがとう", + "どうも", + "고맙습니다", + "谢谢", + "Slàinte" + ], + "thanksCooldown": 60, //In seconds + "thanksReminderCooldown": 86400, //24 hours in seconds + "thanksMinJoinTime": 600, + + /*Xp parameters*/ + "xpMinPerMessage": 10, + "xpMaxPerMessage": 30, + "xpMinCooldown": 60, + "xpMaxCooldown": 180, + + /*Code parameters*/ + "codeReminderCooldown": 86400, + + "isSomeoneThere": [ + "is anyone around?", + "can someone help?", + "can someone help me?" + ] + } diff --git a/k8s/prod/bot.yaml b/k8s/prod/bot.yaml new file mode 100644 index 00000000..8421fb2d --- /dev/null +++ b/k8s/prod/bot.yaml @@ -0,0 +1,164 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: bot-server-data + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: udc-bot + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + replicas: 0 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: udc-bot + template: + metadata: + labels: + app.kubernetes.io/name: udc-bot + app.kubernetes.io/part-of: udc-bot + environment: prod + spec: + initContainers: + - name: render-config + image: alpine:3.20 + command: + - sh + - -c + - | + set -e + apk add --no-cache gettext >/dev/null 2>&1 + envsubst '${BOT_TOKEN} ${DB_PASSWORD} ${WEATHER_KEY} ${IPGEO_KEY} ${FLIGHT_KEY} ${FLIGHT_SECRET} ${AIRLAB_KEY}' < /config-template/Settings.json > /app-settings/Settings.json + cp /rules-template/Rules.json /app-settings/Rules.json + cp /faqs-template/FAQs.json /app-settings/FAQs.json + cp /usersettings-template/UserSettings.json /app-settings/UserSettings.json + env: + - name: BOT_TOKEN + valueFrom: + secretKeyRef: + name: discord-bot-token + key: identifiant + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-user-credentials + key: password + - name: WEATHER_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: weather-api-key + - name: IPGEO_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: ipgeo-api-key + - name: FLIGHT_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: flight-api-key + - name: FLIGHT_SECRET + valueFrom: + secretKeyRef: + name: bot-api-keys + key: flight-api-secret + - name: AIRLAB_KEY + valueFrom: + secretKeyRef: + name: bot-api-keys + key: airlab-api-key + volumeMounts: + - name: config-template + mountPath: /config-template + readOnly: true + - name: rules-template + mountPath: /rules-template + readOnly: true + - name: faqs-template + mountPath: /faqs-template + readOnly: true + - name: usersettings-template + mountPath: /usersettings-template + readOnly: true + - name: app-settings + mountPath: /app-settings + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + - name: wait-for-mysql + image: busybox:1.37 + command: + - sh + - -c + - | + until nc -z mysql 3306; do + echo "Waiting for MySQL..." + sleep 2 + done + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + containers: + - name: bot + image: ghcr.io/unity-developer-community/udc-bot:latest + volumeMounts: + - name: app-settings + mountPath: /app/Settings + - name: server-data + mountPath: /app/SERVER + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + add: + - DAC_OVERRIDE + volumes: + - name: config-template + configMap: + name: bot-config + - name: rules-template + configMap: + name: bot-rules + - name: faqs-template + configMap: + name: bot-faqs + - name: usersettings-template + configMap: + name: bot-user-settings + - name: app-settings + emptyDir: {} + - name: server-data + persistentVolumeClaim: + claimName: bot-server-data diff --git a/k8s/prod/external-secrets.yaml b/k8s/prod/external-secrets.yaml new file mode 100644 index 00000000..4b3baa74 --- /dev/null +++ b/k8s/prod/external-secrets.yaml @@ -0,0 +1,115 @@ +--- +# MySQL root password — from 1Password "MySQL Server - Root User - Prod" +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: mysql-credentials + namespace: udc-bot-prod +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: mysql-credentials + data: + - secretKey: password + remoteRef: + key: "MySQL Server - Root User - Prod" + property: password +--- +# MySQL udcbot user password — from 1Password "MySQL Server - UDC User - Prod" +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: mysql-user-credentials + namespace: udc-bot-prod +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: mysql-user-credentials + data: + - secretKey: password + remoteRef: + key: "MySQL Server - UDC User - Prod" + property: password +--- +# Discord bot token — from 1Password "Bot Token - Prod" +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: discord-bot-token + namespace: udc-bot-prod +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: discord-bot-token + data: + - secretKey: identifiant + remoteRef: + key: "Bot Token - Prod" + property: identifiant +--- +# AWS credentials for MySQL backups to S3 (shared with dev) +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: mysql-backup-credentials + namespace: udc-bot-prod +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: mysql-backup-credentials + data: + - secretKey: AWS_ACCESS_KEY_ID + remoteRef: + key: "AWS Backup Credentials" + property: access-key-id + - secretKey: AWS_SECRET_ACCESS_KEY + remoteRef: + key: "AWS Backup Credentials" + property: secret-access-key +--- +# Bot API keys — same keys shared between dev and prod +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: bot-api-keys + namespace: udc-bot-prod +spec: + refreshInterval: 1h + secretStoreRef: + name: onepassword + kind: ClusterSecretStore + target: + name: bot-api-keys + data: + - secretKey: weather-api-key + remoteRef: + key: "OpenWeather" + property: credential + - secretKey: ipgeo-api-key + remoteRef: + key: "IP Geolocation" + property: credential + - secretKey: flight-api-key + remoteRef: + key: "FlightAPI" + property: api_key + - secretKey: flight-api-secret + remoteRef: + key: "FlightAPI" + property: api_secret + - secretKey: airlab-api-key + remoteRef: + key: "AirlabAPI" + property: api_key diff --git a/k8s/prod/mysql-backup.yaml b/k8s/prod/mysql-backup.yaml new file mode 100644 index 00000000..0bd1e584 --- /dev/null +++ b/k8s/prod/mysql-backup.yaml @@ -0,0 +1,69 @@ +# MySQL backup using databack/mysql-backup — handles its own daily schedule internally. +# Dumps go to S3 bucket "udc-bot-mysql-backups" with 90-day retention via S3 lifecycle. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql-backup + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: mysql-backup + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: mysql-backup + template: + metadata: + labels: + app.kubernetes.io/name: mysql-backup + app.kubernetes.io/part-of: udc-bot + environment: prod + spec: + containers: + - name: mysql-backup + image: databack/mysql-backup:1.4.0 + args: ["dump"] + env: + - name: DB_SERVER + value: mysql + - name: DB_PORT + value: "3306" + - name: DB_USER + value: root + - name: DB_PASS + valueFrom: + secretKeyRef: + name: mysql-credentials + key: password + - name: DB_DUMP_TARGET + value: s3://udc-bot-mysql-backups/udc-bot-mysql-prod + - name: DB_DUMP_FREQ + value: "1440" + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: mysql-backup-credentials + key: AWS_ACCESS_KEY_ID + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: mysql-backup-credentials + key: AWS_SECRET_ACCESS_KEY + - name: AWS_DEFAULT_REGION + value: eu-west-3 + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 100m + memory: 128Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL diff --git a/k8s/prod/mysql.yaml b/k8s/prod/mysql.yaml new file mode 100644 index 00000000..33b73f77 --- /dev/null +++ b/k8s/prod/mysql.yaml @@ -0,0 +1,115 @@ +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-data + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: mysql + template: + metadata: + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: prod + spec: + containers: + - name: mysql + image: mysql:8.0 + ports: + - containerPort: 3306 + protocol: TCP + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-credentials + key: password + - name: MYSQL_DATABASE + value: udcbot + - name: MYSQL_USER + value: udcbot + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-user-credentials + key: password + volumeMounts: + - name: mysql-data + mountPath: /var/lib/mysql + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1000m + memory: 1Gi + readinessProbe: + exec: + command: + - sh + - -c + - mysqladmin ping -h 127.0.0.1 -u root -p"${MYSQL_ROOT_PASSWORD}" + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + livenessProbe: + exec: + command: + - sh + - -c + - mysqladmin ping -h 127.0.0.1 -u root -p"${MYSQL_ROOT_PASSWORD}" + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + volumes: + - name: mysql-data + persistentVolumeClaim: + claimName: mysql-data +--- +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + type: ClusterIP + ports: + - port: 3306 + targetPort: 3306 + protocol: TCP + selector: + app.kubernetes.io/name: mysql diff --git a/k8s/prod/namespace.yaml b/k8s/prod/namespace.yaml new file mode 100644 index 00000000..7e368fb5 --- /dev/null +++ b/k8s/prod/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: udc-bot-prod + labels: + app.kubernetes.io/part-of: udc-bot + environment: prod diff --git a/k8s/prod/phpmyadmin.yaml b/k8s/prod/phpmyadmin.yaml new file mode 100644 index 00000000..4194b194 --- /dev/null +++ b/k8s/prod/phpmyadmin.yaml @@ -0,0 +1,96 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: phpmyadmin + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: phpmyadmin + template: + metadata: + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: prod + spec: + containers: + - name: phpmyadmin + image: phpmyadmin:5.2.3 + ports: + - containerPort: 80 + protocol: TCP + env: + - name: PMA_HOST + value: mysql + - name: PMA_PORT + value: "3306" + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 200m + memory: 256Mi + readinessProbe: + httpGet: + path: / + port: 80 + initialDelaySeconds: 10 + periodSeconds: 10 + securityContext: + allowPrivilegeEscalation: false +--- +apiVersion: v1 +kind: Service +metadata: + name: phpmyadmin + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: prod +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + selector: + app.kubernetes.io/name: phpmyadmin +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: phpmyadmin + namespace: udc-bot-prod + labels: + app.kubernetes.io/name: phpmyadmin + app.kubernetes.io/part-of: udc-bot + environment: prod + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod + traefik.ingress.kubernetes.io/router.middlewares: default-ip-allowlist@kubernetescrd +spec: + ingressClassName: traefik + tls: + - hosts: + - phpmyadmin.bot.udc.ovh + secretName: phpmyadmin-prod-tls + rules: + - host: phpmyadmin.bot.udc.ovh + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: phpmyadmin + port: + number: 80