├── .deepsource.toml ├── .github ├── dependabot.yml └── workflows │ ├── AddCommentOnPR.yml │ ├── Build&Deploy.yml │ ├── DeployDev.yml │ ├── DeployProd.yml │ ├── Dotnet.yml │ └── HandleDeployCommand.yml ├── .gitignore ├── .idea └── .idea.DiscordBot │ └── .idea │ ├── .gitignore │ ├── .name │ ├── indexLayout.xml │ ├── riderModule.iml │ └── vcs.xml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── DiscordBot.sln ├── DiscordBot ├── .gitignore ├── AssemblyDefinition.cs ├── Attributes │ ├── BotCommandChannelAttribute.cs │ ├── HideFromHelpAttribute.cs │ ├── IgnoreBotsAttribute.cs │ ├── RoleAttributes.cs │ └── ThreadAttributes.cs ├── Constants.cs ├── Data │ └── UnityAPI.cs ├── DiscordBot.csproj ├── Domain │ ├── ProfileData.cs │ └── RectangleD.cs ├── Extensions │ ├── ChannelExtensions.cs │ ├── ContextExtension.cs │ ├── DBConnectionExtension.cs │ ├── DateExtensions.cs │ ├── EmbedBuilderExtension.cs │ ├── InternetExtensions.cs │ ├── MessageExtensions.cs │ ├── ReactMessageExtensions.cs │ ├── StringExtensions.cs │ ├── TaskExtensions.cs │ ├── UserDBRepository.cs │ ├── UserExtensions.cs │ └── UserServiceExtensions.cs ├── GlobalSuppressions.cs ├── GlobalUsings.cs ├── Modules │ ├── AirportModule.cs │ ├── EmbedModule.cs │ ├── ModerationModule.cs │ ├── ReactionRoleModule.cs │ ├── ReminderModule.cs │ ├── TicketModule.cs │ ├── TipModule.cs │ ├── UnityHelp │ │ ├── CannedInteractiveModule.cs │ │ ├── CannedResponseModule.cs │ │ ├── GeneralHelpModule.cs │ │ ├── UnityHelpInteractiveModule.cs │ │ └── UnityHelpModule.cs │ ├── UserModule.cs │ ├── UserSlashModule.cs │ └── Weather │ │ ├── WeatherContainers.cs │ │ └── WeatherModule.cs ├── Program.cs ├── Properties │ ├── Resources.Designer.cs │ └── Resources.resx ├── SERVER │ ├── FAQs.json │ ├── fonts │ │ ├── Consolas.ttf │ │ ├── ConsolasBold.ttf │ │ ├── OpenSans-Regular.ttf │ │ ├── OpenSansEmoji.ttf │ │ └── georgia.ttf │ ├── images │ │ ├── ExampleExport.png │ │ ├── Layout.txt │ │ ├── background.png │ │ ├── background.psd │ │ ├── background_old.png │ │ ├── default.png │ │ ├── foreground.png │ │ ├── foreground.psd │ │ ├── levelupcard.png │ │ ├── levelupcard.psd │ │ ├── levelupcardbackground.png │ │ └── triangle.png │ └── skins │ │ ├── background.png │ │ ├── foreground.png │ │ ├── skin.default.json │ │ └── skin.json ├── Services │ ├── AirportService.cs │ ├── CommandHandlingService.cs │ ├── CurrencyService.cs │ ├── DatabaseService.cs │ ├── FeedService.cs │ ├── LoggingService.cs │ ├── Moderation │ │ └── IntroductionWatcherService.cs │ ├── ModerationService.cs │ ├── PublisherService.cs │ ├── ReactRoleService.cs │ ├── Recruitment │ │ └── RecruitService.cs │ ├── ReminderService.cs │ ├── Tips │ │ ├── Components │ │ │ └── Tip.cs │ │ └── TipService.cs │ ├── UnityHelp │ │ ├── CannedResponseService.cs │ │ ├── Components │ │ │ ├── HelpBotMessage.cs │ │ │ └── ThreadContainer.cs │ │ └── UnityHelpService.cs │ ├── UpdateService.cs │ ├── UserExtendedService.cs │ ├── UserService.cs │ └── WeatherService.cs ├── Settings │ ├── Deserialized │ │ ├── ReactionRole.cs │ │ ├── Rules.cs │ │ ├── Settings.cs │ │ └── UserSettings.cs │ ├── ReactionRoles.json │ ├── Rules.json │ ├── Settings.example.json │ └── UserSettings.json ├── Skin │ ├── AvatarBorderSkinModule.cs │ ├── BaseTextSkinModule.cs │ ├── CustomTextSkinModule.cs │ ├── ISkinModule.cs │ ├── KarmaPointsSkinModule.cs │ ├── KarmaRankSkinModule.cs │ ├── LevelSkinModule.cs │ ├── RectangleD.cs │ ├── RectangleSampleAvatarColorSkinModule.cs │ ├── SkinData.cs │ ├── SkinLayer.cs │ ├── SkinModuleJsonConverter.cs │ ├── TotalXpSkinModule.cs │ ├── UsernameSkinModule.cs │ ├── XpBarInfoSkinModule.cs │ ├── XpBarSkinModule.cs │ └── XpRankSkinModule.cs └── Utils │ ├── MathUtility.cs │ ├── SerializeUtil.cs │ ├── StringUtil.cs │ ├── Utils.cs │ └── WebUtil.cs ├── Dockerfile ├── LICENSE ├── NuGet.config ├── README.md ├── UpgradeLog.htm └── docker-compose.yml /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "csharp" 5 | enabled = true -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/AddCommentOnPR.yml: -------------------------------------------------------------------------------- 1 | name: Add Deployment Comment to PR 2 | "on": 3 | pull_request_target: 4 | types: [opened] 5 | 6 | jobs: 7 | add_deploy_comment: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Add deployment comment 11 | uses: thollander/actions-comment-pull-request@v3 12 | with: 13 | message: | 14 | ### 🚀 Deploy this PR to an environment 15 | 16 | You can deploy this PR to either development or staging environment: 17 | 18 | - Comment `/deploy_dev` to deploy to the **development** environment 19 | 20 | Alternatively, you can: 21 | 1. Go to Actions tab 22 | 2. Click on "Manual Deploy to Firebase" workflow 23 | 3. Click the "Run workflow" button 24 | 4. Select branch: `${{ github.event.pull_request.head.ref }}` 25 | 5. Choose environment: DEV 26 | 6. Enter a deployment message 27 | 7. Click "Run workflow" 28 | -------------------------------------------------------------------------------- /.github/workflows/Build&Deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build & Deploy 2 | 3 | on: 4 | push: 5 | branches: [dev] 6 | workflow_call: 7 | inputs: 8 | env: 9 | required: true 10 | type: string 11 | workflow_dispatch: 12 | inputs: 13 | env: 14 | description: "Environment to deploy to" 15 | required: true 16 | default: "dev" 17 | type: choice 18 | options: 19 | - dev 20 | 21 | jobs: 22 | push_to_registry: 23 | name: Push Docker image to GitHub Packages 24 | runs-on: ubuntu-latest 25 | 26 | steps: 27 | - name: Login to GitHub Container Registry 28 | uses: docker/login-action@v1 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.repository_owner }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Build and push 35 | uses: docker/build-push-action@v2 36 | with: 37 | push: true 38 | tags: ghcr.io/unity-developer-community/udc-bot-dev:latest 39 | 40 | restart: 41 | name: Restart Bot 42 | needs: push_to_registry 43 | runs-on: ubuntu-latest 44 | 45 | environment: 46 | name: ${{ inputs.env }} 47 | 48 | steps: 49 | - name: Run commands in SSH 50 | uses: appleboy/ssh-action@master 51 | with: 52 | script: | 53 | cd ${{ secrets.SERVER_BUILD_DIR }} 54 | docker-compose pull 55 | docker-compose up -d 56 | host: ${{ secrets.SERVER_IP }} 57 | port: ${{ secrets.SERVER_PORT }} 58 | username: ${{ secrets.SERVER_USER }} 59 | password: ${{ secrets.SERVER_PASSWORD }} 60 | 61 | - name: Discord notification 62 | env: 63 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 64 | uses: Ilshidur/action-discord@master 65 | with: 66 | args: Bot has been deployment to Test Server successfully. 67 | -------------------------------------------------------------------------------- /.github/workflows/DeployDev.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Test Server 2 | 3 | on: 4 | push: 5 | branches: [dev] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to GitHub Packages 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Login to GitHub Container Registry 15 | uses: docker/login-action@v1 16 | with: 17 | registry: ghcr.io 18 | username: ${{ github.repository_owner }} 19 | password: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Build and push 22 | uses: docker/build-push-action@v2 23 | with: 24 | push: true 25 | tags: ghcr.io/unity-developer-community/udc-bot-dev:latest 26 | 27 | restart: 28 | name: Restart Bot 29 | needs: push_to_registry 30 | runs-on: ubuntu-latest 31 | 32 | environment: 33 | name: dev 34 | 35 | steps: 36 | - name: Run commands in SSH 37 | uses: appleboy/ssh-action@master 38 | with: 39 | script: | 40 | cd ${{ secrets.SERVER_BUILD_DIR }} 41 | docker-compose pull 42 | docker-compose up -d 43 | host: ${{ secrets.SERVER_IP }} 44 | port: ${{ secrets.SERVER_PORT }} 45 | username: ${{ secrets.SERVER_USER }} 46 | password: ${{ secrets.SERVER_PASSWORD }} 47 | 48 | - name: Discord notification 49 | env: 50 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 51 | uses: Ilshidur/action-discord@master 52 | with: 53 | args: Bot has been deployment to Test Server successfully. 54 | -------------------------------------------------------------------------------- /.github/workflows/DeployProd.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to UDC Server 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | push_to_registry: 10 | name: Push Docker image to GitHub Packages 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Login to GitHub Container Registry 15 | uses: docker/login-action@v1 16 | with: 17 | registry: ghcr.io 18 | username: ${{ github.repository_owner }} 19 | password: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | - name: Build and push 22 | uses: docker/build-push-action@v2 23 | with: 24 | push: true 25 | tags: ghcr.io/unity-developer-community/udc-bot:latest 26 | 27 | restart: 28 | name: Restart Bot 29 | needs: push_to_registry 30 | runs-on: ubuntu-latest 31 | 32 | environment: 33 | name: prod 34 | 35 | steps: 36 | - name: Run commands in SSH 37 | uses: appleboy/ssh-action@master 38 | with: 39 | script: | 40 | cd ${{ secrets.SERVER_BUILD_DIR }} 41 | docker-compose pull 42 | docker-compose up -d 43 | host: ${{ secrets.SERVER_IP }} 44 | port: ${{ secrets.SERVER_PORT }} 45 | username: ${{ secrets.SERVER_USER }} 46 | password: ${{ secrets.SERVER_PASSWORD }} 47 | 48 | - name: Discord notification 49 | env: 50 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 51 | uses: Ilshidur/action-discord@master 52 | with: 53 | args: Bot has been deployment to UDC Server successfully. 54 | -------------------------------------------------------------------------------- /.github/workflows/Dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET Build 2 | 3 | on: 4 | pull_request: 5 | branches: [master, dev] 6 | 7 | jobs: 8 | build: 9 | name: Build & Test 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Setup .NET Core 16 | uses: actions/setup-dotnet@v1 17 | with: 18 | dotnet-version: 6.0.x 19 | 20 | - name: Install dependencies 21 | run: dotnet restore 22 | 23 | - name: Build 24 | run: dotnet build --configuration Release --no-restore 25 | -------------------------------------------------------------------------------- /.github/workflows/HandleDeployCommand.yml: -------------------------------------------------------------------------------- 1 | name: Handle Deploy Command 2 | "on": 3 | issue_comment: 4 | types: [created] 5 | 6 | jobs: 7 | process_comment: 8 | if: github.event.issue.pull_request && (github.event.comment.body == '/deploy_dev') 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Determine deployment environment 12 | id: deployment_env 13 | run: | 14 | if [[ "${{ github.event.comment.body }}" == "/deploy_dev" ]]; then 15 | echo "env=dev" >> $GITHUB_OUTPUT 16 | echo "env_name=development" >> $GITHUB_OUTPUT 17 | fi 18 | 19 | - name: Get PR information 20 | id: pr_info 21 | uses: actions/github-script@v6 22 | with: 23 | github-token: ${{ secrets.GITHUB_TOKEN }} 24 | script: | 25 | const { owner, repo, number } = context.issue; 26 | const { data: pull } = await github.rest.pulls.get({ 27 | owner, 28 | repo, 29 | pull_number: number 30 | }); 31 | 32 | console.log("PR head branch:", pull.head.ref); 33 | console.log("PR head SHA:", pull.head.sha); 34 | 35 | core.setOutput("branch", pull.head.ref); 36 | core.setOutput("sha", pull.head.sha); 37 | core.setOutput("repo_name", pull.head.repo.full_name); 38 | return pull.head.ref; 39 | 40 | - name: Add reaction to comment 41 | uses: actions/github-script@v6 42 | with: 43 | github-token: ${{ secrets.GITHUB_TOKEN }} 44 | script: | 45 | github.rest.reactions.createForIssueComment({ 46 | owner: context.repo.owner, 47 | repo: context.repo.repo, 48 | comment_id: context.payload.comment.id, 49 | content: 'rocket' 50 | }); 51 | 52 | - name: Add deployment comment 53 | uses: actions/github-script@v6 54 | with: 55 | github-token: ${{ secrets.GITHUB_TOKEN }} 56 | script: | 57 | github.rest.issues.createComment({ 58 | owner: context.repo.owner, 59 | repo: context.repo.repo, 60 | issue_number: context.issue.number, 61 | body: `🚀 Starting deployment of \`${{ steps.pr_info.outputs.repo_name }}:${{ steps.pr_info.outputs.branch }}\` to ${{ steps.deployment_env.outputs.env_name }}...` 62 | }); 63 | 64 | - name: Generate unique branch name 65 | id: branch_name 66 | run: | 67 | TIMESTAMP=$(date +%s) 68 | UNIQUE_BRANCH="deploy-branch-${{ steps.pr_info.outputs.branch }}-$TIMESTAMP" 69 | echo "name=$UNIQUE_BRANCH" >> $GITHUB_OUTPUT 70 | 71 | - name: Checkout PR branch 72 | uses: actions/checkout@v3 73 | with: 74 | ref: ${{ steps.pr_info.outputs.sha }} 75 | repository: ${{ github.event.issue.pull_request.head.repo.full_name }} 76 | fetch-depth: 0 77 | 78 | - name: Create temporary branch 79 | run: | 80 | git checkout -b ${{ steps.branch_name.outputs.name }} 81 | git push origin ${{ steps.branch_name.outputs.name }} 82 | 83 | - name: Trigger deployment workflow 84 | uses: benc-uk/workflow-dispatch@v1 85 | with: 86 | workflow: Build & Deploy 87 | token: ${{ secrets.GITHUB_TOKEN }} 88 | ref: ${{ steps.branch_name.outputs.name }} 89 | inputs: | 90 | { 91 | "env": "${{ steps.deployment_env.outputs.env }}" 92 | } 93 | 94 | - name: Wait for deployment to start 95 | run: sleep 60 # Wait 60 seconds to ensure workflow has started 96 | 97 | - name: Clean up temporary branch 98 | if: always() 99 | run: | 100 | git push origin --delete ${{ steps.branch_name.outputs.name }} || true 101 | -------------------------------------------------------------------------------- /.idea/.idea.DiscordBot/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /contentModel.xml 6 | /modules.xml 7 | /.idea.DiscordBot.iml 8 | /projectSettingsUpdater.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | -------------------------------------------------------------------------------- /.idea/.idea.DiscordBot/.idea/.name: -------------------------------------------------------------------------------- 1 | DiscordBot -------------------------------------------------------------------------------- /.idea/.idea.DiscordBot/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.DiscordBot/.idea/riderModule.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/.idea.DiscordBot/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/DiscordBot/bin/Debug/netcoreapp2.1/DiscordBot.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/DiscordBot", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "sonarlint.connectedMode.project": { 3 | "connectionId": "unity-developer-community", 4 | "projectKey": "Unity-Developer-Community_UDC-Bot" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/DiscordBot/DiscordBot.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/DiscordBot/DiscordBot.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/DiscordBot/DiscordBot.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /DiscordBot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27421.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordBot", "DiscordBot\DiscordBot.csproj", "{D021BBDF-02DC-4938-B035-75D7EDBDBAC2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {D021BBDF-02DC-4938-B035-75D7EDBDBAC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {D021BBDF-02DC-4938-B035-75D7EDBDBAC2}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {D021BBDF-02DC-4938-B035-75D7EDBDBAC2}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {D021BBDF-02DC-4938-B035-75D7EDBDBAC2}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {8C06F0F6-2B30-4931-B93E-AE005F92C676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {8C06F0F6-2B30-4931-B93E-AE005F92C676}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {8C06F0F6-2B30-4931-B93E-AE005F92C676}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {8C06F0F6-2B30-4931-B93E-AE005F92C676}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(ExtensibilityGlobals) = postSolution 27 | SolutionGuid = {78E5BAD2-164B-46CF-8D4F-DDD5720A6DE0} 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /DiscordBot/.gitignore: -------------------------------------------------------------------------------- 1 | /Settings/Settings.json 2 | /SERVER/images/profiles/ 3 | /SERVER/images/subtitles/ 4 | /SERVER/log.txt 5 | 6 | SERVER/logXP.txt 7 | SERVER/scrap.py 8 | SERVER/botdata.json 9 | SERVER/unityapi.json 10 | SERVER/unitymanual.json 11 | SERVER/userdata.json 12 | SERVER/feeds.json 13 | -------------------------------------------------------------------------------- /DiscordBot/AssemblyDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("DiscordBot.Tests")] -------------------------------------------------------------------------------- /DiscordBot/Attributes/BotCommandChannelAttribute.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using DiscordBot.Settings; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace DiscordBot.Attributes; 6 | 7 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 8 | public class BotCommandChannelAttribute : PreconditionAttribute 9 | { 10 | public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 11 | { 12 | var settings = services.GetRequiredService(); 13 | 14 | if (context.Channel.Id == settings.BotCommandsChannel.Id) 15 | { 16 | return await Task.FromResult(PreconditionResult.FromSuccess()); 17 | } 18 | 19 | Task task = context.Message.DeleteAfterSeconds(seconds: 10); 20 | return await Task.FromResult(PreconditionResult.FromError($"This command can only be used in <#{settings.BotCommandsChannel.Id.ToString()}>.")); 21 | } 22 | } -------------------------------------------------------------------------------- /DiscordBot/Attributes/HideFromHelpAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Attributes; 2 | 3 | public class HideFromHelpAttribute : Attribute 4 | { 5 | } -------------------------------------------------------------------------------- /DiscordBot/Attributes/IgnoreBotsAttribute.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | 3 | namespace DiscordBot.Attributes; 4 | 5 | /// 6 | /// Simple attribute, if the command is used by a bot, it escapes early and doesn't run the command. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class IgnoreBotsAttribute : PreconditionAttribute 10 | { 11 | public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 12 | { 13 | if (context.Message.Author.IsBot) 14 | { 15 | return Task.FromResult(PreconditionResult.FromError(string.Empty)); 16 | } 17 | 18 | return Task.FromResult(PreconditionResult.FromSuccess()); 19 | } 20 | } 21 | 22 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 23 | public class IgnoreBotsAndWebhooksAttribute : PreconditionAttribute 24 | { 25 | public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 26 | { 27 | if (context.Message.Author.IsBot || context.Message.Author.IsWebhook) 28 | { 29 | return Task.FromResult(PreconditionResult.FromError(string.Empty)); 30 | } 31 | 32 | return Task.FromResult(PreconditionResult.FromSuccess()); 33 | } 34 | } 35 | 36 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 37 | public class IgnoreWebhooksAttribute : PreconditionAttribute 38 | { 39 | public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 40 | { 41 | if (context.Message.Author.IsWebhook) 42 | { 43 | return Task.FromResult(PreconditionResult.FromError(string.Empty)); 44 | } 45 | 46 | return Task.FromResult(PreconditionResult.FromSuccess()); 47 | } 48 | } -------------------------------------------------------------------------------- /DiscordBot/Attributes/RoleAttributes.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using Discord.WebSocket; 3 | using DiscordBot.Settings; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace DiscordBot.Attributes; 7 | 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class RequireAdminAttribute : PreconditionAttribute 10 | { 11 | public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 12 | { 13 | var user = (SocketGuildUser)context.Message.Author; 14 | 15 | if (user.Roles.Any(x => x.Permissions.Administrator)) return Task.FromResult(PreconditionResult.FromSuccess()); 16 | return Task.FromResult(PreconditionResult.FromError(user + " attempted to use admin only command!")); 17 | } 18 | } 19 | 20 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 21 | public class RequireModeratorAttribute : PreconditionAttribute 22 | { 23 | public override Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 24 | { 25 | var user = (SocketGuildUser)context.Message.Author; 26 | var settings = services.GetRequiredService(); 27 | 28 | if (user.Roles.Any(x => x.Id == settings.ModeratorRoleId)) return Task.FromResult(PreconditionResult.FromSuccess()); 29 | return Task.FromResult(PreconditionResult.FromError(user + " attempted to use a moderator command!")); 30 | } 31 | } -------------------------------------------------------------------------------- /DiscordBot/Attributes/ThreadAttributes.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using Discord.WebSocket; 3 | using DiscordBot.Settings; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace DiscordBot.Attributes; 7 | 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 9 | public class RequireThreadAttribute : PreconditionAttribute 10 | { 11 | protected SocketThreadChannel _currentThread; 12 | 13 | public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 14 | { 15 | this._currentThread = context.Message.Channel as SocketThreadChannel; 16 | if (this._currentThread != null) return await Task.FromResult(PreconditionResult.FromSuccess()); 17 | 18 | Task task = context.Message.DeleteAfterSeconds(seconds: 10); 19 | return await Task.FromResult(PreconditionResult.FromError("This command can only be used in a thread.")); 20 | } 21 | } 22 | 23 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 24 | public class RequireAutoThreadAttribute : RequireThreadAttribute 25 | { 26 | protected AutoThreadChannel _autoThreadChannel; 27 | 28 | public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 29 | { 30 | var res = await base.CheckPermissionsAsync(context, command, services); 31 | if (!res.IsSuccess) return res; 32 | 33 | var settings = services.GetRequiredService(); 34 | this._autoThreadChannel = settings.AutoThreadChannels.Find(x => this._currentThread.ParentChannel.Id == x.Id); 35 | if (this._autoThreadChannel != null) return await Task.FromResult(PreconditionResult.FromSuccess()); 36 | 37 | Task task = context.Message.DeleteAfterSeconds(seconds: 10); 38 | return await Task.FromResult(PreconditionResult.FromError("This command can only be used in a thread created automatically.")); 39 | 40 | } 41 | } 42 | 43 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 44 | public class RequireArchivableAutoThreadAttribute : RequireAutoThreadAttribute 45 | { 46 | public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 47 | { 48 | var res = await base.CheckPermissionsAsync(context, command, services); 49 | if (!res.IsSuccess) return res; 50 | 51 | if (this._autoThreadChannel.CanArchive) return await Task.FromResult(PreconditionResult.FromSuccess()); 52 | 53 | Task task = context.Message.DeleteAfterSeconds(seconds: 10); 54 | return await Task.FromResult(PreconditionResult.FromError("This command cannot be used in a this thread.")); 55 | } 56 | } 57 | 58 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 59 | public class RequireDeletableAutoThreadAttribute : RequireAutoThreadAttribute 60 | { 61 | public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 62 | { 63 | var res = await base.CheckPermissionsAsync(context, command, services); 64 | if (!res.IsSuccess) return res; 65 | 66 | if (this._autoThreadChannel.CanDelete) return await Task.FromResult(PreconditionResult.FromSuccess()); 67 | 68 | Task task = context.Message.DeleteAfterSeconds(seconds: 10); 69 | return await Task.FromResult(PreconditionResult.FromError("This command cannot be used in a this thread.")); 70 | } 71 | } 72 | 73 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 74 | public class RequireAutoThreadAuthorAttribute : RequireAutoThreadAttribute 75 | { 76 | public override async Task CheckPermissionsAsync(ICommandContext context, CommandInfo command, IServiceProvider services) 77 | { 78 | var res = await base.CheckPermissionsAsync(context, command, services); 79 | if (!res.IsSuccess) return res; 80 | 81 | var messages = await this._currentThread.GetPinnedMessagesAsync(); 82 | var firstMessage = messages.LastOrDefault(); 83 | if (firstMessage != null) 84 | { 85 | var user = (SocketGuildUser)context.Message.Author; 86 | if (firstMessage.MentionedUsers.Any(x => x.Id == context.User.Id)) 87 | return await Task.FromResult(PreconditionResult.FromSuccess()); 88 | } 89 | 90 | Task task = context.Message.DeleteAfterSeconds(seconds: 10); 91 | return await Task.FromResult(PreconditionResult.FromError("This command can only be used by the thread author.")); 92 | } 93 | } -------------------------------------------------------------------------------- /DiscordBot/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot; 2 | 3 | public static class Constants 4 | { 5 | public const int MaxLengthChannelMessage = 2000; 6 | } -------------------------------------------------------------------------------- /DiscordBot/Data/UnityAPI.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Data; 2 | 3 | public class Rating 4 | { 5 | public object Count { get; set; } 6 | public int Average { get; set; } 7 | } 8 | 9 | public class Kategory 10 | { 11 | public string Slug { get; set; } 12 | public string Name { get; set; } 13 | public string Id { get; set; } 14 | } 15 | 16 | public class Category 17 | { 18 | public string TreeId { get; set; } 19 | public string LabelEnglish { get; set; } 20 | public string Label { get; set; } 21 | public string Id { get; set; } 22 | public string Multiple { get; set; } 23 | } 24 | 25 | public class Publisher 26 | { 27 | public string LabelEnglish { get; set; } 28 | public string Url { get; set; } 29 | public string Slug { get; set; } 30 | public string Label { get; set; } 31 | public string Id { get; set; } 32 | public string SupportEmail { get; set; } 33 | public object SupportUrl { get; set; } 34 | } 35 | 36 | public class Link 37 | { 38 | public string Type { get; set; } 39 | public string Id { get; set; } 40 | } 41 | 42 | public class List 43 | { 44 | public string Slug { get; set; } 45 | public string SlugV2 { get; set; } 46 | public string Name { get; set; } 47 | public object Overlay { get; set; } 48 | } 49 | 50 | public class Flags 51 | { 52 | } 53 | 54 | public class Image 55 | { 56 | public string Link { get; set; } 57 | public string Width { get; set; } 58 | public string Name { get; set; } 59 | public string Type { get; set; } 60 | public string Height { get; set; } 61 | public string Thumb { get; set; } 62 | } 63 | 64 | public class Keyimage 65 | { 66 | public string Small { get; set; } 67 | public string Big { get; set; } 68 | public object SmallLegacy { get; set; } 69 | public object Facebook { get; set; } 70 | public object BigLegacy { get; set; } 71 | public string Icon { get; set; } 72 | public string Icon75 { get; set; } 73 | public string Icon25 { get; set; } 74 | } 75 | 76 | public class Daily 77 | { 78 | public string Icon { get; set; } 79 | public Rating Rating { get; set; } 80 | public int Remaining { get; set; } 81 | public Kategory Kategory { get; set; } 82 | public string PackageVersionId { get; set; } 83 | public string Slug { get; set; } 84 | public Category Category { get; set; } 85 | public string Hotness { get; set; } 86 | public string Id { get; set; } 87 | public Publisher Publisher { get; set; } 88 | public List List { get; set; } 89 | public Link Link { get; set; } 90 | public Flags Flags { get; set; } 91 | public Keyimage Keyimage { get; set; } 92 | public string Description { get; set; } 93 | public string TitleEnglish { get; set; } 94 | public string Title { get; set; } 95 | } 96 | 97 | public class Content 98 | { 99 | public string Pubdate { get; set; } 100 | public string MinUnityVersion { get; set; } 101 | public Rating Rating { get; set; } 102 | public Kategory Kategory { get; set; } 103 | public List UnityVersions { get; set; } 104 | public string Url { get; set; } 105 | public string PackageVersionId { get; set; } 106 | public string Slug { get; set; } 107 | public Category Category { get; set; } 108 | public string Id { get; set; } 109 | public Publisher Publisher { get; set; } 110 | public string Sizetext { get; set; } 111 | public List List { get; set; } 112 | public Link Link { get; set; } 113 | public List Images { get; set; } 114 | public Flags Flags { get; set; } 115 | public string Version { get; set; } 116 | public string FirstPublishedAt { get; set; } 117 | public Keyimage Keyimage { get; set; } 118 | public int License { get; set; } 119 | public string Description { get; set; } 120 | public List Upgrades { get; set; } 121 | public string Publishnotes { get; set; } 122 | public string Title { get; set; } 123 | public string ShortUrl { get; set; } 124 | public List Upgradables { get; set; } 125 | } 126 | 127 | public class DailyObject 128 | { 129 | public string Banner { get; set; } 130 | public string Feed { get; set; } 131 | public string Status { get; set; } 132 | public int DaysLeft { get; set; } 133 | public int Total { get; set; } 134 | public Daily Daily { get; set; } 135 | public int Remaining { get; set; } 136 | public string Badge { get; set; } 137 | public string Title { get; set; } 138 | public bool Countdown { get; set; } 139 | public List Results { get; set; } 140 | } 141 | 142 | public class PackageObject 143 | { 144 | public Content Content { get; set; } 145 | } 146 | 147 | public class PriceObject 148 | { 149 | public string Vat { get; set; } 150 | public string PriceExvat { get; set; } 151 | public string Price { get; set; } 152 | public bool IsFree { get; set; } 153 | } 154 | 155 | public class Result 156 | { 157 | public string Category { get; set; } 158 | public string Title { get; set; } 159 | public string Publisher { get; set; } 160 | } 161 | 162 | public class PackageHeadObject 163 | { 164 | public Result Result { get; set; } 165 | } -------------------------------------------------------------------------------- /DiscordBot/DiscordBot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net6.0 5 | 10 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /DiscordBot/Domain/ProfileData.cs: -------------------------------------------------------------------------------- 1 | using ImageMagick; 2 | 3 | namespace DiscordBot.Domain; 4 | 5 | public class ProfileData 6 | { 7 | public ulong UserId { get; set; } 8 | public string Nickname { get; set; } 9 | public string Username { get; set; } 10 | public uint XpTotal { get; set; } 11 | public uint XpRank { get; set; } 12 | public uint KarmaRank { get; set; } 13 | public uint Karma { get; set; } 14 | public uint Level { get; set; } 15 | public double XpLow { get; set; } 16 | public double XpHigh { get; set; } 17 | public uint XpShown { get; set; } 18 | public uint MaxXpShown { get; set; } 19 | public float XpPercentage { get; set; } 20 | public Color MainRoleColor { get; set; } 21 | public MagickImage Picture { get; set; } 22 | } -------------------------------------------------------------------------------- /DiscordBot/Domain/RectangleD.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Domain; 2 | 3 | public struct RectangleD 4 | { 5 | public double UpperLeftX; 6 | public double UpperLeftY; 7 | public double LowerRightX; 8 | public double LowerRightY; 9 | 10 | public RectangleD(double upperLeftX, double upperLeftY, double lowerRightX, double lowerRightY) 11 | { 12 | UpperLeftX = upperLeftX; 13 | UpperLeftY = upperLeftY; 14 | LowerRightX = lowerRightX; 15 | LowerRightY = lowerRightY; 16 | } 17 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/ChannelExtensions.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | 3 | namespace DiscordBot.Extensions; 4 | 5 | public static class ChannelExtensions 6 | { 7 | public static bool IsThreadInForumChannel(this IMessageChannel channel) 8 | { 9 | if (channel is not SocketThreadChannel threadChannel) 10 | return false; 11 | if (threadChannel.ParentChannel is not SocketForumChannel parentChannel) 12 | return false; 13 | return true; 14 | } 15 | 16 | public static bool IsThreadInChannel(this IMessageChannel channel, ulong channelId) 17 | { 18 | if (!channel.IsThreadInForumChannel()) 19 | return false; 20 | return ((SocketThreadChannel)channel).ParentChannel.Id == channelId; 21 | } 22 | 23 | public static bool IsPinned(this IThreadChannel channel) 24 | { 25 | return channel.Flags.HasFlag(ChannelFlags.Pinned); 26 | } 27 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/ContextExtension.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | 3 | namespace DiscordBot.Extensions; 4 | 5 | public static class ContextExtension 6 | { 7 | /// 8 | /// Sanity test to confirm a Context doesn't contain role or everyone mentions. 9 | /// 10 | /// Use `HasAnyPingableMention` to also include user mentions. 11 | public static bool HasRoleOrEveryoneMention(this ICommandContext context) 12 | { 13 | return context.Message.MentionedRoleIds.Count != 0 || context.Message.MentionedEveryone; 14 | } 15 | 16 | /// 17 | /// True if the context includes a RoleID, UserID or Mentions Everyone (Should include @here, unsure) 18 | /// 19 | /// Use `HasRoleOrEveryoneMention` to check for ONLY RoleIDs or Everyone mentions. 20 | public static bool HasAnyPingableMention(this ICommandContext context) 21 | { 22 | return context.Message.MentionedUserIds.Count > 0 || context.HasRoleOrEveryoneMention(); 23 | } 24 | 25 | /// 26 | /// True if the Context contains a message that is a reply and only mentions the user that sent the message. 27 | /// ie; the message is a reply to the user but doesn't contain any other mentions. 28 | /// 29 | public static bool IsOnlyReplyingToAuthor(this ICommandContext context) 30 | { 31 | if (!context.IsReply()) 32 | return false; 33 | if (context.Message.MentionedUserIds.Count != 1) 34 | return false; 35 | return context.Message.MentionedUserIds.First() == context.Message.ReferencedMessage.Author.Id; 36 | } 37 | 38 | /// 39 | /// Returns true if the Context has a reference to another message. 40 | /// ie; the message is a reply to another message. 41 | /// 42 | public static bool IsReply(this ICommandContext context) 43 | { 44 | return context.Message.ReferencedMessage != null; 45 | } 46 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/DBConnectionExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Common; 2 | using Insight.Database; 3 | 4 | namespace DiscordBot.Extensions; 5 | 6 | public static class DBConnectionExtension 7 | { 8 | public static async Task ColumnExists(this DbConnection connection, string tableName, string columnName) 9 | { 10 | // Execute the query `SHOW COLUMNS FROM `{tableName}` LIKE '{columnName}'` and check if any rows are returned 11 | var query = $"SHOW COLUMNS FROM `{tableName}` LIKE '{columnName}'"; 12 | var response = await connection.QuerySqlAsync(query); 13 | return response.Count > 0; 14 | } 15 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/DateExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Extensions; 2 | 3 | public static class DateExtensions 4 | { 5 | public static long ToUnixTimestamp(this DateTime date) 6 | { 7 | return (long)(date.ToUniversalTime().Subtract(new DateTime(1970, 1, 1))).TotalSeconds; 8 | } 9 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/EmbedBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Extensions; 2 | 3 | public static class EmbedBuilderExtension 4 | { 5 | 6 | public static EmbedBuilder FooterRequestedBy(this EmbedBuilder builder, IUser requestor) 7 | { 8 | builder.WithFooter( 9 | $"Requested by {requestor.GetUserPreferredName()}", 10 | requestor.GetAvatarUrl()); 11 | return builder; 12 | } 13 | 14 | public static EmbedBuilder FooterQuoteBy(this EmbedBuilder builder, IUser requestor, IChannel channel) 15 | { 16 | builder.WithFooter( 17 | $"Quoted by {requestor.GetUserPreferredName()}, • From channel #{channel.Name}", 18 | requestor.GetAvatarUrl()); 19 | return builder; 20 | } 21 | 22 | public static EmbedBuilder FooterInChannel(this EmbedBuilder builder, IChannel channel) 23 | { 24 | builder.WithFooter( 25 | $"In channel #{channel.Name}", null); 26 | return builder; 27 | } 28 | 29 | public static EmbedBuilder AddAuthor(this EmbedBuilder builder, IUser user, bool includeAvatar = true) 30 | { 31 | builder.WithAuthor( 32 | user.GetUserPreferredName(), 33 | includeAvatar ? user.GetAvatarUrl() : null); 34 | return builder; 35 | } 36 | 37 | public static EmbedBuilder AddAuthorWithAction(this EmbedBuilder builder, IUser user, string action, bool includeAvatar = true) 38 | { 39 | builder.WithAuthor( 40 | $"{user.GetUserPreferredName()} - {action}", 41 | includeAvatar ? user.GetAvatarUrl() : null); 42 | return builder; 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/InternetExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net; 3 | 4 | namespace DiscordBot.Extensions; 5 | 6 | public static class InternetExtensions 7 | { 8 | /// 9 | /// Loads a webpage and returns the contents as a string, Return an empty string on failure. 10 | /// 11 | public static async Task GetHttpContents(string uri) 12 | { 13 | try 14 | { 15 | var request = (HttpWebRequest)WebRequest.Create(uri); 16 | request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; 17 | 18 | using var response = (HttpWebResponse)await request.GetResponseAsync(); 19 | await using var stream = response.GetResponseStream(); 20 | using var reader = new StreamReader(stream); 21 | return await reader.ReadToEndAsync(); 22 | } 23 | catch (Exception e) 24 | { 25 | LoggingService.LogToConsole($"Error trying to load HTTP content.\rER: {e.Message}", Discord.LogSeverity.Warning); 26 | return string.Empty; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/MessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace DiscordBot.Extensions; 4 | 5 | public static class MessageExtensions 6 | { 7 | private const string InviteLinkPattern = @"(https?:\/\/)?(www\.)?(discord\.gg\/[a-zA-Z0-9]+)"; 8 | 9 | public static async Task TrySendMessage(this IDMChannel channel, string message = "", Embed embed = null) 10 | { 11 | try 12 | { 13 | await channel.SendMessageAsync(message, embed: embed); 14 | } 15 | catch (Exception) 16 | { 17 | return false; 18 | } 19 | return true; 20 | } 21 | 22 | /// 23 | /// Returns true if the message includes any RoleID's, UserID's or Mentions Everyone 24 | /// 25 | public static bool HasAnyPingableMention(this IUserMessage message) 26 | { 27 | return message.MentionedUserIds.Count > 0 || message.MentionedRoleIds.Count > 0 || message.MentionedEveryone; 28 | } 29 | 30 | /// 31 | /// Returns true if the message contains any discord invite links, ie; discord.gg/invite 32 | /// 33 | public static bool ContainsInviteLink(this IUserMessage message) 34 | { 35 | return Regex.IsMatch(message.Content, InviteLinkPattern, RegexOptions.IgnoreCase); 36 | } 37 | 38 | /// 39 | /// Returns true if the message contains any discord invite links, ie; discord.gg/invite 40 | /// 41 | public static bool ContainsInviteLink(this string message) 42 | { 43 | return Regex.IsMatch(message, InviteLinkPattern, RegexOptions.IgnoreCase); 44 | } 45 | 46 | /// 47 | /// Returns true if the message contains any discord invite links, ie; discord.gg/invite 48 | /// 49 | public static bool ContainsInviteLink(this IMessage message) 50 | { 51 | return Regex.IsMatch(message.Content, InviteLinkPattern, RegexOptions.IgnoreCase); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/ReactMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Settings; 2 | 3 | namespace DiscordBot.Extensions; 4 | 5 | public static class ReactMessageExtensions 6 | { 7 | public static string MessageLinkBack(this UserReactMessage message, ulong guildId) 8 | { 9 | if (message == null) return ""; 10 | return $"https://discordapp.com/channels/{guildId.ToString()}/{message.ChannelId.ToString()}/{message.MessageId.ToString()}"; 11 | } 12 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System.Text; 3 | using System.Text.RegularExpressions; 4 | using DiscordBot.Properties; 5 | 6 | namespace DiscordBot.Extensions; 7 | 8 | public static class StringExtensions 9 | { 10 | public static string Truncate(this string value, int maxLength) 11 | { 12 | if (string.IsNullOrEmpty(value)) return value; 13 | return value.Length <= maxLength ? value : value.Substring(0, maxLength); 14 | } 15 | 16 | public static List MessageSplit(this string str, int maxLength = 1990) 17 | { 18 | var list = str.Split('\n').ToList(); 19 | var ret = new List(); 20 | 21 | var currentString = string.Empty; 22 | foreach (var s in list) 23 | if (currentString.Length + s.Length < maxLength) 24 | currentString += s + "\n"; 25 | else 26 | { 27 | ret.Add(currentString); 28 | currentString = s + "\n"; 29 | } 30 | 31 | if (!string.IsNullOrEmpty(currentString)) 32 | ret.Add(currentString); 33 | 34 | return ret; 35 | } 36 | 37 | public static List MessageSplitToSize(this string str, 38 | int maxLength = Constants.MaxLengthChannelMessage) 39 | { 40 | var container = new List(); 41 | if (str.Length < Constants.MaxLengthChannelMessage) 42 | { 43 | container.Add(str); 44 | return container; 45 | } 46 | 47 | var cuts = (str.Length / Constants.MaxLengthChannelMessage) + 1; 48 | var indexOfLine = 0; 49 | for (var cut = 1; cut <= cuts; cut++) 50 | { 51 | string page; 52 | page = cut == cuts ? str.Substring(indexOfLine) : str.Substring(indexOfLine, Constants.MaxLengthChannelMessage); 53 | 54 | indexOfLine = page.LastIndexOf("\n", StringComparison.Ordinal) + 1; 55 | container.Add(cut == cuts ? page : page.Remove(indexOfLine - 1)); 56 | } 57 | return container; 58 | } 59 | 60 | /// 61 | /// Adds a backslash behind each special character used by Discord to make a message appear plain-text. 62 | /// 63 | /// 64 | /// 65 | public static string EscapeDiscordMarkup(this string content) => Regex.Replace(content, @"([\\~\\_\`\*\`])", "\\$1"); 66 | 67 | public static int CalculateLevenshteinDistance(this string source1, string source2) //O(n*m) 68 | { 69 | var source1Length = source1.Length; 70 | var source2Length = source2.Length; 71 | 72 | var matrix = new int[source1Length + 1, source2Length + 1]; 73 | 74 | // First calculation, if one entry is empty return full length 75 | if (source1Length == 0) 76 | return source2Length; 77 | 78 | if (source2Length == 0) 79 | return source1Length; 80 | 81 | // Initialization of matrix with row size source1Length and columns size source2Length 82 | for (var i = 0; i <= source1Length; matrix[i, 0] = i++) ; 83 | for (var j = 0; j <= source2Length; matrix[0, j] = j++) ; 84 | 85 | // Calculate rows and collumns distances 86 | for (var i = 1; i <= source1Length; i++) 87 | { 88 | for (var j = 1; j <= source2Length; j++) 89 | { 90 | var cost = source2[j - 1] == source1[i - 1] ? 0 : 1; 91 | 92 | matrix[i, j] = Math.Min( 93 | Math.Min(matrix[i - 1, j] + 1, matrix[i, j - 1] + 1), 94 | matrix[i - 1, j - 1] + cost); 95 | } 96 | } 97 | 98 | // return result 99 | return matrix[source1Length, source2Length]; 100 | } 101 | 102 | public static string AsCodeBlock(this string code, string language = "cs") => Resources.DiscordCodeBlock.Replace("{code}", code).Replace("{language}", language); 103 | 104 | public static string GetSha256(this string input) 105 | { 106 | var hash = new SHA256CryptoServiceProvider(); 107 | // Convert the input string to a byte array and compute the hash. 108 | var data = hash.ComputeHash(Encoding.UTF8.GetBytes(input)); 109 | 110 | // Create a new Stringbuilder to collect the bytes 111 | // and create a string. 112 | var sb = new StringBuilder(); 113 | 114 | // Loop through each byte of the hashed data 115 | // and format each one as a hexadecimal string. 116 | for (var i = 0; i < data.Length; i++) sb.Append(data[i].ToString("x2")); 117 | 118 | // Return the hexadecimal string. 119 | return sb.ToString(); 120 | } 121 | 122 | /// 123 | /// Returns true if the string contains only upper case characters, including spaces and all punctuation ie; "I NEED HELP!?!?!?!#$?!" will return true 124 | /// 125 | public static bool IsAlLCaps(this string str) 126 | { 127 | return Regex.IsMatch(str, @"^[A-Z\s\p{P}]+$"); 128 | } 129 | 130 | public static string ToCapitalizeFirstLetter(this string str) 131 | { 132 | if (string.IsNullOrEmpty(str)) 133 | return string.Empty; 134 | return char.ToUpper(str[0]) + str[1..]; 135 | } 136 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Extensions; 2 | 3 | public static class TaskExtensions 4 | { 5 | public static Task DeleteAfterTime(this IDeletable message, int seconds = 0, int minutes = 0, int hours = 0, int days = 0) => message?.DeleteAfterTimeSpan(new TimeSpan(days, hours, minutes, seconds)); 6 | public static Task DeleteAfterSeconds(this IDeletable message, double seconds) => message?.DeleteAfterTimeSpan(TimeSpan.FromSeconds(seconds)); 7 | 8 | public static Task DeleteAfterTimeSpan(this IDeletable message, TimeSpan timeSpan) 9 | { 10 | return Task.Delay(timeSpan).ContinueWith(async _ => 11 | { 12 | if (message != null) await message?.DeleteAsync(); 13 | }); 14 | } 15 | 16 | public static Task DeleteAfterTime(this Task task, int seconds = 0, int minutes = 0, int hours = 0, int days = 0, bool awaitDeletion = false) where T : IDeletable => task?.DeleteAfterTimeSpan(new TimeSpan(days, hours, minutes, seconds), awaitDeletion); 17 | public static Task DeleteAfterSeconds(this Task task, double seconds, bool awaitDeletion = false) where T : IDeletable => task?.DeleteAfterTimeSpan(TimeSpan.FromSeconds(seconds), awaitDeletion); 18 | 19 | public static Task DeleteAfterTimeSpan(this Task task, TimeSpan timeSpan, bool awaitDeletion = false) where T : IDeletable 20 | { 21 | var deletion = Task.Run(async () => await (await task)?.DeleteAfterTimeSpan(timeSpan)); 22 | return awaitDeletion ? deletion : task; 23 | } 24 | 25 | public static Task RemoveAfterSeconds(this ICollection list, T val, double seconds) => list.RemoveAfterTimeSpan(val, TimeSpan.FromSeconds(seconds)); 26 | 27 | public static Task RemoveAfterTimeSpan(this ICollection list, T val, TimeSpan timeSpan) 28 | { 29 | return Task.Delay(timeSpan).ContinueWith(_ => list.Remove(val)); 30 | } 31 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/UserDBRepository.cs: -------------------------------------------------------------------------------- 1 | using Insight.Database; 2 | 3 | namespace DiscordBot.Extensions; 4 | 5 | public class ServerUser 6 | { 7 | // ReSharper disable once InconsistentNaming 8 | public string UserID { get; set; } 9 | public uint Karma { get; set; } 10 | public uint KarmaWeekly { get; set; } 11 | public uint KarmaMonthly { get; set; } 12 | public uint KarmaYearly { get; set; } 13 | public uint KarmaGiven { get; set; } 14 | public ulong Exp { get; set; } 15 | public uint Level { get; set; } 16 | // DefaultCity - Optional Location for Weather, BDay, Temp, Time, etc. (Added - Jan 2024) 17 | public string DefaultCity { get; set; } = string.Empty; 18 | } 19 | 20 | /// 21 | /// Table Properties for ServerUser. Intended to be used with IServerUserRepo and enforce consistency and reduce errors. 22 | /// 23 | public static class UserProps 24 | { 25 | public const string TableName = "users"; 26 | 27 | public const string UserID = nameof(ServerUser.UserID); 28 | public const string Karma = nameof(ServerUser.Karma); 29 | public const string KarmaWeekly = nameof(ServerUser.KarmaWeekly); 30 | public const string KarmaMonthly = nameof(ServerUser.KarmaMonthly); 31 | public const string KarmaYearly = nameof(ServerUser.KarmaYearly); 32 | public const string KarmaGiven = nameof(ServerUser.KarmaGiven); 33 | public const string Exp = nameof(ServerUser.Exp); 34 | public const string Level = nameof(ServerUser.Level); 35 | public const string DefaultCity = nameof(ServerUser.DefaultCity); 36 | } 37 | 38 | public interface IServerUserRepo 39 | { 40 | [Sql($@" 41 | INSERT INTO {UserProps.TableName} ({UserProps.UserID}) VALUES (@{UserProps.UserID}); 42 | SELECT * FROM {UserProps.TableName} WHERE {UserProps.UserID} = @{UserProps.UserID}")] 43 | Task InsertUser(ServerUser user); 44 | [Sql($"DELETE FROM {UserProps.TableName} WHERE {UserProps.UserID} = @userId")] 45 | Task RemoveUser(string userId); 46 | 47 | [Sql($"SELECT * FROM {UserProps.TableName} WHERE {UserProps.UserID} = @userId")] 48 | Task GetUser(string userId); 49 | 50 | #region Ranks 51 | 52 | [Sql($"SELECT {UserProps.UserID}, {UserProps.Karma}, {UserProps.Level}, {UserProps.Exp} FROM {UserProps.TableName} ORDER BY {UserProps.Level} DESC, RAND() LIMIT @n")] 53 | Task> GetTopLevel(int n); 54 | [Sql($"SELECT {UserProps.UserID}, {UserProps.Karma}, {UserProps.KarmaGiven} FROM {UserProps.TableName} ORDER BY {UserProps.Karma} DESC, RAND() LIMIT @n")] 55 | Task> GetTopKarma(int n); 56 | [Sql($"SELECT {UserProps.UserID}, {UserProps.KarmaWeekly} FROM {UserProps.TableName} ORDER BY {UserProps.KarmaWeekly} DESC, RAND() LIMIT @n")] 57 | Task> GetTopKarmaWeekly(int n); 58 | [Sql($"SELECT {UserProps.UserID}, {UserProps.KarmaMonthly} FROM {UserProps.TableName} ORDER BY {UserProps.KarmaMonthly} DESC, RAND() LIMIT @n")] 59 | Task> GetTopKarmaMonthly(int n); 60 | [Sql($"SELECT {UserProps.UserID}, {UserProps.KarmaYearly} FROM {UserProps.TableName} ORDER BY {UserProps.KarmaYearly} DESC, RAND() LIMIT @n")] 61 | Task> GetTopKarmaYearly(int n); 62 | [Sql($"SELECT COUNT({UserProps.UserID})+1 FROM {UserProps.TableName} WHERE {UserProps.Level} > @level")] 63 | Task GetLevelRank(string userId, uint level); 64 | [Sql($"SELECT COUNT({UserProps.UserID})+1 FROM {UserProps.TableName} WHERE {UserProps.Karma} > @karma")] 65 | Task GetKarmaRank(string userId, uint karma); 66 | 67 | #endregion // Ranks 68 | 69 | #region Update Values 70 | 71 | [Sql($"UPDATE {UserProps.TableName} SET {UserProps.Karma} = @karma WHERE {UserProps.UserID} = @userId")] 72 | Task UpdateKarma(string userId, uint karma); 73 | [Sql($"UPDATE {UserProps.TableName} SET {UserProps.Karma} = {UserProps.Karma} + 1, {UserProps.KarmaWeekly} = {UserProps.KarmaWeekly} + 1, {UserProps.KarmaMonthly} = {UserProps.KarmaMonthly} + 1, {UserProps.KarmaYearly} = {UserProps.KarmaYearly} + 1 WHERE {UserProps.UserID} = @userId")] 74 | Task IncrementKarma(string userId); 75 | [Sql($"UPDATE {UserProps.TableName} SET {UserProps.KarmaGiven} = @karmaGiven WHERE {UserProps.UserID} = @userId")] 76 | Task UpdateKarmaGiven(string userId, uint karmaGiven); 77 | [Sql($"UPDATE {UserProps.TableName} SET {UserProps.Exp} = @xp WHERE {UserProps.UserID} = @userId")] 78 | Task UpdateXp(string userId, ulong xp); 79 | [Sql($"UPDATE {UserProps.TableName} SET {UserProps.Level} = @level WHERE {UserProps.UserID} = @userId")] 80 | Task UpdateLevel(string userId, uint level); 81 | [Sql($"UPDATE {UserProps.TableName} SET {UserProps.DefaultCity} = @city WHERE {UserProps.UserID} = @userId")] 82 | Task UpdateDefaultCity(string userId, string city); 83 | 84 | #endregion // Update Values 85 | 86 | #region Get Single Values 87 | 88 | [Sql($"SELECT {UserProps.Karma} FROM {UserProps.TableName} WHERE {UserProps.UserID} = @userId")] 89 | Task GetKarma(string userId); 90 | [Sql($"SELECT {UserProps.KarmaGiven} FROM {UserProps.TableName} WHERE {UserProps.UserID} = @userId")] 91 | Task GetKarmaGiven(string userId); 92 | [Sql($"SELECT {UserProps.Exp} FROM {UserProps.TableName} WHERE {UserProps.UserID} = @userId")] 93 | Task GetXp(string userId); 94 | [Sql($"SELECT {UserProps.Level} FROM {UserProps.TableName} WHERE {UserProps.UserID} = @userId")] 95 | Task GetLevel(string userId); 96 | [Sql($"SELECT {UserProps.DefaultCity} FROM {UserProps.TableName} WHERE {UserProps.UserID} = @userId")] 97 | Task GetDefaultCity(string userId); 98 | 99 | #endregion // Get Single Values 100 | 101 | /// Returns a count of {Props.TableName} in the Table, otherwise it fails. 102 | [Sql($"SELECT COUNT(*) FROM {UserProps.TableName}")] 103 | Task TestConnection(); 104 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/UserExtensions.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | 3 | namespace DiscordBot.Extensions; 4 | 5 | public static class UserExtensions 6 | { 7 | public static bool IsUserBotOrWebhook(this IUser user) 8 | { 9 | return user.IsBot || user.IsWebhook; 10 | } 11 | 12 | public static bool HasRoleGroup(this IUser user, SocketRole role) 13 | { 14 | return HasRoleGroup(user, role.Id); 15 | } 16 | public static bool HasRoleGroup(this IUser user, ulong roleId) 17 | { 18 | var guildUser = user as IGuildUser; 19 | if (guildUser == null) 20 | return false; 21 | return guildUser.RoleIds.Any(x => x == roleId); 22 | } 23 | 24 | // Returns the users DisplayName (nickname) if it exists, otherwise returns the username 25 | public static string GetUserPreferredName(this IUser user) 26 | { 27 | var guildUser = user as SocketGuildUser; 28 | return guildUser?.DisplayName ?? user.Username; 29 | } 30 | 31 | public static string GetPreferredAndUsername(this IUser user) 32 | { 33 | var guildUser = user as SocketGuildUser; 34 | if (guildUser == null) 35 | return user.Username; 36 | if (string.Equals(guildUser.DisplayName, user.Username, StringComparison.CurrentCultureIgnoreCase)) 37 | return guildUser.DisplayName; 38 | return $"{guildUser.DisplayName} (aka {user.Username})"; 39 | } 40 | 41 | public static string GetUserLoggingString(this IUser user) 42 | { 43 | var guildUser = user as SocketGuildUser; 44 | if (guildUser == null) 45 | return $"{user.Username} `{user.Id}`"; 46 | return $"{guildUser.GetPreferredAndUsername()} `{guildUser.Id}`"; 47 | } 48 | } -------------------------------------------------------------------------------- /DiscordBot/Extensions/UserServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Extensions; 2 | 3 | public static class UserServiceExtensions 4 | { 5 | /// 6 | /// Safely converts a double to an int without running into exceptions. Number will be reduced to limits of int value. 7 | /// 8 | /// 9 | /// 10 | public static int ToInt(this double val) 11 | { 12 | if (val > int.MaxValue) return int.MaxValue; 13 | 14 | if (val < int.MinValue) return int.MinValue; 15 | 16 | return (int)val; 17 | } 18 | 19 | #region Cooldown related 20 | 21 | /// 22 | /// Checks to see if user is on this cooldown list. User is automatically removed from list if their time is up and 23 | /// will return false. 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | public static bool HasUser(this Dictionary cooldowns, ulong userId, bool evenIfCooldownNowOver = false) 30 | { 31 | if (cooldowns.ContainsKey(userId)) 32 | { 33 | if (cooldowns[userId] > DateTime.Now) return true; 34 | 35 | cooldowns.Remove(userId); 36 | 37 | if (evenIfCooldownNowOver) 38 | return true; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | /// 45 | /// Adds user to cooldown list with given amount of time. If user already on list, time is added to existing time. 46 | /// 47 | /// 48 | /// 49 | /// 50 | /// 51 | /// 52 | /// 53 | /// Sets the cooldown time absolutely, instead of adding to existing. 54 | public static void AddCooldown(this Dictionary cooldowns, ulong userId, int seconds = 0, int minutes = 0, 55 | int hours = 0, int days = 0, bool ignoreExisting = false) 56 | { 57 | var cooldownTime = new TimeSpan(days, hours, minutes, seconds); 58 | 59 | if (cooldowns.HasUser(userId)) 60 | { 61 | if (ignoreExisting) 62 | { 63 | cooldowns[userId] = DateTime.Now.Add(cooldownTime); 64 | return; 65 | } 66 | 67 | cooldowns[userId] = cooldowns[userId].Add(cooldownTime); 68 | return; 69 | } 70 | 71 | cooldowns.Add(userId, DateTime.Now.Add(cooldownTime)); 72 | } 73 | 74 | /// 75 | /// Set a max days (permanent) cooldown for the given user, or removes the permanent cooldown if set false. 76 | /// 77 | /// 78 | /// 79 | /// Set to true for permanent, or false to remove it. 80 | public static void SetPermanent(this Dictionary cooldowns, ulong userId, bool enabled) 81 | { 82 | cooldowns.AddCooldown(userId, days: enabled ? 9999 : 0, ignoreExisting: true); 83 | } 84 | 85 | public static bool IsPermanent(this Dictionary cooldowns, ulong userId) => cooldowns.Days(userId) > 5000; 86 | 87 | public static double Days(this Dictionary cooldowns, ulong userId) => cooldowns.HasUser(userId) ? cooldowns[userId].Subtract(DateTime.Now).TotalDays : 0; 88 | 89 | public static double Hours(this Dictionary cooldowns, ulong userId) => cooldowns.HasUser(userId) ? cooldowns[userId].Subtract(DateTime.Now).TotalHours : 0; 90 | 91 | public static double Minutes(this Dictionary cooldowns, ulong userId) => cooldowns.HasUser(userId) ? cooldowns[userId].Subtract(DateTime.Now).TotalMinutes : 0; 92 | 93 | public static double Seconds(this Dictionary cooldowns, ulong userId) => cooldowns.HasUser(userId) ? cooldowns[userId].Subtract(DateTime.Now).TotalSeconds : 0; 94 | 95 | public static double Milliseconds(this Dictionary cooldowns, ulong userId) => cooldowns.HasUser(userId) ? cooldowns[userId].Subtract(DateTime.Now).TotalMilliseconds : 0; 96 | 97 | /// 98 | /// Returns when the cooldown list no-longer contains the user. 99 | /// 100 | /// 101 | /// 102 | /// 103 | public static async Task AwaitCooldown(this Dictionary cooldowns, ulong userId) 104 | { 105 | while (cooldowns.HasUser(userId)) 106 | { 107 | var delay = cooldowns.Milliseconds(userId).ToInt() + 100; 108 | if (delay > 0) 109 | await Task.Delay(delay); 110 | else 111 | await Task.Delay(1000); 112 | } 113 | } 114 | 115 | #endregion 116 | } -------------------------------------------------------------------------------- /DiscordBot/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Design", "RCS1090:Call 'ConfigureAwait(false)'.", Justification = "")] -------------------------------------------------------------------------------- /DiscordBot/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | // Global using, these should be namespaces that are used very frequently 2 | // Native namespace 3 | global using System; 4 | global using System.Collections.Generic; 5 | global using System.Linq; 6 | global using System.Threading; 7 | global using System.Threading.Tasks; 8 | 9 | // Other libraries 10 | global using Discord; 11 | 12 | // Our code 13 | global using DiscordBot.Extensions; 14 | global using DiscordBot.Services.Logging; -------------------------------------------------------------------------------- /DiscordBot/Modules/AirportModule.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using DiscordBot.Modules.Weather; 3 | using DiscordBot.Services; 4 | using DiscordBot.Settings; 5 | 6 | namespace DiscordBot.Modules; 7 | 8 | // Allows UserModule !help to show commands from this module 9 | [Group("UserModule"), Alias("")] 10 | public class AirportModule : ModuleBase 11 | { 12 | #region Dependency Injection 13 | 14 | public AirportService AirportService { get; set; } 15 | public BotSettings Settings { get; set; } 16 | // Needed to locate cities lon/lat easier 17 | public WeatherService WeatherService { get; set; } 18 | 19 | #endregion // Dependency Injection 20 | 21 | #region API Results 22 | 23 | public class FlightResults 24 | { 25 | public string iata { get; set; } 26 | public string fs { get; set; } 27 | public string name { get; set; } 28 | } 29 | 30 | public class FlightRoot 31 | { 32 | public List data { get; set; } 33 | } 34 | 35 | #endregion // API Results 36 | 37 | #region Commands 38 | 39 | [Command("Fly")] 40 | [Summary("Fly to a city")] 41 | public async Task FlyTo(string from, string to) 42 | { 43 | // Make sure command is in Bot-Commands or OffTopic 44 | if (Context.Channel.Id != Settings.BotCommandsChannel.Id && Context.Channel.Id != Settings.GeneralChannel.Id) 45 | { 46 | await ReplyAsync($"Command can only be used in <#{Settings.BotCommandsChannel.Id}> or <#{Settings.GeneralChannel.Id}>.").DeleteAfterSeconds(5f); 47 | await Context.Message.DeleteAfterSeconds(2f); 48 | return; 49 | } 50 | 51 | EmbedBuilder embed = new(); 52 | embed.Title = "Flight Finder"; 53 | 54 | embed.Description = "Finding cities"; 55 | var msg = await ReplyAsync(string.Empty, false, embed.Build()); 56 | 57 | // Use Weather API to get lon/lat of cities 58 | var fromCity = await GetCity(from, embed, msg); 59 | if (fromCity == null) 60 | return; 61 | var toCity = await GetCity(to, embed, msg); 62 | if (toCity == null) 63 | return; 64 | 65 | // Find closest Airport using AirLabs API 66 | embed.Description = "Finding airports"; 67 | await msg.ModifyAsync(x => x.Embed = embed.Build()); 68 | 69 | var fromAirport = await GetAirport(fromCity, embed, msg); 70 | if (fromAirport == null) 71 | return; 72 | var toAirport = await GetAirport(toCity, embed, msg); 73 | if (toAirport == null) 74 | return; 75 | 76 | // Find cheapest flight using GetFlightInfo 77 | embed.Description = $"Searching {fromAirport.name} to {toAirport.name}"; 78 | await msg.ModifyAsync(x => x.Embed = embed.Build()); 79 | 80 | var daysUntilTuesday = (int)DateTime.Now.DayOfWeek - 2; 81 | if (daysUntilTuesday < 0) 82 | daysUntilTuesday += 7; 83 | 84 | var flights = await AirportService.GetFlightInfo(fromAirport.iata_code, toAirport.iata_code, daysUntilTuesday); 85 | if (flights == null) 86 | { 87 | embed.Description += "\\nNo flights found, sorry."; 88 | await msg.ModifyAsync(x => x.Embed = embed.Build()); 89 | await msg.DeleteAfterSeconds(30f); 90 | return; 91 | } 92 | 93 | var flight = flights[0]; 94 | 95 | var itinerary = flight.itineraries.First(); 96 | var numberOfStops = itinerary.segments.Count - 1; 97 | var departTime = itinerary.segments.First().departure; 98 | var arriveTime = itinerary.segments.Last().arrival; 99 | 100 | var totalDuration = itinerary.duration.Replace("PT", string.Empty); 101 | 102 | embed.Title += $" - {fromAirport.iata_code} to {toAirport.iata_code} | {flight.price.total} {flight.price.currency}"; 103 | embed.ThumbnailUrl = $"https://countryflagsapi.com/png/{toAirport.country_code}"; 104 | embed.Description = $"{fromAirport.name} to {toAirport.name}"; 105 | embed.Description += $"\nDuration: {totalDuration}, with {(numberOfStops > 1 ? "at least" : string.Empty)} {numberOfStops} stop{(numberOfStops != 1 ? "s" : string.Empty)}."; 106 | // embed.Description += 107 | // $"\nSeats remaining: {flight.numberOfBookableSeats}, Bags: {(flight.pricingOptions.includedCheckedBagsOnly ? "Y" : "N")}, OneWay: {(flight.oneWay ? "Y" : "N")}"; 108 | embed.Description += $"\nDepart: {departTime.at:dd/MM/yy HH:MM}, Arrive: {arriveTime.at:dd/MM/yy HH:MM}"; 109 | 110 | // string price = $"Base: {flight.price.@base}"; 111 | // foreach (var fee in flight.price.fees) 112 | // { 113 | // if (float.Parse(fee.amount) > 0) 114 | // { 115 | // price += $"\n{fee.type}: {fee.amount}"; 116 | // } 117 | // } 118 | // embed.AddField($"Total Price ({flight.price.grandTotal} {flight.price.currency})", price); 119 | 120 | await msg.ModifyAsync(x => x.Embed = embed.Build()); 121 | } 122 | 123 | #endregion // Commands 124 | 125 | #region Utility Methods 126 | 127 | private async Task GetCity(string city, EmbedBuilder embed, IUserMessage msg) 128 | { 129 | var cityResult = await WeatherService.GetWeather(city); 130 | if (cityResult == null) 131 | { 132 | embed.Description += $"\n{city} could not be found."; 133 | await msg.ModifyAsync(x => x.Embed = embed.Build()); 134 | await msg.DeleteAfterSeconds(10f); 135 | return null; 136 | } 137 | return cityResult; 138 | } 139 | 140 | private async Task GetAirport(WeatherContainer.Result weather, EmbedBuilder embed, IUserMessage msg) 141 | { 142 | var airportResult = await AirportService.GetClosestAirport(weather.coord.Lat, weather.coord.Lon); 143 | if (airportResult == null) 144 | { 145 | embed.Description += $"\nAirport near {weather.name} ({weather.sys.country}) could not be found."; 146 | await msg.ModifyAsync(x => x.Embed = embed.Build()); 147 | await msg.DeleteAfterSeconds(10f); 148 | return null; 149 | } 150 | return airportResult; 151 | } 152 | 153 | #endregion // Utility Methods 154 | 155 | } -------------------------------------------------------------------------------- /DiscordBot/Modules/TicketModule.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using DiscordBot.Attributes; 3 | using DiscordBot.Services; 4 | using DiscordBot.Settings; 5 | 6 | // ReSharper disable all UnusedMember.Local 7 | namespace DiscordBot.Modules; 8 | 9 | public class TicketModule : ModuleBase 10 | { 11 | #region Dependency Injection 12 | 13 | public CommandHandlingService CommandHandlingService { get; set; } 14 | public BotSettings Settings { get; set; } 15 | 16 | #endregion 17 | 18 | /// 19 | /// Creates a private channel only accessable by the mods, admins, and the user who used the command. 20 | /// One command, no args, simple. 21 | /// 22 | [Command("Complain"), Alias("complains", "complaint"), Summary("Opens a private channel to complain.")] 23 | public async Task Complaint() 24 | { 25 | await Context.Message.DeleteAsync(); 26 | 27 | var categoryExist = (await Context.Guild.GetCategoriesAsync()).Any(category => category.Id == Settings.ComplaintCategoryId); 28 | 29 | var hash = Context.User.Id.ToString().GetSha256().Substring(0, 8); 30 | var channelName = ParseToDiscordChannel($"{Settings.ComplaintChannelPrefix}-{hash}"); 31 | 32 | var channels = await Context.Guild.GetChannelsAsync(); 33 | // Check if channel with same name already exist in the Complaint Category (if it exists). 34 | if (channels.Any(channel => channel.Name == channelName && (!categoryExist || ((INestedChannel)channel).CategoryId == Settings.ComplaintCategoryId))) 35 | { 36 | await ReplyAsync($"{Context.User.Mention}, you already have an open complaint! Please use that channel!") 37 | .DeleteAfterSeconds(15); 38 | return; 39 | } 40 | 41 | var newChannel = await Context.Guild.CreateTextChannelAsync(channelName, x => 42 | { 43 | if (categoryExist) x.CategoryId = Settings.ComplaintCategoryId; 44 | }); 45 | 46 | var userPerms = new OverwritePermissions(viewChannel: PermValue.Allow); 47 | var modRole = Context.Guild.Roles.First(r => r.Id == Settings.ModeratorRoleId); 48 | await newChannel.AddPermissionOverwriteAsync(Context.Guild.EveryoneRole, new OverwritePermissions(viewChannel: PermValue.Deny)); 49 | await newChannel.AddPermissionOverwriteAsync(Context.User, userPerms); 50 | await newChannel.AddPermissionOverwriteAsync(modRole, userPerms); 51 | await newChannel.AddPermissionOverwriteAsync(Context.Client.CurrentUser, userPerms); 52 | 53 | await newChannel.SendMessageAsync( 54 | $"The content of this conversation will stay strictly between you {Context.User.Mention} and the {modRole.Mention}.\n" + 55 | "Please stay civil, any insults or offensive language could see you punished.\n" + 56 | "Do not ping anyone and wait until a staff member is free to examine your complaint."); 57 | await newChannel.SendMessageAsync("A staff member will be able to close this chat by doing `!ticket close`."); 58 | } 59 | 60 | /// 61 | /// Archives the ticket. 62 | /// 63 | [Command("Ticket Close"), Alias("Ticket end", "Ticket done", "Ticket archive"), Summary("Closes the ticket.")] 64 | [RequireModerator] 65 | public async Task Close() 66 | { 67 | await Context.Message.DeleteAsync(); 68 | 69 | if (!Context.Channel.Name.StartsWith(Settings.ComplaintChannelPrefix.ToLower())) return; 70 | 71 | var categoryExist = (await Context.Guild.GetCategoriesAsync()).Any(category => category.Id == Settings.ClosedComplaintCategoryId); 72 | 73 | var currentChannel = await Context.Guild.GetChannelAsync(Context.Channel.Id); 74 | 75 | // Remove the override permissions for the user who opened the complaint. 76 | foreach (var a in currentChannel.PermissionOverwrites) 77 | { 78 | if (a.TargetType != PermissionTarget.User) continue; 79 | 80 | var user = await Context.Guild.GetUserAsync(a.TargetId); 81 | await currentChannel.RemovePermissionOverwriteAsync(user); 82 | } 83 | 84 | var newName = Settings.ClosedComplaintChannelPrefix + currentChannel.Name; 85 | await currentChannel.ModifyAsync(x => 86 | { 87 | if (categoryExist) x.CategoryId = Settings.ClosedComplaintCategoryId; 88 | x.Name = newName; 89 | }); 90 | } 91 | 92 | /// 93 | /// Delete the ticket. 94 | /// 95 | [Command("Ticket Delete"), Summary("Deletes the ticket.")] 96 | [RequireAdmin] 97 | private async Task Delete() 98 | { 99 | await Context.Message.DeleteAsync(); 100 | 101 | if (Context.Channel.Name.StartsWith(Settings.ComplaintChannelPrefix.ToLower()) || 102 | Context.Channel.Name.StartsWith(Settings.ClosedComplaintChannelPrefix.ToLower())) 103 | { 104 | await Context.Guild.GetChannelAsync(Context.Channel.Id).Result.DeleteAsync(); 105 | } 106 | } 107 | 108 | private string ParseToDiscordChannel(string channelName) => channelName.ToLower().Replace(" ", "-"); 109 | 110 | #region CommandList 111 | [RequireModerator] 112 | [Summary("Does what you see now.")] 113 | [Command("Ticket Help")] 114 | public async Task TicketHelp() 115 | { 116 | foreach (var message in CommandHandlingService.GetCommandListMessages("TicketModule", true, true, false)) 117 | { 118 | await ReplyAsync(message); 119 | } 120 | } 121 | #endregion 122 | } -------------------------------------------------------------------------------- /DiscordBot/Modules/TipModule.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Discord.Commands; 3 | using DiscordBot.Attributes; 4 | using DiscordBot.Services; 5 | using DiscordBot.Services.Tips; 6 | using DiscordBot.Services.Tips.Components; 7 | using DiscordBot.Settings; 8 | 9 | // ReSharper disable all UnusedMember.Local 10 | namespace DiscordBot.Modules; 11 | 12 | public class TipModule : ModuleBase 13 | { 14 | #region Dependency Injection 15 | 16 | public CommandHandlingService CommandHandlingService { get; set; } 17 | public BotSettings Settings { get; set; } 18 | public TipService TipService { get; set; } 19 | 20 | #endregion 21 | 22 | [Command("Tip")] 23 | [Summary("Find and provide pre-authored tips (images or text) by their keywords.")] 24 | /* for now */ [RequireModerator] /* maybe Helper too */ 25 | public async Task Tip(string keywords) 26 | { 27 | var tips = TipService.GetTips(keywords); 28 | if (tips.Count == 0) 29 | { 30 | await ReplyAsync("No tips for the keywords provided were found."); 31 | return; 32 | } 33 | 34 | foreach (var tip in tips) 35 | tip.Requests++; 36 | 37 | var isAnyTextTips = tips.Any(tip => !string.IsNullOrEmpty(tip.Content)); 38 | var builder = new EmbedBuilder(); 39 | if (isAnyTextTips) 40 | { 41 | // Loop through tips in order, have dot point list of the .Content property in an embed 42 | builder 43 | .WithTitle("Tip List") 44 | .WithDescription("Here are the tips for your keywords:"); 45 | foreach (var tip in tips) 46 | { 47 | builder.AddField(tip.Keywords.Count == 1 ? tip.Keywords[0] : "Multiple Keywords", tip.Content); 48 | } 49 | } 50 | 51 | var attachments = tips 52 | .Where(tip => tip.ImagePaths != null && tip.ImagePaths.Any()) 53 | .SelectMany(tip => tip.ImagePaths) 54 | .Select(imagePath => new FileAttachment(TipService.GetTipPath(imagePath))) 55 | .ToList(); 56 | 57 | if (attachments.Count > 0) 58 | { 59 | if (isAnyTextTips) 60 | { 61 | await Context.Channel.SendFilesAsync(attachments, embed: builder.Build()); 62 | } 63 | else 64 | { 65 | await Context.Channel.SendFilesAsync(attachments); 66 | } 67 | } 68 | else 69 | { 70 | await ReplyAsync(embed: builder.Build()); 71 | } 72 | 73 | var ids = string.Join(" ", tips.Select(t => t.Id.ToString()).ToArray()); 74 | await ReplyAsync($"-# Tip ID {ids}"); 75 | await Context.Message.DeleteAsync(); 76 | await TipService.CommitTipDatabase(); 77 | } 78 | 79 | [Command("AddTip")] 80 | [Summary("Add a tip to the database.")] 81 | [RequireModerator] 82 | public async Task AddTip(string keywords, string content = "") 83 | { 84 | await TipService.AddTip(Context.Message, keywords, content); 85 | } 86 | 87 | [Command("RemoveTip")] 88 | [Summary("Remove a tip from the database.")] 89 | [RequireModerator] 90 | public async Task RemoveTip(ulong tipId) 91 | { 92 | Tip tip = TipService.GetTip(tipId); 93 | if (tip == null) 94 | { 95 | await Context.Channel.SendMessageAsync("No such tip found to be removed."); 96 | return; 97 | } 98 | 99 | await TipService.RemoveTip(Context.Message, tip); 100 | } 101 | 102 | [Command("ReplaceTip")] 103 | [Summary("Replace image content of an existing tip in the database.")] 104 | [RequireModerator] 105 | public async Task ReplaceTip(ulong tipId, string content = "") 106 | { 107 | Tip tip = TipService.GetTip(tipId); 108 | if (tip == null) 109 | { 110 | await Context.Channel.SendMessageAsync("No such tip found to be replaced."); 111 | return; 112 | } 113 | 114 | await TipService.ReplaceTip(Context.Message, tip, content); 115 | } 116 | 117 | #if false 118 | [Command("DumpTips")] 119 | [Summary("For debugging, view the tip index.")] 120 | [RequireModerator] 121 | public async Task DumpTipDatabase() 122 | { 123 | string json = TipService.DumpTipDatabase(); 124 | string prefix = "Tip database index as JSON:\n"; 125 | int chunkSize = 1800; 126 | int chunkTime = 2000; 127 | while (!string.IsNullOrEmpty(json)) 128 | { 129 | string chunk = json; 130 | if (json.Length > chunkSize) 131 | { 132 | chunk = json.Substring(0, chunkSize); 133 | json = json.Substring(chunkSize); 134 | } 135 | else 136 | { 137 | json = string.Empty; 138 | } 139 | await Context.Channel.SendMessageAsync( 140 | $"{prefix}```\n{chunk}\n```"); 141 | prefix = string.Empty; 142 | if (!string.IsNullOrEmpty(json)) 143 | await Task.Delay(chunkTime); 144 | } 145 | } 146 | #endif 147 | 148 | [Command("ReloadTips")] 149 | [Summary("Reload the database of tips.")] 150 | [RequireModerator] 151 | public async Task ReloadTipDatabase() 152 | { 153 | // rare usage, but in case someone with a shell decides 154 | // to edit the json for debugging/expansion reasons... 155 | await TipService.ReloadTipDatabase(); 156 | await ReplyAsync("Tip index reloaded."); 157 | } 158 | 159 | [Command("ListTips")] 160 | [Summary("List available tips by their keywords.")] 161 | [RequireModerator] 162 | public async Task ListTips() 163 | { 164 | List tips = TipService.GetAllTips().OrderBy(t => t.Id).ToList(); 165 | int chunkCount = 10; 166 | int chunkTime = 2000; 167 | bool first = true; 168 | 169 | while (tips.Count > 0) 170 | { 171 | var builder = new EmbedBuilder(); 172 | if (first) 173 | { 174 | builder 175 | .WithTitle("List of Tips") 176 | .WithDescription("Tips available for the following keywords:"); 177 | first = false; 178 | } 179 | 180 | int chunk = 0; 181 | while (tips.Count > 0 && chunk < chunkCount) 182 | { 183 | string keywords = string.Join("`, `", tips[0].Keywords.OrderBy(k => k)); 184 | string images = String.Concat( 185 | Enumerable.Repeat(" :frame_photo:", 186 | tips[0].ImagePaths.Count).ToArray()); 187 | builder.AddField($"ID: {tips[0].Id} {images}", $"`{keywords}`"); 188 | tips.RemoveAt(0); 189 | chunk++; 190 | } 191 | 192 | await ReplyAsync(embed: builder.Build()); 193 | if (tips.Count > 0) 194 | await Task.Delay(chunkTime); 195 | } 196 | } 197 | 198 | #region CommandList 199 | [Command("TipHelp")] 200 | [Alias("TipsHelp")] 201 | [Summary("Shows available tip database commands.")] 202 | public async Task TipHelp() 203 | { 204 | // NOTE: skips the RequireModerator commands, so nearly an empty list 205 | foreach (var message in CommandHandlingService.GetCommandListMessages("TipModule", true, true, false)) 206 | { 207 | await ReplyAsync(message); 208 | } 209 | } 210 | #endregion 211 | 212 | } 213 | -------------------------------------------------------------------------------- /DiscordBot/Modules/UnityHelp/CannedInteractiveModule.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using Discord.Interactions; 3 | using Discord.WebSocket; 4 | using DiscordBot.Service; 5 | using DiscordBot.Services; 6 | using DiscordBot.Settings; 7 | using static DiscordBot.Service.CannedResponseService; 8 | 9 | namespace DiscordBot.Modules; 10 | 11 | public class CannedInteractiveModule : InteractionModuleBase 12 | { 13 | #region Dependency Injection 14 | 15 | public UnityHelpService HelpService { get; set; } 16 | public BotSettings BotSettings { get; set; } 17 | public CannedResponseService CannedResponseService { get; set; } 18 | 19 | #endregion // Dependency Injection 20 | 21 | // Responses are any of the CannedResponseType enum 22 | [SlashCommand("faq", "Prepared responses to help answer common questions")] 23 | public async Task CannedResponses(CannedHelp type) 24 | { 25 | if (Context.User.IsUserBotOrWebhook()) 26 | return; 27 | 28 | var embed = CannedResponseService.GetCannedResponse((CannedResponseType)type); 29 | await Context.Interaction.RespondAsync(string.Empty, embed: embed.Build()); 30 | } 31 | 32 | [SlashCommand("resources", "Links to resources to help answer common questions")] 33 | public async Task Resources(CannedResources type) 34 | { 35 | if (Context.User.IsUserBotOrWebhook()) 36 | return; 37 | 38 | var embed = CannedResponseService.GetCannedResponse((CannedResponseType)type); 39 | await Context.Interaction.RespondAsync(string.Empty, embed: embed.Build()); 40 | } 41 | } -------------------------------------------------------------------------------- /DiscordBot/Modules/UnityHelp/CannedResponseModule.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using DiscordBot.Attributes; 3 | using DiscordBot.Service; 4 | using DiscordBot.Services; 5 | using DiscordBot.Settings; 6 | using static DiscordBot.Service.CannedResponseService; 7 | 8 | namespace DiscordBot.Modules; 9 | 10 | public class CannedResponseModule : ModuleBase 11 | { 12 | #region Dependency Injection 13 | 14 | public UserService UserService { get; set; } 15 | public BotSettings BotSettings { get; set; } 16 | public CannedResponseService CannedResponseService { get; set; } 17 | 18 | #endregion // Dependency Injection 19 | 20 | // The core command for the canned response module 21 | public async Task RespondWithCannedResponse(CannedResponseType type) 22 | { 23 | if (Context.User.IsUserBotOrWebhook()) 24 | return; 25 | 26 | var embed = CannedResponseService.GetCannedResponse(type, Context.User); 27 | await Context.Message.DeleteAsync(); 28 | 29 | await ReplyAsync(string.Empty, false, embed.Build()); 30 | } 31 | 32 | [Command("ask"), Alias("dontasktoask", "nohello")] 33 | [Summary("When someone asks to ask a question, respond with a link to the 'How to Ask' page.")] 34 | public async Task RespondWithHowToAsk() 35 | { 36 | await RespondWithCannedResponse(CannedResponseType.HowToAsk); 37 | } 38 | 39 | [Command("paste")] 40 | [Summary("When someone asks how to paste code, respond with a link to the 'How to Paste Code' page.")] 41 | public async Task RespondWithHowToPaste() 42 | { 43 | await RespondWithCannedResponse(CannedResponseType.Paste); 44 | } 45 | 46 | [Command("nocode")] 47 | [Summary("When someone asks for help with code, but doesn't provide any, respond with a link to the 'No Code Provided' page.")] 48 | public async Task RespondWithNoCode() 49 | { 50 | await RespondWithCannedResponse(CannedResponseType.NoCode); 51 | } 52 | 53 | [Command("xy")] 54 | [Summary("When someone is asking about their attempted solution rather than their actual problem, respond with a link to the 'XY Problem' page.")] 55 | public async Task RespondWithXYProblem() 56 | { 57 | await RespondWithCannedResponse(CannedResponseType.XYProblem); 58 | } 59 | 60 | [Command("biggame"), Alias("scope", "bigscope", "scopecreep")] 61 | [Summary("When someone is asking for help with a large project, respond with a link to the 'Game Too Big' page.")] 62 | public async Task RespondWithGameToBig() 63 | { 64 | await RespondWithCannedResponse(CannedResponseType.GameTooBig); 65 | } 66 | 67 | [Command("google"), Alias("search", "howtosearch")] 68 | [Summary("When someone asks a question that could have been answered by a quick search, respond with a link to the 'How to Google' page.")] 69 | public async Task RespondWithHowToGoogle() 70 | { 71 | await RespondWithCannedResponse(CannedResponseType.HowToGoogle); 72 | } 73 | 74 | [Command("debug")] 75 | [Summary("When someone asks for help debugging, respond with a link to the 'How to Debug' page.")] 76 | public async Task RespondWithHowToDebug() 77 | { 78 | await RespondWithCannedResponse(CannedResponseType.Debugging); 79 | } 80 | 81 | [Command("folder"), Alias("directory", "structure")] 82 | [Summary("When someone asks about folder structure, respond with a link to the 'Folder Structure' page.")] 83 | public async Task RespondWithFolderStructure() 84 | { 85 | await RespondWithCannedResponse(CannedResponseType.FolderStructure); 86 | } 87 | 88 | [Command("programming")] 89 | [Summary("When someone asks for programming resources, respond with a link to the 'Programming Resources' page.")] 90 | public async Task RespondWithProgrammingResources() 91 | { 92 | await RespondWithCannedResponse(CannedResponseType.Programming); 93 | } 94 | 95 | [Command("art")] 96 | [Summary("When someone asks for art resources, respond with a link to the 'Art Resources' page.")] 97 | public async Task RespondWithArtResources() 98 | { 99 | await RespondWithCannedResponse(CannedResponseType.Art); 100 | } 101 | 102 | [Command("3d"), Alias("3dmodeling", "3dassets")] 103 | [Summary("When someone asks for 3D modeling resources, respond with a link to the '3D Modeling Resources' page.")] 104 | public async Task RespondWith3DModelingResources() 105 | { 106 | await RespondWithCannedResponse(CannedResponseType.ThreeD); 107 | } 108 | 109 | [Command("2d"), Alias("2dmodeling", "2dassets")] 110 | [Summary("When someone asks for 2D modeling resources, respond with a link to the '2D Modeling Resources' page.")] 111 | public async Task RespondWith2DModelingResources() 112 | { 113 | await RespondWithCannedResponse(CannedResponseType.TwoD); 114 | } 115 | 116 | [Command("audio"), Alias("sound", "music")] 117 | [Summary("When someone asks for audio resources, respond with a link to the 'Audio Resources' page.")] 118 | public async Task RespondWithAudioResources() 119 | { 120 | await RespondWithCannedResponse(CannedResponseType.Audio); 121 | } 122 | 123 | [Command("design"), Alias("ui", "ux")] 124 | [Summary("When someone asks for design resources, respond with a link to the 'Design Resources' page.")] 125 | public async Task RespondWithDesignResources() 126 | { 127 | await RespondWithCannedResponse(CannedResponseType.Design); 128 | } 129 | 130 | [Command("delta"), Alias("deltatime", "fixedupdate")] 131 | [Summary("When someone asks about delta time, respond with a link to the 'Delta Time' page.")] 132 | public async Task RespondWithDeltaTime() 133 | { 134 | await RespondWithCannedResponse(CannedResponseType.DeltaTime); 135 | } 136 | } -------------------------------------------------------------------------------- /DiscordBot/Modules/UnityHelp/GeneralHelpModule.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using DiscordBot.Services; 3 | using DiscordBot.Settings; 4 | using DiscordBot.Utils; 5 | using HtmlAgilityPack; 6 | 7 | namespace DiscordBot.Modules; 8 | 9 | public class GeneralHelpModule : ModuleBase 10 | { 11 | #region Dependency Injection 12 | 13 | public UserService UserService { get; set; } 14 | public BotSettings BotSettings { get; set; } 15 | 16 | #endregion // Dependency Injection 17 | 18 | [Command("error")] 19 | [Summary("Uses a C# error code, or Unity error code and returns a link to appropriate documentation.")] 20 | public async Task RespondWithErrorDocumentation(string error) 21 | { 22 | if (Context.User.IsUserBotOrWebhook()) 23 | return; 24 | 25 | // If we're dealing with C# error 26 | if (error.StartsWith("CS")) 27 | { 28 | // an array of potential url 29 | List urls = new() 30 | { 31 | "https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/", 32 | "https://docs.microsoft.com/en-us/dotnet/csharp/misc/" 33 | }; 34 | 35 | HtmlDocument errorPage = null; 36 | string usedUrl = string.Empty; 37 | 38 | foreach (var url in urls) 39 | { 40 | errorPage = await WebUtil.GetHtmlDocument($"{url}{error}"); 41 | if (errorPage.DocumentNode.InnerHtml.Contains("Page not found")) 42 | { 43 | continue; 44 | } 45 | usedUrl = url; 46 | break; 47 | } 48 | 49 | if (errorPage == null) 50 | { 51 | await respondFailure( 52 | $"Failed to locate {error} error page, however you should try google the error code, there is likely documentation for it."); 53 | return; 54 | } 55 | 56 | // We try to pull the first header and pray it contains the error code 57 | // We grab the first h1 inside the "main" tag, or has class main-column 58 | string header = errorPage.DocumentNode.SelectSingleNode("//main//h1")? 59 | .InnerText ?? string.Empty; 60 | // Attempt to grab the first paragraph inside a class with the id "main" 61 | string summary = errorPage.DocumentNode.SelectSingleNode("//main//p")? 62 | .InnerText ?? string.Empty; 63 | 64 | if (string.IsNullOrEmpty(header)) 65 | { 66 | await respondFailure($"Couldn't find documentation for error code {error}."); 67 | return; 68 | } 69 | 70 | // Construct an Embed, Title "C# Error Code: {error}", Description: {summary}, with a link to {url}{error} 71 | var embed = new EmbedBuilder() 72 | .WithTitle($"C# Error Code: {error}") 73 | .WithDescription(summary) 74 | .WithUrl($"{usedUrl}{error}") 75 | .FooterRequestedBy(Context.User) 76 | .Build(); 77 | 78 | await ReplyAsync(string.Empty, false, embed); 79 | } 80 | } 81 | 82 | 83 | private async Task respondFailure(string errorMessage) 84 | { 85 | await ReplyAsync(errorMessage).DeleteAfterSeconds(30); 86 | await Context.Message.DeleteAfterSeconds(30); 87 | } 88 | } -------------------------------------------------------------------------------- /DiscordBot/Modules/UnityHelp/UnityHelpInteractiveModule.cs: -------------------------------------------------------------------------------- 1 | using Discord.Interactions; 2 | using DiscordBot.Services; 3 | using DiscordBot.Settings; 4 | using Discord.WebSocket; 5 | 6 | namespace DiscordBot.Modules; 7 | 8 | public class UnityHelpInteractiveModule : InteractionModuleBase 9 | { 10 | #region Dependency Injection 11 | 12 | public UnityHelpService HelpService { get; set; } 13 | public BotSettings BotSettings { get; set; } 14 | 15 | #endregion // Dependency Injection 16 | 17 | [SlashCommand("resolve-question", "If in unity-help forum channel, resolve the thread")] 18 | public async Task ResolveQuestion() 19 | { 20 | if (!BotSettings.UnityHelpBabySitterEnabled) 21 | return; 22 | 23 | await Context.Interaction.DeferAsync(ephemeral: true); 24 | 25 | if (!IsValidUser()) 26 | { 27 | await Context.Interaction.RespondAsync("Invalid User", ephemeral: true); 28 | return; 29 | } 30 | 31 | if (!IsInHelpChannel()) 32 | { 33 | await Context.Interaction.FollowupAsync( 34 | $"This command can only be used in <#{BotSettings.GenericHelpChannel.Id}> channels", ephemeral: true); 35 | return; 36 | } 37 | 38 | var response = 39 | await HelpService.OnUserRequestChannelClose(Context.User, Context.Channel as SocketThreadChannel); 40 | await Context.Interaction.FollowupAsync(response, ephemeral: true); 41 | } 42 | 43 | #region Message Commands 44 | 45 | [MessageCommand("Correct Answer")] 46 | public async Task MarkResponseAnswer(IMessage targetResponse) 47 | { 48 | if (!BotSettings.UnityHelpBabySitterEnabled) 49 | return; 50 | 51 | await Context.Interaction.DeferAsync(ephemeral: true); 52 | if (!IsValidUser()) 53 | { 54 | await Context.Interaction.RespondAsync(string.Empty, ephemeral: true); 55 | return; 56 | } 57 | if (!IsInHelpChannel()) 58 | { 59 | await Context.Interaction.FollowupAsync( 60 | $"This command can only be used in <#{BotSettings.GenericHelpChannel.Id}> channels", ephemeral: true); 61 | return; 62 | } 63 | 64 | if (targetResponse.Author == Context.User || targetResponse.Author.IsBot) 65 | { 66 | await Context.Interaction.FollowupAsync("You can't mark your own response as correct", ephemeral: true); 67 | return; 68 | } 69 | 70 | var response = await HelpService.MarkResponseAsAnswer(Context.User, targetResponse); 71 | await Context.Interaction.FollowupAsync( response, ephemeral: true); 72 | } 73 | 74 | #endregion // Context Commands 75 | 76 | 77 | #region Utility 78 | 79 | private bool IsInHelpChannel() => Context.Channel.IsThreadInChannel(BotSettings.GenericHelpChannel.Id); 80 | private bool IsValidUser() => !Context.User.IsUserBotOrWebhook(); 81 | 82 | #endregion // Utility 83 | } -------------------------------------------------------------------------------- /DiscordBot/Modules/UnityHelp/UnityHelpModule.cs: -------------------------------------------------------------------------------- 1 | using Discord.Commands; 2 | using Discord.WebSocket; 3 | using DiscordBot.Attributes; 4 | using DiscordBot.Services; 5 | using DiscordBot.Settings; 6 | 7 | namespace DiscordBot.Modules; 8 | 9 | public class UnityHelpModule : ModuleBase 10 | { 11 | #region Dependency Injection 12 | 13 | public UnityHelpService HelpService { get; set; } 14 | public UserService UserService { get; set; } 15 | public BotSettings BotSettings { get; set; } 16 | 17 | #endregion // Dependency Injection 18 | 19 | [Command("resolve"), Alias("complete")] 20 | [Summary("When a question is answered, use this command to mark it as resolved.")] 21 | public async Task ResolveAsync() 22 | { 23 | if (!BotSettings.UnityHelpBabySitterEnabled) 24 | return; 25 | if (!IsValidUser() || !IsInHelpChannel()) 26 | await Context.Message.DeleteAsync(); 27 | await HelpService.OnUserRequestChannelClose(Context.User, Context.Channel as SocketThreadChannel); 28 | } 29 | 30 | [Command("pending-questions")] 31 | [Summary("Moderation only command, announces the number of pending questions in the help channel.")] 32 | [RequireModerator, HideFromHelp, IgnoreBots] 33 | public async Task PendingQuestionsAsync() 34 | { 35 | if (!BotSettings.UnityHelpBabySitterEnabled) 36 | { 37 | await ReplyAsync("UnityHelp Service currently disabled.").DeleteAfterSeconds(15); 38 | return; 39 | } 40 | var trackedQuestionCount = HelpService.GetTrackedQuestionCount(); 41 | await ReplyAsync($"There are {trackedQuestionCount} pending questions in the help channel."); 42 | } 43 | 44 | #region Utility 45 | 46 | private bool IsInHelpChannel() => Context.Channel.IsThreadInChannel(BotSettings.GenericHelpChannel.Id); 47 | private bool IsValidUser() => !Context.User.IsUserBotOrWebhook(); 48 | 49 | #endregion // Utility 50 | } -------------------------------------------------------------------------------- /DiscordBot/Modules/Weather/WeatherContainers.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace DiscordBot.Modules.Weather; 4 | 5 | #region Weather Results 6 | 7 | #pragma warning disable 0649 8 | // ReSharper disable InconsistentNaming 9 | public class WeatherContainer 10 | { 11 | public class Coord 12 | { 13 | public double Lon { get; set; } 14 | public double Lat { get; set; } 15 | } 16 | 17 | public class Weather 18 | { 19 | public int id { get; set; } 20 | [JsonProperty("main")] public string Name { get; set; } 21 | public string Description { get; set; } 22 | public string Icon { get; set; } 23 | } 24 | 25 | public class Main 26 | { 27 | public float Temp { get; set; } 28 | [JsonProperty("feels_like")] public double Feels { get; set; } 29 | [JsonProperty("temp_min")] public double Min { get; set; } 30 | [JsonProperty("temp_max")] public double Max { get; set; } 31 | public int Pressure { get; set; } 32 | public int Humidity { get; set; } 33 | } 34 | 35 | public class Wind 36 | { 37 | public double Speed { get; set; } 38 | public int Deg { get; set; } 39 | } 40 | 41 | public class Clouds 42 | { 43 | public int all { get; set; } 44 | } 45 | 46 | public class Rain 47 | { 48 | [JsonProperty("1h")] public double Rain1h { get; set; } 49 | [JsonProperty("3h")] public double Rain3h { get; set; } 50 | } 51 | 52 | public class Snow 53 | { 54 | [JsonProperty("1h")] public double Snow1h { get; set; } 55 | [JsonProperty("3h")] public double Snow3h { get; set; } 56 | } 57 | 58 | public class Sys 59 | { 60 | public int type { get; set; } 61 | public int id { get; set; } 62 | public double message { get; set; } 63 | public string country { get; set; } 64 | public int sunrise { get; set; } 65 | public int sunset { get; set; } 66 | } 67 | 68 | public class Result 69 | { 70 | public Coord coord { get; set; } 71 | public List weather { get; set; } 72 | public string @base { get; set; } 73 | public Main main { get; set; } 74 | public int visibility { get; set; } 75 | public Wind wind { get; set; } 76 | public Clouds clouds { get; set; } 77 | public Rain rain { get; set; } 78 | public Snow snow { get; set; } 79 | public int dt { get; set; } 80 | public Sys sys { get; set; } 81 | public int timezone { get; set; } 82 | public int id { get; set; } 83 | public string name { get; set; } 84 | public int cod { get; set; } 85 | } 86 | } 87 | 88 | #endregion 89 | #region Pollution Results 90 | 91 | public class PollutionContainer 92 | { 93 | public class Coord 94 | { 95 | public double lon { get; set; } 96 | public double lat { get; set; } 97 | } 98 | public class Main 99 | { 100 | public int aqi { get; set; } 101 | } 102 | public class Components 103 | { 104 | [JsonProperty("co")] public double CarbonMonoxide { get; set; } 105 | [JsonProperty("no")] public double NitrogenMonoxide { get; set; } 106 | [JsonProperty("no2")] public double NitrogenDioxide { get; set; } 107 | [JsonProperty("o3")] public double Ozone { get; set; } 108 | [JsonProperty("so2")] public double SulphurDioxide { get; set; } 109 | [JsonProperty("pm2_5")] public double FineParticles { get; set; } 110 | [JsonProperty("pm10")] public double CoarseParticulate { get; set; } 111 | [JsonProperty("nh3")] public double Ammonia { get; set; } 112 | } 113 | 114 | public class List 115 | { 116 | public Main main { get; set; } 117 | public Components components { get; set; } 118 | public int dt { get; set; } 119 | } 120 | public class Result 121 | { 122 | public Coord coord { get; set; } 123 | public List list { get; set; } 124 | } 125 | } 126 | 127 | // ReSharper restore InconsistentNaming 128 | #pragma warning restore 0649 129 | #endregion -------------------------------------------------------------------------------- /DiscordBot/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Discord.Commands; 3 | using Discord.Interactions; 4 | using Discord.WebSocket; 5 | using DiscordBot.Service; 6 | using DiscordBot.Services; 7 | using DiscordBot.Services.Tips; 8 | using DiscordBot.Settings; 9 | using DiscordBot.Utils; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using RunMode = Discord.Commands.RunMode; 12 | 13 | namespace DiscordBot; 14 | 15 | public class Program 16 | { 17 | private bool _isInitialized = false; 18 | 19 | private static Rules _rules; 20 | private static BotSettings _settings; 21 | private static UserSettings _userSettings; 22 | private DiscordSocketClient _client; 23 | private CommandHandlingService _commandHandlingService; 24 | 25 | private CommandService _commandService; 26 | private InteractionService _interactionService; 27 | private IServiceProvider _services; 28 | 29 | private UnityHelpService _unityHelpService; 30 | private RecruitService _recruitService; 31 | 32 | public static void Main(string[] args) => 33 | new Program().MainAsync().GetAwaiter().GetResult(); 34 | 35 | private async Task MainAsync() 36 | { 37 | DeserializeSettings(); 38 | 39 | _client = new DiscordSocketClient(new DiscordSocketConfig 40 | { 41 | LogLevel = LogSeverity.Verbose, 42 | AlwaysDownloadUsers = true, 43 | MessageCacheSize = 1024, 44 | GatewayIntents = GatewayIntents.All, 45 | }); 46 | _client.Log += LoggingService.DiscordNetLogger; 47 | 48 | await _client.LoginAsync(TokenType.Bot, _settings.Token); 49 | await _client.StartAsync(); 50 | 51 | _client.Ready += () => 52 | { 53 | // Ready can be called additional times if the bot disconnects for long enough, 54 | // so we need to make sure we only initialize commands and such for the bot once if it manages to re-establish connection 55 | if (_isInitialized) return Task.CompletedTask; 56 | 57 | _interactionService = new InteractionService(_client); 58 | _commandService = new CommandService(new CommandServiceConfig 59 | { 60 | CaseSensitiveCommands = false, 61 | DefaultRunMode = RunMode.Async 62 | }); 63 | 64 | _services = ConfigureServices(); 65 | _commandHandlingService = _services.GetRequiredService(); 66 | 67 | // Announce, and Log bot started to track issues a bit easier 68 | var logger = _services.GetRequiredService(); 69 | logger.LogChannelAndFile("Bot Started.", ExtendedLogSeverity.Positive); 70 | 71 | LoggingService.LogToConsole("Bot is connected.", ExtendedLogSeverity.Positive); 72 | _isInitialized = true; 73 | 74 | _unityHelpService = _services.GetRequiredService(); 75 | _recruitService = _services.GetRequiredService(); 76 | _services.GetRequiredService(); 77 | 78 | return Task.CompletedTask; 79 | }; 80 | 81 | await Task.Delay(-1); 82 | } 83 | 84 | private IServiceProvider ConfigureServices() => 85 | new ServiceCollection() 86 | .AddSingleton(_settings) 87 | .AddSingleton(_rules) 88 | .AddSingleton(_userSettings) 89 | .AddSingleton(_client) 90 | .AddSingleton(_commandService) 91 | .AddSingleton(_interactionService) 92 | .AddSingleton() 93 | .AddSingleton() 94 | .AddSingleton() 95 | .AddSingleton() 96 | .AddSingleton() 97 | .AddSingleton() 98 | .AddSingleton() 99 | .AddSingleton() 100 | .AddSingleton() 101 | .AddSingleton() 102 | .AddSingleton() 103 | .AddSingleton() 104 | .AddSingleton() 105 | .AddSingleton() 106 | .AddSingleton() 107 | .AddSingleton() 108 | .AddSingleton() 109 | .AddSingleton() 110 | .AddSingleton() 111 | .BuildServiceProvider(); 112 | 113 | private static void DeserializeSettings() 114 | { 115 | _settings = SerializeUtil.DeserializeFile(@"Settings/Settings.json"); 116 | _rules = SerializeUtil.DeserializeFile(@"Settings/Rules.json"); 117 | _userSettings = SerializeUtil.DeserializeFile(@"Settings/UserSettings.json"); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /DiscordBot/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | using System.CodeDom.Compiler; 12 | using System.ComponentModel; 13 | using System.Diagnostics; 14 | using System.Diagnostics.CodeAnalysis; 15 | using System.Globalization; 16 | using System.Resources; 17 | using System.Runtime.CompilerServices; 18 | 19 | namespace DiscordBot.Properties 20 | { 21 | /// 22 | /// A strongly-typed resource class, for looking up localized strings, etc. 23 | /// 24 | // This class was auto-generated by the StronglyTypedResourceBuilder 25 | // class via a tool like ResGen or Visual Studio. 26 | // To add or remove a member, edit your .ResX file then rerun ResGen 27 | // with the /str option, or rebuild your VS project. 28 | [GeneratedCode("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 29 | [DebuggerNonUserCode()] 30 | [CompilerGenerated()] 31 | internal class Resources 32 | { 33 | 34 | private static ResourceManager resourceMan; 35 | 36 | private static CultureInfo resourceCulture; 37 | 38 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 39 | internal Resources() 40 | { 41 | } 42 | 43 | /// 44 | /// Returns the cached ResourceManager instance used by this class. 45 | /// 46 | [EditorBrowsable(EditorBrowsableState.Advanced)] 47 | internal static ResourceManager ResourceManager 48 | { 49 | get 50 | { 51 | if (ReferenceEquals(resourceMan, null)) 52 | { 53 | ResourceManager temp = new("DiscordBot.Properties.Resources", typeof(Resources).Assembly); 54 | resourceMan = temp; 55 | } 56 | return resourceMan; 57 | } 58 | } 59 | 60 | /// 61 | /// Overrides the current thread's CurrentUICulture property for all 62 | /// resource lookups using this strongly typed resource class. 63 | /// 64 | [EditorBrowsable(EditorBrowsableState.Advanced)] 65 | internal static CultureInfo Culture 66 | { 67 | get 68 | { 69 | return resourceCulture; 70 | } 71 | set 72 | { 73 | resourceCulture = value; 74 | } 75 | } 76 | 77 | /// 78 | /// Looks up a localized string similar to ```{language} 79 | ///{code} 80 | ///``` 81 | ///. 82 | /// 83 | internal static string DiscordCodeBlock 84 | { 85 | get 86 | { 87 | return ResourceManager.GetString("DiscordCodeBlock", resourceCulture); 88 | } 89 | } 90 | 91 | /// 92 | /// Looks up a localized string similar to using System; 93 | ///using System.Collections.Generic; 94 | /// 95 | ///public class Hello 96 | ///{ 97 | /// public static void Main() 98 | /// { 99 | /// {code} 100 | /// } 101 | ///}. 102 | /// 103 | internal static string PaizaCodeTemplate 104 | { 105 | get 106 | { 107 | return ResourceManager.GetString("PaizaCodeTemplate", resourceCulture); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /DiscordBot/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | text/microsoft-resx 113 | 114 | 115 | 2.0 116 | 117 | 118 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 122 | 123 | 124 | -------------------------------------------------------------------------------- /DiscordBot/SERVER/FAQs.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "question": "What is karma ?", 4 | "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.", 5 | "keywords": [ 6 | "karma", 7 | "xp" 8 | ] 9 | } 10 | ] -------------------------------------------------------------------------------- /DiscordBot/SERVER/fonts/Consolas.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/fonts/Consolas.ttf -------------------------------------------------------------------------------- /DiscordBot/SERVER/fonts/ConsolasBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/fonts/ConsolasBold.ttf -------------------------------------------------------------------------------- /DiscordBot/SERVER/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /DiscordBot/SERVER/fonts/OpenSansEmoji.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/fonts/OpenSansEmoji.ttf -------------------------------------------------------------------------------- /DiscordBot/SERVER/fonts/georgia.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/fonts/georgia.ttf -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/ExampleExport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/ExampleExport.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/Layout.txt: -------------------------------------------------------------------------------- 1 | XP Bar: 2 | - Back_Rect: (104, 39, 232, 16) (X,Y,W,H) 3 | - Fore_Rect: (105, 40, 126, 14) (X,Y,W,H) 4 | - Background:(#3F3F3F) 5 | - Foreground:(#00F0FF) 6 | 7 | Rank Triangle: 8 | - Rect: (346, 12, 54, 104) (X,Y,W,H) 9 | - Color:(Based on UDH Rank Color) 10 | 11 | Fonts: 12 | - Name: Consolas (24pt, #3C3C3C) 13 | - Level: Consolas (59pt, #3C3C3C) 14 | - Meta: Opens Sans (16pt, #3C3C3C) 15 | 16 | Effects (If Possible): 17 | - Card: Drop-Shadow (16px, 50% Opacity), Alpha-Blend (75% Opacity) 18 | - XP Bar: Drop-Shadow (4px, 50% Opacity) 19 | - Level: Drop-Shadow (4px, 25% Opacity) 20 | 21 | 22 | NEW 23 | ========================================== 24 | Image: 25 | - Role_Rect: (11, 11, 113, 113) (X,Y,W,H) 26 | - Picture_Rect: (27, 17, 102, 102) (X,Y,W,H) 27 | 28 | Name: 29 | - Rect: (131, 18, 272, 26) (X,Y,W,H) 30 | - Font: Consolas - Regular (24pt, #3a3a3a) 31 | 32 | XP-Bar: 33 | - Back_Rect: (131, 44, 275, 15) (X,Y,W,H) 34 | - Front_Rect: (133, 46, 271, 11) (X,Y,W,H) <- filled 35 | - Text: (131, 45, 272, 10) (X,Y,W,H) 36 | - Font: Consolas - Regular (11pt, #3a3a3a) 37 | - Color: #c5c5c7 38 | 39 | LEVEL Text 40 | - Rect: (131, 63, 90, 18) (X,Y,W,H) 41 | - Font: Consolas - Bold (22pt, #3a3a3a) - Tracking: 40 - Center-align 42 | 43 | Level # Text 44 | - Rect: (131, 89, 90, 30) (X,Y,W,H) 45 | - Font: Consolas - Bold (30pt, #3a3a3a) - Center-align 46 | 47 | Server Rank Text 48 | - Rect: (235, 67, 109, 18) (X,Y,W,H) 49 | - Font: Consolas - Regular (16pt, #3a3a3a) 50 | 51 | Server Rank Value Text 52 | - Rect: (344, 67, 45, 18) (X,Y,W,H) 53 | - Font: Consolas - Regular (16pt, #3a3a3a) - Right-align 54 | 55 | Karma Points Text 56 | - Rect: (235, 95, 109, 18) (X,Y,W,H) 57 | - Font: Consolas - Regular (16pt, #3a3a3a) 58 | 59 | Server Rank Value Text 60 | - Rect: (344, 95, 45, 18) (X,Y,W,H) 61 | - Font: Consolas - Regular (16pt, #3a3a3a) - Right-align -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/background.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/background.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/background.psd -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/background_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/background_old.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/default.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/foreground.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/foreground.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/foreground.psd -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/levelupcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/levelupcard.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/levelupcard.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/levelupcard.psd -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/levelupcardbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/levelupcardbackground.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/images/triangle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/images/triangle.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/skins/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/skins/background.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/skins/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/DiscordBot/SERVER/skins/foreground.png -------------------------------------------------------------------------------- /DiscordBot/SERVER/skins/skin.default.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "Default", 3 | "Codename": "default", 4 | "Description": "Default avatar", 5 | "AvatarSize": 128, 6 | "Background": "background.png", 7 | "Layers": [ 8 | { 9 | "Image": "foreground.png", 10 | "StartX": 30, 11 | "StartY": 20, 12 | "Width": 640, 13 | "Height": 209, 14 | "Modules": [ 15 | { 16 | "Type": "AvatarBorder", 17 | "StartX": 6, 18 | "StartY": 20 19 | }, 20 | { 21 | "Type": "XpBar", 22 | "StartX": 150, 23 | "StartY": 42, 24 | "Width": 360, 25 | "Height": 18, 26 | "OutsideStrokeColor": "#778899FF", 27 | "OutsideFillColor": "#F5F5F5FF", 28 | "InsideStrokeColor": "#FFFFFF00", 29 | "InsideFillColor": "#32CD32FF" 30 | }, 31 | { 32 | "Type": "XpBarInfo", 33 | "StartX": 315, 34 | "StartY": 57, 35 | "Width": 360, 36 | "Height": 18, 37 | "Font": "Consolas", 38 | "FillColor": "#000000FF", 39 | "StrokeColor": "#00000000", 40 | "FontPointSize": 17, 41 | "TextAlignment": "Center" 42 | }, 43 | { 44 | "Type": "Username", 45 | "StartX": 150, 46 | "StartY": 38, 47 | "FontPointSize": 34, 48 | "Font": "Consolas", 49 | "StrokeColor": "#8A2BE2FF", 50 | "FillColor": "#00BFFFFF", 51 | "StrokeAntialias": true, 52 | "StrokeWidth": 0.4 53 | }, 54 | { 55 | "Type": "TotalXp", 56 | "StartX": 535, 57 | "StartY": 83, 58 | "FontPointSize": 17, 59 | "Font": "Consolas", 60 | "FillColor": "#000000FF", 61 | "TextAlignment": "Right" 62 | }, 63 | { 64 | "Type": "XpRank", 65 | "StartX": 535, 66 | "StartY": 108, 67 | "FontPointSize": 17, 68 | "Font": "Consolas", 69 | "FillColor": "#000000FF", 70 | "TextAlignment": "Right" 71 | }, 72 | { 73 | "Type": "KarmaRank", 74 | "StartX": 535, 75 | "StartY": 153, 76 | "FontPointSize": 17, 77 | "Font": "Consolas", 78 | "FillColor": "#000000FF", 79 | "TextAlignment": "Right" 80 | }, 81 | { 82 | "Type": "KarmaPoints", 83 | "StartX": 535, 84 | "StartY": 130, 85 | "FontPointSize": 17, 86 | "Font": "Consolas", 87 | "FillColor": "#000000FF", 88 | "TextAlignment": "Right" 89 | }, 90 | { 91 | "Type": "Level", 92 | "StartX": 220, 93 | "StartY": 140, 94 | "FontPointSize": 50, 95 | "Font": "Consolas", 96 | "TextAlignment": "Center" 97 | } 98 | ] 99 | }, 100 | { 101 | "Image": "avatar", 102 | "StartX": 36, 103 | "StartY": 40 104 | } 105 | ] 106 | } -------------------------------------------------------------------------------- /DiscordBot/SERVER/skins/skin.json: -------------------------------------------------------------------------------- 1 | { 2 | "Name": "flat", 3 | "Codename": "flat", 4 | "Description": "nobelium uranium", 5 | "AvatarSize": 209, 6 | "Background": "background.png", 7 | "Layers": [ 8 | { 9 | "Image": null, 10 | "StartX": 0, 11 | "StartY": 0, 12 | "Width": 640, 13 | "Height": 209, 14 | "Modules": [ 15 | { 16 | "Type": "RectangleSampleAvatarColor", 17 | "StartX": 0, 18 | "StartY": 0, 19 | "Width": 640, 20 | "Height": 209, 21 | "WhiteFix": true, 22 | "DefaultColor": "#1B2631FF" 23 | }, 24 | { 25 | "Type": "Username", 26 | "FontPointSize": 50, 27 | "StartX": 45, 28 | "StartY": 60, 29 | "StrokeColor": "#FFF", 30 | "FillColor": "#FFF", 31 | "StrokeWidth": 0, 32 | "StrokeAntiAlias": true 33 | }, 34 | // Level 35 | { 36 | "Type": "CustomText", 37 | "StartX": 45, 38 | "StartY": 102, 39 | "FillColor": "#FFF", 40 | "FontPointSize": 28, 41 | "Text": "LVL" 42 | }, 43 | { 44 | "Type": "Level", 45 | "StartX": 105, 46 | "StartY": 102, 47 | "FontPointSize": 28, 48 | "FillColor": "#FFF", 49 | "StrokeColor": "#0000" 50 | }, 51 | // xp stuff 52 | { 53 | "Type": "XpBarInfo", 54 | "StartX": 45, 55 | "StartY": 183, 56 | "FillColor": "#FFF", 57 | "StrokeColor": "#0000", 58 | "FontPointSize": 12 59 | }, 60 | { 61 | "Type": "XpRank", 62 | "StartX": 130, 63 | "StartY": 162, 64 | "FontPointSize": 20, 65 | "FillColor": "#FFF" 66 | }, 67 | { 68 | "Type": "CustomText", 69 | "StartX": 45, 70 | "StartY": 162, 71 | "FontPointSize": 20, 72 | "FillColor": "#FFF", 73 | "Text": "XP Rank" 74 | }, 75 | // Karma stuff 76 | { 77 | "Type": "CustomText", 78 | "StartX": 240, 79 | "StartY": 162, 80 | "FontPointSize": 15, 81 | "FillColor": "#FFF", 82 | "Text": "Karma Rank" 83 | }, 84 | { 85 | "Type": "KarmaRank", 86 | "StartX": 363, 87 | "StartY": 162, 88 | "FontPointSize": 15, 89 | "FillColor": "#FFFF", 90 | "TextAlignmnent": "Right" 91 | }, 92 | { 93 | "Type": "CustomText", 94 | "StartX": 240, 95 | "StartY": 183, 96 | "FillColor": "#FFF", 97 | "FontPointSize": 15, 98 | "Text": "Karma Points" 99 | }, 100 | { 101 | "Type": "KarmaPoints", 102 | "StartX": 363, 103 | "StartY": 183, 104 | "FontPointSize": 15, 105 | "FillColor": "#FFF", 106 | "TextAlignmnent": "Right" 107 | } 108 | ] 109 | }, 110 | { 111 | "Image": "avatar", 112 | "StartX": 431, 113 | "StartY": 0 114 | } 115 | ] 116 | } -------------------------------------------------------------------------------- /DiscordBot/Services/CurrencyService.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Utils; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace DiscordBot.Services; 5 | 6 | public class CurrencyService 7 | { 8 | private const string ServiceName = "CurrencyService"; 9 | 10 | #region Configuration 11 | 12 | private const int ApiVersion = 1; 13 | private const string TargetDate = "latest"; 14 | private const string ValidCurrenciesEndpoint = "currencies.min.json"; 15 | private const string ExchangeRatesEndpoint = "currencies"; 16 | 17 | private class Currency 18 | { 19 | public string Name { get; set; } 20 | public string Short { get; set; } 21 | } 22 | 23 | #endregion // Configuration 24 | 25 | private readonly Dictionary _currencies = new(); 26 | 27 | private static readonly string ApiUrl = $"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@{TargetDate}/v{ApiVersion}/"; 28 | 29 | public async Task GetConversion(string toCurrency, string fromCurrency = "usd") 30 | { 31 | toCurrency = toCurrency.ToLower(); 32 | fromCurrency = fromCurrency.ToLower(); 33 | 34 | var url = $"{ApiUrl}{ExchangeRatesEndpoint}/{fromCurrency.ToLower()}.min.json"; 35 | 36 | // Check if success 37 | var (success, response) = await WebUtil.TryGetObjectFromJson(url); 38 | if (!success) 39 | return -1; 40 | 41 | // json[fromCurrency][toCurrency] 42 | var value = response.SelectToken($"{fromCurrency}.{toCurrency}"); 43 | if (value == null) 44 | return -1; 45 | 46 | return value.Value(); 47 | } 48 | 49 | #region Public Methods 50 | 51 | public async Task GetCurrencyName(string currency) 52 | { 53 | currency = currency.ToLower(); 54 | if (!await IsCurrency(currency)) 55 | return string.Empty; 56 | return _currencies[currency].Name; 57 | } 58 | 59 | // Checks if a provided currency is valid, it also checks is we have a list of currencies to check against and rebuilds it if not. (If the API was down when bot started) 60 | public async Task IsCurrency(string currency) 61 | { 62 | if (_currencies.Count <= 1) 63 | await BuildCurrencyList(); 64 | return _currencies.ContainsKey(currency); 65 | } 66 | 67 | #endregion // Public Methods 68 | 69 | #region Private Methods 70 | 71 | private async Task BuildCurrencyList() 72 | { 73 | var url = ApiUrl + ValidCurrenciesEndpoint; 74 | var currencies = await WebUtil.GetObjectFromJson>(url); 75 | 76 | // Json is weird format of `Code: Name` each in dependant ie; {"1inch":"1inch Network","aave":"Aave"} 77 | foreach (var currency in currencies) 78 | { 79 | _currencies.Add(currency.Key, new Currency 80 | { 81 | Name = currency.Value!.ToString(), 82 | Short = currency.Key 83 | }); 84 | } 85 | 86 | LoggingService.LogToConsole($"[{ServiceName}] Built currency list with {_currencies.Count} currencies.", ExtendedLogSeverity.Positive); 87 | } 88 | 89 | #endregion // Private Methods 90 | 91 | } -------------------------------------------------------------------------------- /DiscordBot/Services/Moderation/IntroductionWatcherService.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | using DiscordBot.Settings; 3 | using DiscordBot.Services.UnityHelp; 4 | 5 | namespace DiscordBot.Services; 6 | 7 | // Small service to watch users posting new messages in introductions, keeping track of the last 500 messages and deleting any from the same user 8 | public class IntroductionWatcherService 9 | { 10 | private const string ServiceName = "IntroductionWatcherService"; 11 | 12 | private readonly DiscordSocketClient _client; 13 | private readonly ILoggingService _loggingService; 14 | private readonly SocketChannel _introductionChannel; 15 | 16 | private readonly HashSet _uniqueUsers = new HashSet(MaxMessagesToTrack + 1); 17 | private readonly Queue _orderedUsers = new Queue(MaxMessagesToTrack + 1); 18 | 19 | private SocketRole ModeratorRole { get; set; } 20 | 21 | private const int MaxMessagesToTrack = 1000; 22 | 23 | public IntroductionWatcherService(DiscordSocketClient client, ILoggingService loggingService, BotSettings settings) 24 | { 25 | _client = client; 26 | _loggingService = loggingService; 27 | 28 | ModeratorRole = _client.GetGuild(settings.GuildId).GetRole(settings.ModeratorRoleId); 29 | 30 | if (!settings.IntroductionWatcherServiceEnabled) 31 | { 32 | LoggingService.LogServiceDisabled(ServiceName, nameof(settings.IntroductionWatcherServiceEnabled)); 33 | return; 34 | } 35 | 36 | _introductionChannel = client.GetChannel(settings.IntroductionChannel.Id); 37 | if (_introductionChannel == null) 38 | { 39 | _loggingService.LogAction($"[{ServiceName}] Error: Could not find introduction channel.", ExtendedLogSeverity.Warning); 40 | return; 41 | } 42 | 43 | _client.MessageReceived += MessageReceived; 44 | } 45 | 46 | private async Task MessageReceived(SocketMessage message) 47 | { 48 | // We only watch the introduction channel 49 | if (_introductionChannel == null || message.Channel.Id != _introductionChannel.Id) 50 | return; 51 | 52 | if (message.Author.HasRoleGroup(ModeratorRole)) 53 | return; 54 | 55 | if (_uniqueUsers.Contains(message.Author.Id)) 56 | { 57 | await message.DeleteAsync(); 58 | await _loggingService.LogChannelAndFile( 59 | $"[{ServiceName}]: Duplicate introduction from {message.Author.GetUserLoggingString()} [Message deleted]"); 60 | } 61 | 62 | _uniqueUsers.Add(message.Author.Id); 63 | _orderedUsers.Enqueue(message.Author.Id); 64 | if (_orderedUsers.Count > MaxMessagesToTrack) 65 | { 66 | var oldestUser = _orderedUsers.Dequeue(); 67 | _uniqueUsers.Remove(oldestUser); 68 | } 69 | 70 | await Task.CompletedTask; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DiscordBot/Services/ModerationService.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Discord.WebSocket; 3 | using DiscordBot.Settings; 4 | 5 | namespace DiscordBot.Services; 6 | 7 | public class ModerationService 8 | { 9 | private readonly ILoggingService _loggingService; 10 | private readonly DiscordSocketClient _client; 11 | private readonly CommandHandlingService _commandHandlingService; 12 | 13 | private const int MaxMessageLength = 800; 14 | private static readonly Color DeletedMessageColor = new (200, 128, 128); 15 | private static readonly Color EditedMessageColor = new (255, 255, 128); 16 | 17 | private readonly IMessageChannel _botAnnouncementChannel; 18 | private readonly IMessageChannel _memeChannel; 19 | private readonly bool _moderatorNoInviteLinks; 20 | 21 | public ModerationService(DiscordSocketClient client, BotSettings settings, ILoggingService loggingService, 22 | CommandHandlingService commandHandlingService) 23 | { 24 | _client = client; 25 | _loggingService = loggingService; 26 | _commandHandlingService = commandHandlingService; 27 | 28 | client.MessageDeleted += MessageDeleted; 29 | client.MessageUpdated += MessageUpdated; 30 | client.MessageReceived += MessageReceived; 31 | 32 | if (settings.BotAnnouncementChannel != null) 33 | _botAnnouncementChannel = _client.GetChannel(settings.BotAnnouncementChannel.Id) as IMessageChannel; 34 | if (settings.MemeChannel != null) 35 | _memeChannel = _client.GetChannel(settings.MemeChannel.Id) as IMessageChannel; 36 | _moderatorNoInviteLinks = settings.ModeratorNoInviteLinks; 37 | } 38 | 39 | private async Task MessageDeleted(Cacheable message, Cacheable channel) 40 | { 41 | if (message.HasValue == false) 42 | { 43 | await _loggingService.LogChannelAndFile($"An uncached Message snowflake:`{message.Id}` was deleted from channel <#{(await channel.GetOrDownloadAsync()).Id}>"); 44 | return; 45 | } 46 | 47 | if (message.Value.Author.IsBot || channel.Id == _botAnnouncementChannel.Id) 48 | return; 49 | // Check the author is even in the guild 50 | var guildUser = message.Value.Author as SocketGuildUser; 51 | if (guildUser == null) 52 | return; 53 | 54 | var content = message.Value.Content; 55 | if (content.Length > MaxMessageLength) 56 | content = content[..MaxMessageLength]; 57 | 58 | var user = message.Value.Author; 59 | var builder = new EmbedBuilder() 60 | .WithColor(DeletedMessageColor) 61 | .WithTimestamp(message.Value.Timestamp) 62 | .FooterInChannel(message.Value.Channel) 63 | .AddAuthorWithAction(user, "Deleted a message", true) 64 | .AddField($"Deleted Message {(content.Length != message.Value.Content.Length ? "(truncated)" : "")}", 65 | content); 66 | var embed = builder.Build(); 67 | 68 | await _loggingService.Log(LogBehaviour.Channel, string.Empty, ExtendedLogSeverity.Info, embed); 69 | } 70 | 71 | private async Task MessageUpdated(Cacheable before, SocketMessage after, ISocketMessageChannel channel) 72 | { 73 | if (after.Author.IsBot || channel.Id == _botAnnouncementChannel.Id) 74 | return; 75 | 76 | bool isCached = true; 77 | string content = ""; 78 | var beforeMessage = await before.GetOrDownloadAsync(); 79 | if (beforeMessage == null || beforeMessage.Content == after.Content) 80 | isCached = false; 81 | else 82 | content = beforeMessage.Content; 83 | 84 | // Check the message aren't the same 85 | if (content == after.Content) 86 | return; 87 | if (content.Length == 0 && beforeMessage.Attachments.Count == 0) 88 | return; 89 | 90 | bool isTruncated = false; 91 | if (content.Length > MaxMessageLength) 92 | { 93 | content = content[..MaxMessageLength]; 94 | isTruncated = true; 95 | } 96 | 97 | var user = after.Author; 98 | var builder = new EmbedBuilder() 99 | .WithColor(EditedMessageColor) 100 | .WithTimestamp(after.Timestamp) 101 | .FooterInChannel(after.Channel) 102 | .AddAuthorWithAction(user, "Updated a message", true); 103 | if (isCached) 104 | { 105 | builder.AddField($"Previous message content {(isTruncated ? "(truncated)" : "")}", content); 106 | // if any attachments that after does not, add a link to them and a count 107 | if (beforeMessage.Attachments.Count > 0) 108 | { 109 | var attachments = beforeMessage.Attachments.Where(x => after.Attachments.All(y => y.Url != x.Url)); 110 | var removedAttachments = attachments.ToList(); 111 | if (removedAttachments.Any()) 112 | { 113 | var attachmentString = string.Join("\n", removedAttachments.Select(x => $"[{x.Filename}]({x.Url})")); 114 | builder.AddField($"Previous attachments ({removedAttachments.Count()})", attachmentString); 115 | } 116 | } 117 | } 118 | 119 | builder.WithDescription($"Message: [{after.Id}]({after.GetJumpUrl()})"); 120 | var embed = builder.Build(); 121 | 122 | // TimeStamp for the Footer 123 | 124 | await _loggingService.Log(LogBehaviour.Channel, string.Empty, ExtendedLogSeverity.Info, embed); 125 | } 126 | 127 | // MessageReceived 128 | private async Task MessageReceived(SocketMessage message) 129 | { 130 | if (message.Author.IsBot) 131 | return; 132 | 133 | if (_moderatorNoInviteLinks == true) 134 | { 135 | if (_memeChannel.Id == message.Channel.Id) 136 | { 137 | if (message.ContainsInviteLink()) 138 | { 139 | await message.DeleteAsync(); 140 | // Send a message in _botAnnouncementChannel about the deleted message, nothing fancy, name, userid, channel and message content 141 | await _botAnnouncementChannel.SendMessageAsync( 142 | $"{message.Author.Mention} tried to post an invite link in <#{message.Channel.Id}>: {message.Content}"); 143 | return; 144 | } 145 | } 146 | } 147 | } 148 | 149 | public async Task GetBotCommandHistory(int count) 150 | { 151 | return await _commandHandlingService.GetCommandHistory(count); 152 | } 153 | } -------------------------------------------------------------------------------- /DiscordBot/Services/PublisherService.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Security.Cryptography; 3 | using System.Text.RegularExpressions; 4 | using Discord.WebSocket; 5 | using DiscordBot.Settings; 6 | using MailKit.Net.Smtp; 7 | using MimeKit; 8 | 9 | namespace DiscordBot.Services; 10 | 11 | public class PublisherService 12 | { 13 | private readonly BotSettings _settings; 14 | private readonly Dictionary _verificationCodes; 15 | 16 | public PublisherService(BotSettings settings) 17 | { 18 | _verificationCodes = new Dictionary(); 19 | _settings = settings; 20 | } 21 | 22 | /* 23 | blogfeed (xml) => https://blogs.unity3d.com/feed/ 24 | */ 25 | 26 | // Attempts to get a publishers email from the unity asset store and emails them with confirmation codes to verify their account. 27 | public async Task<(bool, string)> VerifyPublisher(uint publisherId, string name) 28 | { 29 | // Unity is 1, we probably don't want to email them. 30 | if (publisherId < 2) 31 | return (false, "Invalid publisher ID."); 32 | 33 | using (var webClient = new WebClient()) 34 | { 35 | // For the record, this is a terrible way of pulling this information. 36 | var content = await webClient.DownloadStringTaskAsync($"https://assetstore.unity.com/publishers/{publisherId}"); 37 | if (!content.Contains("Error 404")) 38 | { 39 | var email = string.Empty; 40 | var emailMatch = new Regex("mailto:([^\"]+)").Match(content); 41 | if (emailMatch.Success) 42 | email = emailMatch.Groups[1].Value; 43 | 44 | if (email.Length > 2) 45 | { 46 | // No easy way to take their name, so we pass their discord name in. 47 | await SendVerificationCode(name, email, publisherId); 48 | 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."); 49 | } 50 | } 51 | return (false, "We failed to confirm this Publisher ID, double check and try again in a few minutes."); 52 | } 53 | } 54 | 55 | public async Task SendVerificationCode(string name, string email, uint packageId) 56 | { 57 | var random = new byte[9]; 58 | var rand = RandomNumberGenerator.Create(); 59 | rand.GetBytes(random); 60 | 61 | var code = Convert.ToBase64String(random); 62 | 63 | _verificationCodes[packageId] = code; 64 | var message = new MimeMessage(); 65 | message.From.Add(new MailboxAddress("Unity Developer Community", _settings.Email)); 66 | message.To.Add(new MailboxAddress(name, email)); 67 | message.Subject = "Unity Developer Community Package Validation"; 68 | message.Body = new TextPart("plain") 69 | { 70 | Text = @"Here's your validation code : " + code 71 | }; 72 | 73 | using (var client = new SmtpClient()) 74 | { 75 | client.CheckCertificateRevocation = false; 76 | await client.ConnectAsync(_settings.EmailSMTPServer, _settings.EmailSMTPPort, MailKit.Security.SecureSocketOptions.SslOnConnect); 77 | 78 | client.AuthenticationMechanisms.Remove("XOAUTH2"); 79 | await client.AuthenticateAsync(_settings.EmailUsername, _settings.EmailPassword); 80 | 81 | await client.SendAsync(message); 82 | await client.DisconnectAsync(true); 83 | } 84 | 85 | //TODO Delete code after 30min 86 | } 87 | 88 | // User is verified if they have a code that matches the one in _verificationCodes and given `Asset-Publisher` role if so. 89 | public async Task ValidatePublisherWithCode(IUser user, uint packageId, string code) 90 | { 91 | if (!_verificationCodes.TryGetValue(packageId, out string c)) 92 | return "An error occurred while trying to verify your publisher account. Please check your ID is valid."; 93 | if (c != code) 94 | return "The verification code is not valid. Please check and try again."; 95 | 96 | // Give the user the publisher role. 97 | await ((SocketGuildUser)user) 98 | .AddRoleAsync(((SocketGuildUser)user) 99 | .Guild.GetRole(_settings.PublisherRoleId)); 100 | // Remove this code since it is now used. 101 | _verificationCodes.Remove(packageId); 102 | 103 | return "Your publisher account has been verified and you now have the `Asset-Publisher` role!"; 104 | } 105 | } -------------------------------------------------------------------------------- /DiscordBot/Services/Tips/Components/Tip.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | 3 | namespace DiscordBot.Services.Tips.Components; 4 | 5 | public class Tip: IEntity 6 | { 7 | public ulong Id { get; set; } 8 | public string Content { get; set; } 9 | public List Keywords { get; set; } 10 | public List ImagePaths { get; set; } 11 | public int Requests { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /DiscordBot/Services/UnityHelp/Components/HelpBotMessage.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Services.UnityHelp; 2 | 3 | public enum HelpMessageType 4 | { 5 | NoTags, 6 | QuestionLength, 7 | AllCapTitle, // Currently toLowers ALL CAP titles 8 | HelpInTitle, // Currently unused 9 | } 10 | 11 | public class HelpBotMessage 12 | { 13 | public ulong MessageId { get; set; } 14 | public HelpMessageType Type { get; set; } 15 | 16 | public HelpBotMessage(ulong messageId, HelpMessageType type) 17 | { 18 | MessageId = messageId; 19 | Type = type; 20 | } 21 | } -------------------------------------------------------------------------------- /DiscordBot/Services/UnityHelp/Components/ThreadContainer.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Services.UnityHelp; 2 | 3 | public class ThreadContainer 4 | { 5 | public ulong Owner { get; set; } 6 | public ulong FirstUserMessage { get; set; } 7 | public ulong ThreadId { get; set; } 8 | public ulong LatestUserMessage { get; set; } 9 | public ulong PinnedAnswer { get; set; } 10 | 11 | public bool IsResolved { get; set; } = false; 12 | public bool HasInteraction { get; set; } = false; 13 | 14 | 15 | public ulong BotsLastMessage { get; set; } 16 | public CancellationTokenSource CancellationToken { get; set; } 17 | public DateTime ExpectedShutdownTime { get; set; } 18 | 19 | /// 20 | /// Any message the bot sends that could need to be tracked/deleted later is stored here. 21 | /// 22 | public Dictionary HelpMessages { get; set; } = new(); 23 | 24 | public bool HasMessage(HelpMessageType type) => HelpMessages.ContainsKey(type); 25 | public ulong GetMessageId(HelpMessageType type) => HelpMessages[type].MessageId; 26 | public void AddMessage(HelpMessageType type, ulong messageId) => HelpMessages.Add(type, new HelpBotMessage(messageId, type)); 27 | public void RemoveMessage(HelpMessageType type) => HelpMessages.Remove(type); 28 | } -------------------------------------------------------------------------------- /DiscordBot/Services/UserExtendedService.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Services; 2 | 3 | /// 4 | /// May be renamed later. 5 | /// Current purpose is a cache for user data for "fun" commands which only includes DefaultCity behaviour. 6 | /// 7 | public class UserExtendedService 8 | { 9 | private readonly DatabaseService _databaseService; 10 | 11 | // Cached Information 12 | private Dictionary _cityCachedName = new(); 13 | 14 | public UserExtendedService(DatabaseService databaseService) 15 | { 16 | _databaseService = databaseService; 17 | } 18 | 19 | public async Task SetUserDefaultCity(IUser user, string city) 20 | { 21 | // Update Database 22 | await _databaseService.Query.UpdateDefaultCity(user.Id.ToString(), city); 23 | // Update Cache 24 | _cityCachedName[user.Id] = city; 25 | return true; 26 | } 27 | 28 | public async Task DoesUserHaveDefaultCity(IUser user) 29 | { 30 | // Quickest check if we have cached result 31 | if (_cityCachedName.ContainsKey(user.Id)) 32 | return true; 33 | 34 | // Check database 35 | var res = await _databaseService.Query.GetDefaultCity(user.Id.ToString()); 36 | if (string.IsNullOrEmpty(res)) 37 | return false; 38 | 39 | // Cache result 40 | _cityCachedName[user.Id] = res; 41 | return true; 42 | } 43 | 44 | public async Task GetUserDefaultCity(IUser user) 45 | { 46 | if (await DoesUserHaveDefaultCity(user)) 47 | return _cityCachedName[user.Id]; 48 | return ""; 49 | } 50 | 51 | public async Task RemoveUserDefaultCity(IUser user) 52 | { 53 | // Update Database 54 | await _databaseService.Query.UpdateDefaultCity(user.Id.ToString(), null); 55 | // Update Cache 56 | _cityCachedName.Remove(user.Id); 57 | return true; 58 | } 59 | } -------------------------------------------------------------------------------- /DiscordBot/Services/WeatherService.cs: -------------------------------------------------------------------------------- 1 | using Discord.WebSocket; 2 | using DiscordBot.Settings; 3 | using DiscordBot.Utils; 4 | using DiscordBot.Modules.Weather; 5 | 6 | namespace DiscordBot.Services; 7 | 8 | public class WeatherService 9 | { 10 | private const string ServiceName = "FeedService"; 11 | 12 | private readonly DiscordSocketClient _client; 13 | private readonly ILoggingService _loggingService; 14 | private readonly string _weatherApiKey; 15 | 16 | public WeatherService(DiscordSocketClient client, ILoggingService loggingService, BotSettings settings) 17 | { 18 | _client = client; 19 | _loggingService = loggingService; 20 | _weatherApiKey = settings.WeatherAPIKey; 21 | 22 | if (string.IsNullOrWhiteSpace(_weatherApiKey)) 23 | { 24 | _loggingService.LogAction($"[{ServiceName}] Error: Weather API Key is not set.", ExtendedLogSeverity.Warning); 25 | } 26 | } 27 | 28 | 29 | public async Task GetWeather(string city, string unit = "metric") 30 | { 31 | var query = $"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={_weatherApiKey}&units={unit}"; 32 | return await SerializeUtil.LoadUrlDeserializeResult(query); 33 | } 34 | 35 | public async Task GetPollution(double lon, double lat) 36 | { 37 | var query = $"https://api.openweathermap.org/data/2.5/air_pollution?lat={lat}&lon={lon}&appid={_weatherApiKey}"; 38 | return await SerializeUtil.LoadUrlDeserializeResult(query); 39 | } 40 | 41 | public async Task<(bool exists, WeatherContainer.Result result)> CityExists(string city) 42 | { 43 | var res = await GetWeather(city: city); 44 | var exists = !object.Equals(res, default(WeatherContainer.Result)); 45 | return (exists, res); 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /DiscordBot/Settings/Deserialized/ReactionRole.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Settings; 2 | 3 | public class ReactRoleSettings 4 | { 5 | public bool LogUpdates = false; 6 | public uint RoleAddDelay = 5000; // Delay in ms 7 | public List UserReactRoleList; 8 | } 9 | 10 | public class UserReactMessage 11 | { 12 | public ulong ChannelId; 13 | public ulong MessageId; 14 | public List Reactions; 15 | public string Description { get; set; } 16 | 17 | public int RoleCount() => Reactions?.Count ?? 0; 18 | } 19 | 20 | public class ReactRole 21 | { 22 | public string Name; 23 | 24 | public ReactRole(string name, ulong roleId, ulong emojiId) 25 | { 26 | Name = name; 27 | RoleId = roleId; 28 | EmojiId = emojiId; 29 | } 30 | 31 | public ulong RoleId { get; set; } 32 | public ulong EmojiId { get; set; } 33 | } -------------------------------------------------------------------------------- /DiscordBot/Settings/Deserialized/Rules.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Settings; 2 | 3 | public class Rules 4 | { 5 | public List Channel { get; set; } 6 | } 7 | 8 | public class ChannelData 9 | { 10 | public ulong Id { get; set; } 11 | public string Header { get; set; } 12 | public string Content { get; set; } 13 | } -------------------------------------------------------------------------------- /DiscordBot/Settings/Deserialized/Settings.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Settings; 2 | 3 | public class BotSettings 4 | { 5 | #region Important Settings 6 | 7 | public string Token { get; set; } 8 | public string Invite { get; set; } 9 | 10 | public string DbConnectionString { get; set; } 11 | public string ServerRootPath { get; set; } 12 | public char Prefix { get; set; } 13 | public ulong GuildId { get; set; } 14 | public bool LogCommandExecutions { get; set; } = true; 15 | 16 | #endregion // Important 17 | 18 | #region Configuration 19 | 20 | public int WelcomeMessageDelaySeconds { get; set; } = 300; 21 | public bool ModeratorCommandsEnabled { get; set; } 22 | public bool ModeratorNoInviteLinks { get; set; } 23 | // How long between when the bot will scold a user for trying to ping everyone. Default 6 hours 24 | public ulong EveryoneScoldPeriodSeconds { get; set; } = 21600; 25 | 26 | #region Fun Commands 27 | 28 | public List UserModuleSlapChoices { get; set; } = new List() { "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" }; 29 | 30 | #endregion // Fun Commands 31 | 32 | #region Service Enabling 33 | // Used for enabling/disabling services in the bot 34 | 35 | public bool RecruitmentServiceEnabled { get; set; } = false; 36 | public bool UnityHelpBabySitterEnabled { get; set; } = false; 37 | public bool ReactRoleServiceEnabled { get; set; } = false; 38 | public bool IntroductionWatcherServiceEnabled { get; set; } = false; 39 | 40 | #endregion // Service Enabling 41 | 42 | #endregion // Configuration 43 | 44 | #region Asset Publisher 45 | 46 | // Used for Asset Publisher 47 | 48 | public string Email { get; set; } 49 | public string EmailUsername { get; set; } 50 | public string EmailPassword { get; set; } 51 | public string EmailSMTPServer { get; set; } 52 | public int EmailSMTPPort { get; set; } 53 | 54 | #endregion // Asset Publisher 55 | 56 | #region Channels 57 | 58 | public ChannelInfo IntroductionChannel { get; set; } 59 | public ChannelInfo GeneralChannel { get; set; } 60 | public ChannelInfo GenericHelpChannel { get; set; } 61 | 62 | public ChannelInfo BotAnnouncementChannel { get; set; } 63 | public ChannelInfo AnnouncementsChannel { get; set; } 64 | public ChannelInfo BotCommandsChannel { get; set; } 65 | public ChannelInfo UnityNewsChannel { get; set; } 66 | public ChannelInfo UnityReleasesChannel { get; set; } 67 | public ChannelInfo RulesChannel { get; set; } 68 | 69 | // Recruitment Channels 70 | 71 | public ChannelInfo RecruitmentChannel { get; set; } 72 | 73 | public ChannelInfo ReportedMessageChannel { get; set; } 74 | 75 | public ChannelInfo MemeChannel { get; set; } 76 | 77 | #region Complaint Channel 78 | 79 | public ulong ComplaintCategoryId { get; set; } 80 | public string ComplaintChannelPrefix { get; set; } 81 | public ulong ClosedComplaintCategoryId { get; set; } 82 | public string ClosedComplaintChannelPrefix { get; set; } 83 | 84 | #endregion // Complaint Channel 85 | 86 | #region Auto-Threads 87 | 88 | public List AutoThreadChannels { get; set; } = new List(); 89 | public List AutoThreadExclusionPrefixes { get; set; } = new List(); 90 | 91 | #endregion // Auto-Threads 92 | 93 | #endregion // Channels 94 | 95 | #region User Roles 96 | 97 | public RoleGroup UserAssignableRoles { get; set; } 98 | public ulong MutedRoleId { get; set; } 99 | public ulong SubsReleasesRoleId { get; set; } 100 | public ulong SubsNewsRoleId { get; set; } 101 | public ulong PublisherRoleId { get; set; } 102 | public ulong ModeratorRoleId { get; set; } 103 | 104 | #endregion // User Roles 105 | 106 | #region Recruitment Thread 107 | 108 | public string TagLookingToHire { get; set; } 109 | public string TagLookingForWork { get; set; } 110 | public string TagUnpaidCollab { get; set; } 111 | public string TagPositionFilled { get; set; } 112 | 113 | public int EditPermissionAccessTimeMin { get; set; } = 3; 114 | 115 | #endregion // Recruitment Thread Tags 116 | 117 | #region Unity Help Threads 118 | 119 | #region Tips 120 | 121 | public string TipImageDirectory { get; set; } 122 | 123 | public int TipMaxImageFileSize { get; set; } = 1024 * 1024 * 10; // 10MB 124 | // Unlikely, but we prevent exploitation by limiting the max directory size to avoid VPS disk space issues 125 | public int TipMaxDirectoryFileSize { get; set; } = 1024 * 1024 * 1024; // 1GB 126 | 127 | #endregion // Tips 128 | 129 | public string TagUnitHelpResolvedTag { get; set; } 130 | 131 | #endregion // Unity Help Threads 132 | 133 | #region API Keys 134 | 135 | public string WeatherAPIKey { get; set; } 136 | 137 | public string FlightAPIKey { get; set; } 138 | public string FlightAPISecret { get; set; } 139 | public string FlightAPIId { get; set; } 140 | public string AirLabAPIKey { get; set; } 141 | 142 | #endregion // API Keys 143 | 144 | #region Other 145 | 146 | public string AssetStoreFrontPage { get; set; } 147 | public string WikipediaSearchPage { get; set; } 148 | 149 | #endregion // Other 150 | 151 | } 152 | 153 | #region Role Group Collections 154 | 155 | // Classes used to hold information regarding a collection of role ids with a description. 156 | public class RoleGroup 157 | { 158 | public string Desc { get; set; } 159 | public List Roles { get; set; } 160 | } 161 | 162 | #endregion 163 | 164 | #region Channel Information 165 | 166 | // Channel Information. Description and Channel ID 167 | public class ChannelInfo 168 | { 169 | public string Desc { get; set; } 170 | public ulong Id { get; set; } 171 | } 172 | 173 | public class AutoThreadChannel 174 | { 175 | public string Title { get; set; } 176 | public ulong Id { get; set; } 177 | public bool CanArchive { get; set; } = false; 178 | public bool CanDelete { get; set; } = false; 179 | public string TitleArchived { get; set; } 180 | public string FirstMessage { get; set; } 181 | public string Duration { get; set; } 182 | 183 | private static string AuthorName(IUser author) 184 | { 185 | return ((IGuildUser)author).Nickname ?? author.Username; 186 | } 187 | 188 | public string GenerateTitle(IUser author) 189 | { 190 | return String.Format(this.Title, AuthorName(author)); 191 | } 192 | 193 | public string GenerateTitleArchived(IUser author) 194 | { 195 | return String.Format(this.TitleArchived, AuthorName(author)); 196 | } 197 | 198 | public string GenerateFirstMessage(IUser author) 199 | { 200 | return String.Format(this.FirstMessage, author.Mention); 201 | } 202 | } 203 | 204 | #endregion 205 | -------------------------------------------------------------------------------- /DiscordBot/Settings/Deserialized/UserSettings.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Settings; 2 | 3 | public class UserSettings 4 | { 5 | public List Thanks { get; set; } = new List { "thanks", "ty", "thx", "thnx", "thanx", "thankyou", "thank you", "cheers" }; 6 | public int ThanksCooldown { get; set; } = 60; 7 | public int ThanksMinJoinTime { get; set; } = 600; 8 | 9 | public int XpMinPerMessage { get; set; } = 10; 10 | public int XpMaxPerMessage { get; set; } = 30; 11 | public int XpMinCooldown { get; set; } = 60; 12 | public int XpMaxCooldown { get; set; } = 180; 13 | 14 | public int CodeReminderCooldown { get; set; } = 86400; 15 | 16 | //TODO Introduce notice for asking for help "Can someone help" when they haven't posted in a couple minutes would be a giveaway that they should be reminded to post their question, and not just ask if someone is there. 17 | } -------------------------------------------------------------------------------- /DiscordBot/Settings/ReactionRoles.json: -------------------------------------------------------------------------------- 1 | { 2 | "RoleAddDelay": 5000, 3 | "LogUpdates": false, 4 | "UserReactRoleList": null 5 | } -------------------------------------------------------------------------------- /DiscordBot/Settings/Rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "channel": [ 3 | { 4 | "id": 0, 5 | "header": "", 6 | "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." 7 | }, 8 | { 9 | "id": 493511037421879316, 10 | "header": "Ask general Unity questions in this channel. Newbie questions welcome.\n", 11 | "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." 12 | }, 13 | { 14 | "id": 493511468134694912, 15 | "header": "This channel is for advanced C# questions and scripting related discussions (script optimization, naming convention...).", 16 | "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." 17 | }, 18 | { 19 | "id": 493511492226908181, 20 | "header": "Use this channel to ask questions about game networking and discuss networking in general.", 21 | "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." 22 | }, 23 | { 24 | "id": 493511590164037632, 25 | "header": "Use this channel to share your game development through screenshots, videos and devlogs.", 26 | "content": "\n- Keep posts short and sweet with a name and description of what we're looking at!" 27 | }, 28 | { 29 | "id": 493511791578578944, 30 | "header": "Use this channel to post resources and tutorials about Unity and its ecosystem.", 31 | "content": "\n- Only post link, no discussion around them in this channel." 32 | }, 33 | { 34 | "id": 493511872709001221, 35 | "header": "Use this channel if you're looking for a job. Please read the pins for a recommended template.", 36 | "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." 37 | }, 38 | { 39 | "id": 493511844904828990, 40 | "header": "Use this channel if you're looking to hire someone. Please read the pins for a recommended template.", 41 | "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." 42 | }, 43 | { 44 | "id": 493511883651809300, 45 | "header": "Use this channel if you want to collaborate for free. Please read the pins for a recommended template.", 46 | "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." 47 | }, 48 | { 49 | "id": 493511631557492736, 50 | "header": "Post here about your finished projects along with a link to download or purchase it.", 51 | "content": "\n- Do not repost projects multiple times\n- Keep discussion at a minimum to not drown out other projects" 52 | }, 53 | { 54 | "id": 493511757822820353, 55 | "header": "Use this channel to discuss all Not Safe For Work related development.", 56 | "content": "\n- This is a serious development channel, do not post memes here." 57 | }, 58 | { 59 | "id": 493511779499114517, 60 | "header": "Channel to discuss marketing strategy and business.", 61 | "content": "\n- Do not post your finished game links here, use #finished-projects" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /DiscordBot/Settings/Settings.example.json: -------------------------------------------------------------------------------- 1 | { 2 | /* Auth Info requires creating a Bot which can be done through https://discordapp.com/developers/applications/ (Make sure to give it Administrator Perms)*/ 3 | /* Auth info */ 4 | "token": "Y O U R _ B O T _ T O K E N", 5 | "invite": "InviteLink", // Currently Unused 6 | /* Gmail information for Asset Publisher Role to work. */ 7 | "gmail": "", 8 | "gmailUsername": "YourGmailUsername", 9 | "gmailPassword": "YourGmailPassword", 10 | /* 'SSL MODE' and 'Allow User Variables' are only required when running on a local machine with XAMPP. This can often be removed. */ 11 | /* DB Info*/ 12 | "DbConnectionString": "server=localhost;port=3306;database=test;user id=USER;Password=USERPASSWORD;SSL Mode=None;Allow User Variables=True", 13 | /*Server Info*/ 14 | "serverRootPath": "./SERVER", 15 | /* Base info */ 16 | "prefix": "!", 17 | "Administrator": "0", 18 | "ModeratorRoleId": "0", 19 | "guildId": "0", // Replace with your servers guild ID 20 | /* All assignable roles as of 29/04/21 */ 21 | "UserAssignableRoles": { 22 | "desc": "All normal user assignable roles available", 23 | "roles": [ 24 | "Audio-Engineers", 25 | "Technical-Artists", 26 | "Animators", 27 | "3D-Artists", 28 | "2D-Artists", 29 | "XR-Developers", 30 | "Programmers", 31 | "Writers", 32 | "Game-Designers", 33 | "Generalists", 34 | "Hobbyists", 35 | "Students" 36 | ] 37 | }, 38 | /* Channel IDs for certain channels. */ 39 | "generalChannel": { // Off-topic 40 | "desc": "General-Chat Channel", 41 | "id": "0" 42 | }, 43 | "IntroductionChannel": { // Introductions 44 | "desc": "Introductions Channel", 45 | "id": "0" 46 | }, 47 | "botAnnouncementChannel": { // Most bot logs will go here 48 | "desc": "Bot-Announcement Channel", 49 | "id": "0" 50 | }, 51 | "announcementsChannel": { // Not used by bot 52 | "desc": "General Announcement Channel", 53 | "id": "0" // Currently Unused 29/04/21 54 | }, 55 | "botCommandsChannel": { 56 | "desc": "Bot-Commands Channel", 57 | "id": "0" 58 | }, 59 | "unityNewsChannel": { 60 | "desc": "Unity News Channel", 61 | "id": "0" 62 | }, 63 | "ReportedMessageChannel": { 64 | "desc": "Reported Message Channel", 65 | "id": "0" 66 | }, 67 | /* Role Ids */ 68 | "mutedRoleID": "0", 69 | "publisherRoleID": "0", 70 | "SubsNewsRoleId": "0", 71 | "SubsReleasesRoleId": "0", 72 | /*Publisher Stuff*/ 73 | "assetStoreFrontPage": "https://www.assetstore.unity3d.com/en/", 74 | /*Complaints Channels Stuff*/ 75 | "complaintCategoryId": "0", 76 | "complaintChannelPrefix": "Complaint", 77 | "closedComplaintChannelPrefix": "Closed-", 78 | "closedComplaintCategoryId": "662084543662129175", 79 | /*Commands Configuration*/ 80 | "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=", 81 | "EveryoneScoldPeriodSeconds": "21600", 82 | /*API Keys*/ 83 | "WeatherAPIKey": "", // Key for openweathermap.org 84 | "FlightAPIKey": "", 85 | "FlightAPISecret": "", 86 | "AirLabAPIKey": "", 87 | /* Recruitment Service */ 88 | "RecruitmentServiceEnabled": false, 89 | "RecruitmentChannel": { // Recruitment 90 | "desc": "Channel for job postings", 91 | "id": "0" 92 | }, 93 | "TagLookingToHire": "0", 94 | "TagLookingForWork": "0", 95 | "TagUnpaidCollab": "0", 96 | "TagPositionFilled": "0", 97 | "EditPermissionAccessTimeMin": 3, 98 | /* Unity Help Service */ 99 | "UnityHelpBabySitterEnabled": false, 100 | "genericHelpChannel": { 101 | // Unity-help 102 | "desc": "Unity-Help Channel", 103 | "id": "0" 104 | }, 105 | /* React Role Service */ 106 | "ReactRoleServiceEnabled": false 107 | } -------------------------------------------------------------------------------- /DiscordBot/Settings/UserSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | /*Thanks parameters*/ 3 | "thanks": [ 4 | "thanks", 5 | "ty", 6 | "thx", 7 | "thnx", 8 | "thanx", 9 | "thankyou", 10 | "thank you", 11 | "cheers", 12 | "merci", 13 | "mersi", 14 | "gracias", 15 | "danke", 16 | "grazie", 17 | "arigatou", 18 | "有り難う", 19 | "有難う", 20 | "ありがとう", 21 | "どうも", 22 | "고맙습니다", 23 | "谢谢" 24 | ], 25 | "thanksCooldown": 60, //In seconds 26 | "thanksMinJoinTime": 600, 27 | /*Xp parameters*/ 28 | "xpMinPerMessage": 10, 29 | "xpMaxPerMessage": 30, 30 | "xpMinCooldown": 60, 31 | "xpMaxCooldown": 180, 32 | /*Code parameters*/ 33 | "codeReminderCooldown": 86400, 34 | "isSomeoneThere": [ 35 | "is anyone around?", 36 | "can someone help?", 37 | "can someone help me?" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /DiscordBot/Skin/AvatarBorderSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class AvatarBorderSkinModule : ISkinModule 7 | { 8 | public AvatarBorderSkinModule() 9 | { 10 | Size = 128; 11 | } 12 | 13 | public double StartX { get; set; } 14 | public double StartY { get; set; } 15 | public double Size { get; set; } 16 | 17 | public string Type { get; set; } 18 | 19 | public Drawables GetDrawables(ProfileData data) 20 | { 21 | var avatarContourStartX = StartX; 22 | var avatarContourStartY = StartY; 23 | var avatarContour = new RectangleD(avatarContourStartX - 2, avatarContourStartY - 2, 24 | avatarContourStartX + Size + 1, avatarContourStartY + Size + 1); 25 | 26 | return new Drawables() 27 | .StrokeColor(new MagickColor(data.MainRoleColor.R, data.MainRoleColor.G, data.MainRoleColor.B)) 28 | .FillColor(new MagickColor(data.MainRoleColor.R, data.MainRoleColor.G, data.MainRoleColor.B)) 29 | .Rectangle(avatarContour.UpperLeftX, avatarContour.UpperLeftY, avatarContour.LowerRightX, avatarContour.LowerRightY); 30 | } 31 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/BaseTextSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | 6 | namespace DiscordBot.Skin; 7 | 8 | public abstract class BaseTextSkinModule : ISkinModule 9 | { 10 | public BaseTextSkinModule() 11 | { 12 | StrokeWidth = 1; 13 | Font = "Consolas"; 14 | TextAntiAlias = true; 15 | StrokeAntiAlias = true; 16 | TextKerning = 0; 17 | } 18 | 19 | public double StartX { get; set; } 20 | public double StartY { get; set; } 21 | 22 | public bool StrokeAntiAlias { get; set; } 23 | public bool TextAntiAlias { get; set; } 24 | public string StrokeColor { get; set; } 25 | public double StrokeWidth { get; set; } 26 | public string FillColor { get; set; } 27 | public string Font { get; set; } 28 | public double FontPointSize { get; set; } 29 | public string Text { get; set; } 30 | public double TextKerning { get; set; } 31 | [JsonConverter(typeof(StringEnumConverter))] 32 | public TextAlignment TextAlignment { get; set; } 33 | 34 | public virtual string Type { get; set; } 35 | 36 | public virtual Drawables GetDrawables(ProfileData data) 37 | { 38 | var position = new PointD(StartX, StartY); 39 | 40 | return new Drawables() 41 | .FontPointSize(FontPointSize) 42 | .Font(Font) 43 | .StrokeColor(new MagickColor(StrokeColor)) 44 | .StrokeWidth(StrokeWidth) 45 | .StrokeAntialias(StrokeAntiAlias) 46 | .FillColor(new MagickColor(FillColor)) 47 | .TextAntialias(TextAntiAlias) 48 | .TextAlignment(TextAlignment) 49 | .TextKerning(TextKerning) 50 | .Text(position.X, position.Y, Text); 51 | } 52 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/CustomTextSkinModule.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using DiscordBot.Domain; 3 | using ImageMagick; 4 | 5 | namespace DiscordBot.Skin; 6 | 7 | public class CustomTextSkinModule : BaseTextSkinModule 8 | { 9 | public CustomTextSkinModule() 10 | { 11 | StrokeWidth = 1; 12 | FillColor = MagickColors.Black.ToString(); 13 | StrokeColor = MagickColors.Transparent.ToString(); 14 | Font = "Consolas"; 15 | FontPointSize = 15; 16 | } 17 | 18 | public override Drawables GetDrawables(ProfileData data) 19 | { 20 | var textPosition = new PointD(StartX, StartY); 21 | 22 | // Reflection to convert stuff like {Level} to data.Level 23 | var reg = new Regex(@"(?<=\{)(.*?)(?=\})"); 24 | var mc = reg.Matches(Text); 25 | foreach (var match in mc) 26 | { 27 | var prop = typeof(ProfileData).GetProperty(match.ToString()); 28 | if (prop == null) continue; 29 | var value = (dynamic)prop.GetValue(data, null); 30 | Text = Text.Replace("{" + match + "}", value.ToString()); 31 | } 32 | /* ALL properties of ProfileData.cs can be used! 33 | * Like {Level} for ProfileData.Level 34 | * Or {Nickname} for ProfileData.Nickname 35 | */ 36 | 37 | return new Drawables() 38 | .FontPointSize(FontPointSize) 39 | .Font(Font) 40 | .StrokeColor(new MagickColor(StrokeColor)) 41 | .StrokeWidth(StrokeWidth) 42 | .StrokeAntialias(StrokeAntiAlias) 43 | .FillColor(new MagickColor(FillColor)) 44 | .TextAlignment(TextAlignment) 45 | .TextAntialias(TextAntiAlias) 46 | .TextKerning(TextKerning) 47 | .Text(textPosition.X, textPosition.Y, $"{Text ?? Text}"); 48 | } 49 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/ISkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public interface ISkinModule 7 | { 8 | string Type { get; set; } 9 | 10 | Drawables GetDrawables(ProfileData data); 11 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/KarmaPointsSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class KarmaPointsSkinModule : BaseTextSkinModule 7 | { 8 | public KarmaPointsSkinModule() 9 | { 10 | StartX = 535; 11 | StartY = 130; 12 | StrokeColor = MagickColors.Transparent.ToString(); 13 | FillColor = MagickColors.Black.ToString(); 14 | FontPointSize = 17; 15 | } 16 | 17 | public override Drawables GetDrawables(ProfileData data) 18 | { 19 | Text = $"{data.Karma}"; 20 | return base.GetDrawables(data); 21 | } 22 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/KarmaRankSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class KarmaRankSkinModule : BaseTextSkinModule 7 | { 8 | public KarmaRankSkinModule() 9 | { 10 | StartX = 535; 11 | StartY = 153; 12 | StrokeColor = MagickColors.Transparent.ToString(); 13 | FillColor = MagickColors.Black.ToString(); 14 | FontPointSize = 17; 15 | } 16 | 17 | public override Drawables GetDrawables(ProfileData data) 18 | { 19 | Text = $"#{data.KarmaRank}"; 20 | return base.GetDrawables(data); 21 | } 22 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/LevelSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class LevelSkinModule : BaseTextSkinModule 7 | { 8 | public LevelSkinModule() 9 | { 10 | StartX = 220; 11 | StartY = 140; 12 | StrokeColor = MagickColors.IndianRed.ToString(); 13 | FillColor = MagickColors.IndianRed.ToString(); 14 | FontPointSize = 50; 15 | } 16 | 17 | public override Drawables GetDrawables(ProfileData data) 18 | { 19 | Text = data.Level.ToString(); 20 | return base.GetDrawables(data); 21 | } 22 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/RectangleD.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Skin; 2 | 3 | public struct RectangleD 4 | { 5 | public double UpperLeftX; 6 | public double UpperLeftY; 7 | public double LowerRightX; 8 | public double LowerRightY; 9 | 10 | public RectangleD(double upperLeftX, double upperLeftY, double lowerRightX, double lowerRightY) 11 | { 12 | UpperLeftX = upperLeftX; 13 | UpperLeftY = upperLeftY; 14 | LowerRightX = lowerRightX; 15 | LowerRightY = lowerRightY; 16 | } 17 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/RectangleSampleAvatarColorSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | /// 7 | /// Fill the background with the color based on the pfp 8 | /// 9 | public class RectangleSampleAvatarColorSkinModule : ISkinModule 10 | { 11 | public int StartX { get; set; } 12 | public int StartY { get; set; } 13 | public int Width { get; set; } 14 | public int Height { get; set; } 15 | public bool WhiteFix { get; set; } 16 | public string DefaultColor { get; set; } 17 | 18 | public string Type { get; set; } 19 | 20 | public Drawables GetDrawables(ProfileData data) 21 | { 22 | var color = DetermineColor(data.Picture); 23 | 24 | return new Drawables() 25 | .FillColor(color) 26 | .Rectangle(StartX, StartY, StartX + Width, StartY + Height); 27 | } 28 | 29 | private MagickColor DetermineColor(MagickImage dataPicture) 30 | { 31 | //basically we let magick to choose what the main color by resizing to 1x1 32 | var copy = new MagickImage(dataPicture); 33 | copy.Resize(1, 1); 34 | var color = copy.GetPixels()[0, 0].ToColor(); 35 | 36 | if (WhiteFix && color.R + color.G + color.B > 650) 37 | color = new MagickColor(DefaultColor); 38 | 39 | return color; 40 | } 41 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/SkinData.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Skin; 2 | 3 | public class SkinData 4 | { 5 | public SkinData() 6 | { 7 | Layers = new List(); 8 | } 9 | 10 | public string Name { get; set; } 11 | public string Codename { get; set; } 12 | public string Description { get; set; } 13 | public int AvatarSize { get; set; } 14 | public string Background { get; set; } 15 | public List Layers { get; set; } 16 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/SkinLayer.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Skin; 2 | 3 | public class SkinLayer 4 | { 5 | public SkinLayer() 6 | { 7 | Modules = new List(); 8 | } 9 | 10 | public string Image { get; set; } 11 | public double StartX { get; set; } 12 | public double StartY { get; set; } 13 | public double Width { get; set; } 14 | public double Height { get; set; } 15 | 16 | public List Modules { get; set; } 17 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/SkinModuleJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class SkinModuleJsonConverter : JsonConverter 7 | { 8 | public override bool CanWrite => false; 9 | 10 | public override bool CanConvert(Type objectType) => objectType == typeof(ISkinModule); 11 | 12 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 13 | { 14 | var jo = JObject.Load(reader); 15 | Type type; 16 | try 17 | { 18 | var t = $"DiscordBot.Skin.{jo["Type"].Value()}SkinModule"; 19 | type = Type.GetType(t); 20 | return jo.ToObject(type); 21 | } 22 | catch (Exception e) 23 | { 24 | LoggingService.LogToConsole($"{e.ToString()}", LogSeverity.Error); 25 | throw; 26 | } 27 | } 28 | 29 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/TotalXpSkinModule.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using DiscordBot.Domain; 3 | using ImageMagick; 4 | 5 | namespace DiscordBot.Skin; 6 | 7 | public class TotalXpSkinModule : BaseTextSkinModule 8 | { 9 | public TotalXpSkinModule() 10 | { 11 | StrokeColor = MagickColors.Transparent.ToString(); 12 | FillColor = MagickColors.Black.ToString(); 13 | FontPointSize = 17; 14 | } 15 | 16 | public override Drawables GetDrawables(ProfileData data) 17 | { 18 | Text = data.XpTotal.ToString("N0", new CultureInfo("en-US")); 19 | return base.GetDrawables(data); 20 | } 21 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/UsernameSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class UsernameSkinModule : BaseTextSkinModule 7 | { 8 | public UsernameSkinModule() 9 | { 10 | FontPointSize = 34; 11 | Font = "Consolas"; 12 | StrokeColor = MagickColors.BlueViolet.ToString(); 13 | FillColor = MagickColors.DeepSkyBlue.ToString(); 14 | } 15 | 16 | public override Drawables GetDrawables(ProfileData data) 17 | { 18 | Text = $"{data.Nickname ?? data.Username}"; 19 | return base.GetDrawables(data); 20 | } 21 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/XpBarInfoSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class XpBarInfoSkinModule : BaseTextSkinModule 7 | { 8 | public XpBarInfoSkinModule() 9 | { 10 | StrokeWidth = 1; 11 | FillColor = MagickColors.Black.ToString(); 12 | StrokeColor = MagickColors.Transparent.ToString(); 13 | FontPointSize = 17; 14 | } 15 | 16 | public override Drawables GetDrawables(ProfileData data) 17 | { 18 | Text = $"{data.XpShown:#,##0} / {data.MaxXpShown:N0} ({Math.Floor(data.XpPercentage * 100):0}%)"; 19 | return base.GetDrawables(data); 20 | } 21 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/XpBarSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class XpBarSkinModule : ISkinModule 7 | { 8 | public XpBarSkinModule() 9 | { 10 | OutsideStrokeColor = "#778899FF"; 11 | OutsideFillColor = "#F5F5F5FF"; 12 | InsideStrokeColor = "#FFFFFF00"; 13 | InsideFillColor = "#32CD32FF"; 14 | StrokeWidth = 1; 15 | Width = 200; 16 | Height = 20; 17 | } 18 | 19 | public double Width { get; set; } 20 | public double Height { get; set; } 21 | public double StartX { get; set; } 22 | public double StartY { get; set; } 23 | public double StrokeWidth { get; set; } 24 | public string OutsideStrokeColor { get; set; } 25 | public string OutsideFillColor { get; set; } 26 | public string InsideStrokeColor { get; set; } 27 | public string InsideFillColor { get; set; } 28 | 29 | public string Type { get; set; } 30 | 31 | public Drawables GetDrawables(ProfileData data) 32 | { 33 | var xpBarOutsideRectangle = new RectangleD(StartX, StartY, 34 | StartX + Width, StartY + Height); 35 | 36 | var xpBarInsideRectangle = 37 | new RectangleD(xpBarOutsideRectangle.UpperLeftX + 2, xpBarOutsideRectangle.UpperLeftY + 2, 38 | StartX + Width * data.XpPercentage - 2, xpBarOutsideRectangle.LowerRightY - 2); 39 | 40 | return new Drawables() 41 | //XP Bar Outside 42 | .StrokeColor(new MagickColor(OutsideStrokeColor)) 43 | .StrokeWidth(StrokeWidth) 44 | .FillColor(new MagickColor(OutsideFillColor)) 45 | .Rectangle(xpBarOutsideRectangle.UpperLeftX, xpBarOutsideRectangle.UpperLeftY, xpBarOutsideRectangle.LowerRightX, 46 | xpBarOutsideRectangle.LowerRightY) 47 | 48 | //XP Bar Inside 49 | .StrokeColor(new MagickColor(InsideStrokeColor)) 50 | .FillColor(new MagickColor(InsideFillColor)) 51 | .Rectangle(xpBarInsideRectangle.UpperLeftX, xpBarInsideRectangle.UpperLeftY, xpBarInsideRectangle.LowerRightX, 52 | xpBarInsideRectangle.LowerRightY); 53 | } 54 | } -------------------------------------------------------------------------------- /DiscordBot/Skin/XpRankSkinModule.cs: -------------------------------------------------------------------------------- 1 | using DiscordBot.Domain; 2 | using ImageMagick; 3 | 4 | namespace DiscordBot.Skin; 5 | 6 | public class XpRankSkinModule : BaseTextSkinModule 7 | { 8 | public XpRankSkinModule() 9 | { 10 | StrokeColor = MagickColors.Transparent.ToString(); 11 | FillColor = MagickColors.Black.ToString(); 12 | FontPointSize = 17; 13 | } 14 | 15 | public override Drawables GetDrawables(ProfileData data) 16 | { 17 | Text = $"#{data.XpRank}"; 18 | return base.GetDrawables(data); 19 | } 20 | } -------------------------------------------------------------------------------- /DiscordBot/Utils/MathUtility.cs: -------------------------------------------------------------------------------- 1 | namespace DiscordBot.Utils; 2 | 3 | public static class MathUtility 4 | { 5 | public static float CelsiusToFahrenheit(float value) 6 | { 7 | return (float)Math.Round(value * 1.8f + 32, 2); 8 | } 9 | 10 | public static float FahrenheitToCelsius(float value) 11 | { 12 | return (float)Math.Round((value - 32) * 0.555555f, 2); 13 | } 14 | } -------------------------------------------------------------------------------- /DiscordBot/Utils/SerializeUtil.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Newtonsoft.Json; 3 | 4 | namespace DiscordBot.Utils; 5 | 6 | public static class SerializeUtil 7 | { 8 | public static T DeserializeFile(string path, bool newFileIfNotExists = true) where T : new() 9 | { 10 | // Check if file exists, 11 | if (!File.Exists(path)) 12 | { 13 | if (newFileIfNotExists) 14 | { 15 | LoggingService.LogToConsole($@"Deserialized File at '{path}' does not exist, attempting to generate new file.", 16 | LogSeverity.Warning); 17 | var deserializedItem = new T(); 18 | File.WriteAllText(path, JsonConvert.SerializeObject(deserializedItem)); 19 | } 20 | else 21 | { 22 | LoggingService.LogToConsole($@"Deserialized File at '{path}' does not exist.", LogSeverity.Error); 23 | } 24 | } 25 | 26 | using var file = File.OpenText(path); 27 | var content = JsonConvert.DeserializeObject(file.ReadToEnd()) ?? new T(); 28 | return content; 29 | } 30 | 31 | /// Tests objectToSerialize to confirm not null before saving it to path. 32 | public static bool SerializeFile(string path, T objectToSerialize) 33 | { 34 | if (object.Equals(objectToSerialize, default(T))) 35 | { 36 | LoggingService.LogToConsole($"Object `{path}` passed into SerializeFile is null, ignoring save request.", 37 | LogSeverity.Warning); 38 | return false; 39 | } 40 | 41 | File.WriteAllText(path, JsonConvert.SerializeObject(objectToSerialize)); 42 | return true; 43 | } 44 | 45 | public static async Task SerializeFileAsync(string path, T objectToSerialize) 46 | { 47 | if (object.Equals(objectToSerialize, default(T))) 48 | { 49 | LoggingService.LogToConsole($"Object `{path}` passed into SerializeFile is null, ignoring save request.", 50 | LogSeverity.Warning); 51 | return false; 52 | } 53 | await File.WriteAllTextAsync(path, JsonConvert.SerializeObject(objectToSerialize)); 54 | return true; 55 | } 56 | 57 | public static async Task LoadUrlDeserializeResult(string url) 58 | { 59 | var result = await InternetExtensions.GetHttpContents(url); 60 | var resultObject = JsonConvert.DeserializeObject(result); 61 | if (resultObject == null) 62 | { 63 | if (result?.Length > 400) 64 | result = result.Substring(0, 400) + "..."; 65 | LoggingService.LogToConsole($"Failed to deserialize object from {url}\nContent: {result}", LogSeverity.Error); 66 | } 67 | return resultObject; 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /DiscordBot/Utils/StringUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace DiscordBot.Utils; 4 | 5 | public static class StringUtil 6 | { 7 | private static readonly Regex CurrencyRegex = 8 | new (@"(?:\$\s*\d+|\d+\s*\$|\d*\s*(?:USD|£|pounds|€|EUR|euro|euros|GBP|円|YEN))", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250)); 9 | private static readonly Regex RevShareRegex = new (@"\b(?:rev-share|revshare|rev share)\b", RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250)); 10 | 11 | // a string extension that checks if the contents of the string contains a limited selection of currency symbols/words 12 | public static bool ContainsCurrencySymbol(this string str) 13 | { 14 | return !string.IsNullOrWhiteSpace(str) && CurrencyRegex.IsMatch(str); 15 | } 16 | 17 | public static bool ContainsRevShare(this string str) 18 | { 19 | return !string.IsNullOrWhiteSpace(str) && RevShareRegex.IsMatch(str); 20 | } 21 | 22 | public static string MessageSelfDestructIn(int secondsFromNow) 23 | { 24 | var time = DateTime.Now.ToUnixTimestamp() + secondsFromNow; 25 | return $"Self-delete: ****"; 26 | } 27 | 28 | /// 29 | /// Sanitizes @everyone and @here mentions by adding a zero-width space after the @ symbol. 30 | /// 31 | public static string SanitizeEveryoneHereMentions(this string str) 32 | { 33 | return str.Replace("@everyone", "@\u200beveryone").Replace("@here", "@\u200bhere"); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /DiscordBot/Utils/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace DiscordBot.Utils; 5 | 6 | public static class Utils 7 | { 8 | public static string FormatTime(uint seconds) 9 | { 10 | var span = TimeSpan.FromSeconds(seconds); 11 | if (span.TotalSeconds == 0) return "0 seconds"; 12 | 13 | var parts = new List(); 14 | 15 | int days = span.Days; 16 | // if days is over a year 17 | if (days >= 365) 18 | { 19 | int years = days / 365; 20 | parts.Add($"{years} year{(years > 1 ? "s" : "")}"); 21 | days %= 365; 22 | } 23 | 24 | if (days > 0) parts.Add($"{days} day{(days > 1 ? "s" : "")}"); 25 | 26 | if (span.Hours > 0) parts.Add($"{span.Hours} hour{(span.Hours > 1 ? "s" : "")}"); 27 | 28 | if (span.Minutes > 0) parts.Add($"{span.Minutes} minute{(span.Minutes > 1 ? "s" : "")}"); 29 | 30 | if (span.Seconds > 0) parts.Add($"{span.Seconds} second{(span.Seconds > 1 ? "s" : "")}"); 31 | 32 | var finishedTime = string.Empty; 33 | for (var i = 0; i < parts.Count; i++) 34 | { 35 | if (i > 0) 36 | { 37 | if (i == parts.Count - 1) 38 | finishedTime += " and "; 39 | else 40 | finishedTime += ", "; 41 | } 42 | 43 | finishedTime += parts[i]; 44 | } 45 | 46 | return finishedTime; 47 | } 48 | 49 | /// 50 | /// Sanitize XML, from https://seattlesoftware.wordpress.com/2008/09/11/hexadecimal-value-0-is-an-invalid-character/ 51 | /// 52 | /// 53 | /// 54 | /// 55 | public static string SanitizeXml(string xml) 56 | { 57 | if (xml == null) throw new ArgumentNullException("xml"); 58 | 59 | var buffer = new StringBuilder(xml.Length); 60 | 61 | foreach (var c in xml) 62 | if (IsLegalXmlChar(c)) 63 | buffer.Append(c); 64 | 65 | return buffer.ToString(); 66 | } 67 | 68 | /// 69 | /// Whether a given character is allowed by XML 1.0. 70 | /// 71 | public static bool IsLegalXmlChar(int character) => 72 | character == 0x9 /* == '\t' == 9 */ || 73 | character == 0xA /* == '\n' == 10 */ || 74 | character == 0xD /* == '\r' == 13 */ || 75 | character >= 0x20 && character <= 0xD7FF || 76 | character >= 0xE000 && character <= 0xFFFD || 77 | character >= 0x10000 && character <= 0x10FFFF; 78 | 79 | public static ThreadArchiveDuration GetMaxThreadDuration(ThreadArchiveDuration wantedDuration, IGuild guild) 80 | { 81 | var maxDuration = ThreadArchiveDuration.OneDay; 82 | if (guild.PremiumTier >= PremiumTier.Tier2) maxDuration = ThreadArchiveDuration.OneWeek; 83 | else if (guild.PremiumTier >= PremiumTier.Tier1) maxDuration = ThreadArchiveDuration.ThreeDays; 84 | 85 | if (wantedDuration > maxDuration) return maxDuration; 86 | return wantedDuration; 87 | } 88 | 89 | // Returns a datetime from a string using common date terms, ie; '1 year 40 days', '30 minutes 10 seconds', '10m 1d 400s', '1d 10h' 90 | public static DateTime ParseTimeFromString(string time) 91 | { 92 | var timeSpan = TimeSpan.Zero; 93 | var timeSpanRegex = new Regex(@"(?\d+) *(?[^\d\W]+)"); 94 | var matches = timeSpanRegex.Matches(time); 95 | foreach (Match match in matches) 96 | { 97 | var value = int.Parse(match.Groups["value"].Value); 98 | var unit = match.Groups["unit"].Value; 99 | switch (unit) 100 | { 101 | case "s": 102 | case "sec": 103 | case "second": 104 | case "seconds": 105 | timeSpan += TimeSpan.FromSeconds(value); 106 | break; 107 | case "m": 108 | case "min": 109 | case "minute": 110 | case "minutes": 111 | timeSpan += TimeSpan.FromMinutes(value); 112 | break; 113 | case "h": 114 | case "hour": 115 | case "hours": 116 | timeSpan += TimeSpan.FromHours(value); 117 | break; 118 | case "d": 119 | case "day": 120 | case "days": 121 | timeSpan += TimeSpan.FromDays(value); 122 | break; 123 | case "w": 124 | case "week": 125 | case "weeks": 126 | timeSpan += TimeSpan.FromDays(value * 7); 127 | break; 128 | case "mo": 129 | case "month": 130 | case "months": 131 | timeSpan += TimeSpan.FromDays(value * 30); 132 | break; 133 | case "y": 134 | case "year": 135 | case "years": 136 | timeSpan += TimeSpan.FromDays(value * 365); 137 | break; 138 | } 139 | } 140 | return DateTime.Now + timeSpan; 141 | } 142 | 143 | /// 144 | /// Very dirty way to strip html tags, removes anything inside < and > and replaces it with nothing 145 | /// 146 | public static string RemoveHtmlTags(string contents) 147 | { 148 | if (string.IsNullOrEmpty(contents)) return contents; 149 | var regex = new Regex("<.*?>"); 150 | return regex.Replace(contents, string.Empty); 151 | } 152 | 153 | public static string MessageLinkBack(ulong guildId, ulong channelId, ulong messageId) 154 | { 155 | return $"https://discordapp.com/channels/{guildId}/{channelId}/{messageId}"; 156 | } 157 | } -------------------------------------------------------------------------------- /DiscordBot/Utils/WebUtil.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using HtmlAgilityPack; 4 | using Newtonsoft.Json; 5 | 6 | namespace DiscordBot.Utils; 7 | 8 | public static class WebUtil 9 | { 10 | /// 11 | /// Returns the content of a URL as a string, or an empty string if the request fails. 12 | /// 13 | public static async Task GetContent(string url) 14 | { 15 | using var client = new HttpClient(); 16 | try 17 | { 18 | var response = await client.GetAsync(url); 19 | return await response.Content.ReadAsStringAsync(); 20 | } 21 | catch (Exception e) 22 | { 23 | LoggingService.LogToConsole($"[WebUtil] Failed to get content from {url}: {e.Message}", ExtendedLogSeverity.LowWarning); 24 | return ""; 25 | } 26 | } 27 | 28 | /// 29 | /// Returns the Html document of a url, or null if the request fails. 30 | /// Internally calls GetContent and parses the result. 31 | /// 32 | public static async Task GetHtmlDocument(string url) 33 | { 34 | try 35 | { 36 | var html = await GetContent(url); 37 | var doc = new HtmlDocument(); 38 | doc.LoadHtml(html); 39 | return doc; 40 | } 41 | catch (Exception e) 42 | { 43 | return null; 44 | } 45 | } 46 | 47 | /// 48 | /// Returns the Html node of a url and xpath, or null if the request fails. 49 | /// Internally calls GetHtmlDocument and parses the result with xpath. 50 | /// 51 | public static async Task GetHtmlNode(string url, string xpath) 52 | { 53 | try 54 | { 55 | var doc = await GetHtmlDocument(url); 56 | return doc.DocumentNode.SelectSingleNode(xpath); 57 | } 58 | catch (Exception e) 59 | { 60 | return null; 61 | } 62 | } 63 | 64 | /// 65 | /// Returns the Html nodes of a url and xpath, or null if the request fails. 66 | /// 67 | public static async Task GetHtmlNodes(string url, string xpath) 68 | { 69 | try 70 | { 71 | var doc = await GetHtmlDocument(url); 72 | return doc.DocumentNode.SelectNodes(xpath); 73 | } 74 | catch (Exception e) 75 | { 76 | return null; 77 | } 78 | } 79 | 80 | /// 81 | /// Returns the decoded inner text of a url and xpath, or an empty string if the request fails. 82 | /// 83 | public static async Task GetHtmlNodeInnerText(string url, string xpath) 84 | { 85 | try 86 | { 87 | var node = await GetHtmlNode(url, xpath); 88 | return WebUtility.HtmlDecode(node?.InnerText); 89 | } 90 | catch (Exception e) 91 | { 92 | return string.Empty; 93 | } 94 | } 95 | 96 | /// 97 | /// Returns the content of a url as a sanitized XML string, or an empty string if the request fails. 98 | /// 99 | public static async Task GetXMLContent(string url) 100 | { 101 | try 102 | { 103 | var content = await GetContent(url); 104 | // We check if we're dealing with XML and sanitize it, otherwise we just return the content 105 | if (content.StartsWith(" 117 | /// Returns a deserialized object from a JSON string. If the string is empty or can't be deserialized, it returns the default value of the type. 118 | /// 119 | public static async Task GetObjectFromJson(string url) 120 | { 121 | try 122 | { 123 | var content = await GetContent(url); 124 | return JsonConvert.DeserializeObject(content) ?? default; 125 | } 126 | catch (Exception e) 127 | { 128 | LoggingService.LogToConsole($"[WebUtil] Failed to get content from {url}: {e.Message}", ExtendedLogSeverity.LowWarning); 129 | return default; 130 | } 131 | } 132 | 133 | /// 134 | /// Returns a deserialized object from a JSON string, or null if the string is empty or can't be deserialized. 135 | /// 136 | public static async Task<(bool success, T result)> TryGetObjectFromJson(string url) 137 | { 138 | try 139 | { 140 | var content = await GetContent(url); 141 | var result = JsonConvert.DeserializeObject(content); 142 | return (true, result); 143 | } 144 | catch (Exception e) 145 | { 146 | LoggingService.LogToConsole($"[WebUtil] Failed to get content from {url}: {e.Message}", ExtendedLogSeverity.LowWarning); 147 | return (false, default); 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Builds application using dotnet's sdk 2 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 3 | 4 | WORKDIR / 5 | COPY ./DiscordBot/ ./app/ 6 | WORKDIR /app/ 7 | 8 | RUN dotnet restore 9 | RUN dotnet build --configuration Release --no-restore 10 | 11 | 12 | # Build finale image 13 | FROM mcr.microsoft.com/dotnet/runtime:6.0 14 | 15 | WORKDIR /app/ 16 | 17 | COPY --from=build /app/bin/Release/net6.0/ ./ 18 | 19 | RUN echo "deb http://httpredir.debian.org/debian buster main contrib" > /etc/apt/sources.list 20 | RUN echo "deb http://security.debian.org/ buster/updates main contrib" >> /etc/apt/sources.list 21 | RUN echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" | debconf-set-selections 22 | RUN apt update 23 | RUN apt install -y ttf-mscorefonts-installer 24 | RUN apt clean 25 | RUN apt autoremove -y 26 | RUN rm -rf /var/lib/apt/lists/ 27 | 28 | ENTRYPOINT ["./DiscordBot"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sirush, https://github.com/Sirush/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UDC-Bot 2 | 3 | A [Discord.NET](https://github.com/discord-net/Discord.Net) bot made for the server Unity Developer Community 4 | Join us on [Discord](https://discord.gg/bu3bbby) ! 5 | 6 | The code is provided as-is and there will be no guaranteed support to help make it run. 7 | 8 | # Table Of Contents 9 | 10 | - [Compiling](#compiling) 11 | - [Dependencies](#dependencies) 12 | - [Running](#running) 13 | - [Docker](#docker) 14 | - [Runtime Dependencies](#runtime-dependencies) 15 | - [Notes](#notes) 16 | - [Logging](#logging) 17 | - [Discord.Net](#discordnet) 18 | - [FAQ](#faq) 19 | 20 | ## Compiling 21 | 22 | ### Dependencies 23 | 24 | To successfully compile you will need the following. 25 | - [Discord.Net](https://www.nuget.org/packages/Discord.Net/) 26 | - [Visual Studio](https://visualstudio.microsoft.com/vs/community/) (or IDE of choice) 27 | - [.NET Core SDK](https://www.microsoft.com/net/download/core) 28 | - [Docker](https://www.docker.com/get-started) (Optional, but recommended) 29 | 30 | >NOTE: It is highly recommended to install docker and docker-compose to run at least the database in a container for local development. 31 | 32 | ## Running 33 | 34 | Once the above has been done, you'll need to setup a few additional things to get the bot to a functional point. 35 | 36 | - Copy the "**Server**" folder to the build location. If you run the bot, it'll inform you where that location is as an error. 37 | - Copy the "**Settings**" to the Server folder, without the "**Deserialized**" folder. 38 | - Make another copy of the "**Settings.example.json**" and rename it to "**Settings.json**" 39 | - Walk through the new "**Settings.json**" file and ensure the Bot Token and "**DbConnectionString**" have been set. 40 | 41 | _Several comments have been placed throughout the settings file to indicate what needs changing as well as the settings which aren't currently used._ 42 | 43 | _If you plan on running the bot outside of the IDE, you will want to give [Discord Net Deployment](https://discord.foxbot.me/docs/guides/deployment/deployment.html) a read._ 44 | 45 | ### Docker 46 | Running docker should be as simple as running `docker-compose up` in the root directory of the project. This will use the docker-compose.yml file to build the bot and database images and run them. You may need to change the DbConnectionString in the [settings.json](https://github.com/Unity-Developer-Community/UDC-Bot/tree/dev/DiscordBot/Settings) file to match the docker-compose.yml file configuration. 47 | 48 | > Note: Docker will also create an image of the bot, it may be much quicker to run the bot yourself from the IDE if you're making changes. 49 | > You can also use `docker-compose up --build` to force a rebuild of the bot image, but I would advise to disable the bot in the docker-compose.yml file, and run the bot from the IDE. 50 | 51 | ### Runtime Dependencies 52 | 53 | If you choose to not use docker, you will need to host the database yourself and ensure the bot can connect to it. You will need an accessible SQL database setup, the easiest way to do this is using XAMPP which can host the database and make it easier to setup. 54 | 55 | - [XAMPP](https://www.apachefriends.org/download.html) 56 | 57 | If you run the bot now, it will attempt to generate the table for the database. Depending on the permisions of the user, it may fail. You can get around by importing one of the tables below through phpmyadmin. 58 | 59 | - ~~**Emptyusers.sql** An empty table that only creates the database structure.~~ (Not yet) 60 | - ~~**Mockusers.sql** A table that creates the database structure, but contains some mock user data as well.~~ (Not yet) 61 | 62 | _Once you have imported the database, be sure to create a user in phpmyadmin which can access the database, and the details match your **DbConnectionString**._ 63 | 64 | On Linux you might need `sudo apt install ttf-mscorefonts-installer` for ImageMagick. 65 | 66 | ## Notes 67 | 68 | ### Logging 69 | 70 | The bot does attempt to log issues it runs into during startup, not everything is covered yet and a lot of it is stripped from the release build. 71 | A basic Enum is used, Critical/Error uses Red, Warning uses Yellow, Info is White and Verbose/Debug is Gray. ([LoggingService](https://github.com/Unity-Developer-Community/UDC-Bot/blob/dev/DiscordBot/Services/LoggingService.cs#L71)) 72 | 73 | During startup, if you see any yellow/red, good chance something is wrong. 74 | 75 | ### Discord.Net 76 | 77 | I strongly suggest giving [Discord.Net API Documention](https://discordnet.dev/guides/introduction/intro.html) a read when interacting with systems you haven't seen before. Discord Net uses Tasks, Asynchronous Patterns and heavy use of Polymorphism, some systems might not always be straight forward. 78 | 79 | ## FAQ 80 | 81 | None yet, ask some, I might add them, additional help isn't garenteed. ~~get gud~~ 82 | -------------------------------------------------------------------------------- /UpgradeLog.htm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Unity-Developer-Community/UDC-Bot/163f4fb222c08565e3a9cb1aeedf77d378bb5157/UpgradeLog.htm -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | db: 4 | image: mysql 5 | volumes: 6 | - db_data:/var/lib/mysql 7 | restart: always 8 | ports: 9 | - 127.0.0.1:3306:3306 10 | #- 3306:3306 11 | environment: 12 | MYSQL_ROOT_PASSWORD: 123456789 13 | MYSQL_DATABASE: udcbot 14 | MYSQL_USER: udcbot 15 | MYSQL_PASSWORD: 123456789 16 | 17 | phpmyadmin: 18 | image: phpmyadmin 19 | depends_on: 20 | - db 21 | restart: always 22 | ports: 23 | - 8080:80 24 | environment: 25 | PMA_HOST: db 26 | 27 | bot: 28 | build: . 29 | volumes: 30 | - .\DiscordBot\Settings\:/app/Settings 31 | - .\DiscordBot\SERVER\:/app/SERVER 32 | depends_on: 33 | - db 34 | restart: always 35 | 36 | volumes: 37 | db_data: 38 | --------------------------------------------------------------------------------