├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── phpstan.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── fileTemplates │ └── includes │ │ ├── PHP File Header.php │ │ └── PHP Property Doc Comment.php ├── inspectionProfiles │ └── Project_Default.xml ├── jsonSchemas.xml └── vcs.xml ├── .poggit.yml ├── LICENSE ├── composer.json ├── composer.lock ├── phpstan.neon.dist ├── plugin.yml ├── resources ├── config.yml └── help │ ├── english.txt │ └── french.txt ├── src ├── ConfigUtils.php ├── EventListener.php └── Main.php └── test └── github └── get-php.sh /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve the plugin. 4 | title: "[Bug] " 5 | labels: 'Status: Unconfirmed, Type: Bug' 6 | assignees: JaxkDev 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Plugins (please complete the following information):** 27 | - ChatBridge Version: [e.g. v1.x.y] 28 | - DiscordBot Version: [e.g. v2.x.y] 29 | 30 | **Server (please complete the following information):** 31 | - Run `/ver` in-game/in the console and copy *ALL* of the content to this section. 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | - '.github/workflows/*' 8 | - 'test/github/*' 9 | 10 | jobs: 11 | get-php: 12 | name: Download PHP 13 | runs-on: ubuntu-20.04 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Check for PHP cache 18 | id: php-cache 19 | uses: actions/cache@v3 20 | with: 21 | path: "./bin" 22 | key: "php-cache-${{ hashFiles('./test/github/get-php.sh') }}" 23 | 24 | - name: Download PHP 25 | if: steps.php-cache.outputs.cache-hit != 'true' 26 | run: ./test/github/get-php.sh 27 | 28 | phpstan: 29 | name: PHPStan analysis 30 | needs: get-php 31 | runs-on: ubuntu-20.04 32 | 33 | steps: 34 | - uses: actions/checkout@v3 35 | 36 | - name: Restore PHP cache 37 | id: php-cache 38 | uses: actions/cache@v3 39 | with: 40 | path: "./bin" 41 | key: "php-cache-${{ hashFiles('./test/github/get-php.sh') }}" 42 | 43 | - name: Prefix PHP to PATH 44 | run: echo "$(pwd)/bin/php7/bin" >> $GITHUB_PATH 45 | 46 | - name: Install Composer 47 | run: curl -sS https://getcomposer.org/installer | php 48 | 49 | - name: Restore Composer package cache 50 | id: composer-cache 51 | uses: actions/cache@v3 52 | with: 53 | path: | 54 | ~/.cache/composer/files 55 | ~/.cache/composer/vcs 56 | ./vendor 57 | key: "composer-v3-cache-${{ hashFiles('./composer.lock') }}" 58 | restore-keys: | 59 | composer-v3-cache- 60 | 61 | - name: Update Composer dependencies 62 | run: php composer.phar update --no-interaction 63 | 64 | - name: Run PHPStan 65 | run: php ./vendor/phpstan/phpstan/phpstan.phar analyze --no-progress --memory-limit=2G --error-format=github 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /vendor/ 3 | /dist/ 4 | 5 | *.phar 6 | 7 | BUILD.php 8 | TEST.php 9 | PHPSTAN.php 10 | phpstan.neon 11 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 33 | 34 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/fileTemplates/includes/PHP File Header.php: -------------------------------------------------------------------------------- 1 | /* 2 | * ChatBridge, PocketMine-MP Plugin. 3 | * 4 | * Licensed under the Open Software License version 3.0 (OSL-3.0) 5 | * Copyright (C) 2020-present JaxkDev 6 | * 7 | * Twitter :: @JaxkDev 8 | * Discord :: JaxkDev#2698 9 | * Email :: JaxkDev@gmail.com 10 | */ -------------------------------------------------------------------------------- /.idea/fileTemplates/includes/PHP Property Doc Comment.php: -------------------------------------------------------------------------------- 1 | /** @var ${TYPE_HINT} */ -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /.idea/jsonSchemas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.poggit.yml: -------------------------------------------------------------------------------- 1 | --- # Poggit-CI Manifest. Open the CI at https://poggit.pmmp.io/ci/DiscordBot-PMMP/ChatBridge 2 | build-by-default: true 3 | branches: 4 | - stable 5 | projects: 6 | ChatBridge: 7 | path: "" 8 | ... 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Open Software License ("OSL") v. 3.0 2 | 3 | This Open Software License (the "License") applies to any original work of 4 | authorship (the "Original Work") whose owner (the "Licensor") has placed the 5 | following licensing notice adjacent to the copyright notice for the Original 6 | Work: 7 | 8 | Licensed under the Open Software License version 3.0 9 | 10 | 1) Grant of Copyright License. Licensor grants You a worldwide, royalty-free, 11 | non-exclusive, sublicensable license, for the duration of the copyright, to do 12 | the following: 13 | 14 | a) to reproduce the Original Work in copies, either alone or as part of a 15 | collective work; 16 | 17 | b) to translate, adapt, alter, transform, modify, or arrange the Original 18 | Work, thereby creating derivative works ("Derivative Works") based upon the 19 | Original Work; 20 | 21 | c) to distribute or communicate copies of the Original Work and Derivative 22 | Works to the public, with the proviso that copies of Original Work or 23 | Derivative Works that You distribute or communicate shall be licensed under 24 | this Open Software License; 25 | 26 | d) to perform the Original Work publicly; and 27 | 28 | e) to display the Original Work publicly. 29 | 30 | 2) Grant of Patent License. Licensor grants You a worldwide, royalty-free, 31 | non-exclusive, sublicensable license, under patent claims owned or controlled 32 | by the Licensor that are embodied in the Original Work as furnished by the 33 | Licensor, for the duration of the patents, to make, use, sell, offer for sale, 34 | have made, and import the Original Work and Derivative Works. 35 | 36 | 3) Grant of Source Code License. The term "Source Code" means the preferred 37 | form of the Original Work for making modifications to it and all available 38 | documentation describing how to modify the Original Work. Licensor agrees to 39 | provide a machine-readable copy of the Source Code of the Original Work along 40 | with each copy of the Original Work that Licensor distributes. Licensor 41 | reserves the right to satisfy this obligation by placing a machine-readable 42 | copy of the Source Code in an information repository reasonably calculated to 43 | permit inexpensive and convenient access by You for as long as Licensor 44 | continues to distribute the Original Work. 45 | 46 | 4) Exclusions From License Grant. Neither the names of Licensor, nor the names 47 | of any contributors to the Original Work, nor any of their trademarks or 48 | service marks, may be used to endorse or promote products derived from this 49 | Original Work without express prior permission of the Licensor. Except as 50 | expressly stated herein, nothing in this License grants any license to 51 | Licensor's trademarks, copyrights, patents, trade secrets or any other 52 | intellectual property. No patent license is granted to make, use, sell, offer 53 | for sale, have made, or import embodiments of any patent claims other than the 54 | licensed claims defined in Section 2. No license is granted to the trademarks 55 | of Licensor even if such marks are included in the Original Work. Nothing in 56 | this License shall be interpreted to prohibit Licensor from licensing under 57 | terms different from this License any Original Work that Licensor otherwise 58 | would have a right to license. 59 | 60 | 5) External Deployment. The term "External Deployment" means the use, 61 | distribution, or communication of the Original Work or Derivative Works in any 62 | way such that the Original Work or Derivative Works may be used by anyone 63 | other than You, whether those works are distributed or communicated to those 64 | persons or made available as an application intended for use over a network. 65 | As an express condition for the grants of license hereunder, You must treat 66 | any External Deployment by You of the Original Work or a Derivative Work as a 67 | distribution under section 1(c). 68 | 69 | 6) Attribution Rights. You must retain, in the Source Code of any Derivative 70 | Works that You create, all copyright, patent, or trademark notices from the 71 | Source Code of the Original Work, as well as any notices of licensing and any 72 | descriptive text identified therein as an "Attribution Notice." You must cause 73 | the Source Code for any Derivative Works that You create to carry a prominent 74 | Attribution Notice reasonably calculated to inform recipients that You have 75 | modified the Original Work. 76 | 77 | 7) Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that 78 | the copyright in and to the Original Work and the patent rights granted herein 79 | by Licensor are owned by the Licensor or are sublicensed to You under the 80 | terms of this License with the permission of the contributor(s) of those 81 | copyrights and patent rights. Except as expressly stated in the immediately 82 | preceding sentence, the Original Work is provided under this License on an "AS 83 | IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without 84 | limitation, the warranties of non-infringement, merchantability or fitness for 85 | a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK 86 | IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this 87 | License. No license to the Original Work is granted by this License except 88 | under this disclaimer. 89 | 90 | 8) Limitation of Liability. Under no circumstances and under no legal theory, 91 | whether in tort (including negligence), contract, or otherwise, shall the 92 | Licensor be liable to anyone for any indirect, special, incidental, or 93 | consequential damages of any character arising as a result of this License or 94 | the use of the Original Work including, without limitation, damages for loss 95 | of goodwill, work stoppage, computer failure or malfunction, or any and all 96 | other commercial damages or losses. This limitation of liability shall not 97 | apply to the extent applicable law prohibits such limitation. 98 | 99 | 9) Acceptance and Termination. If, at any time, You expressly assented to this 100 | License, that assent indicates your clear and irrevocable acceptance of this 101 | License and all of its terms and conditions. If You distribute or communicate 102 | copies of the Original Work or a Derivative Work, You must make a reasonable 103 | effort under the circumstances to obtain the express assent of recipients to 104 | the terms of this License. This License conditions your rights to undertake 105 | the activities listed in Section 1, including your right to create Derivative 106 | Works based upon the Original Work, and doing so without honoring these terms 107 | and conditions is prohibited by copyright law and international treaty. 108 | Nothing in this License is intended to affect copyright exceptions and 109 | limitations (including "fair use" or "fair dealing"). This License shall 110 | terminate immediately and You may no longer exercise any of the rights granted 111 | to You by this License upon your failure to honor the conditions in Section 112 | 1(c). 113 | 114 | 10) Termination for Patent Action. This License shall terminate automatically 115 | and You may no longer exercise any of the rights granted to You by this 116 | License as of the date You commence an action, including a cross-claim or 117 | counterclaim, against Licensor or any licensee alleging that the Original Work 118 | infringes a patent. This termination provision shall not apply for an action 119 | alleging patent infringement by combinations of the Original Work with other 120 | software or hardware. 121 | 122 | 11) Jurisdiction, Venue and Governing Law. Any action or suit relating to this 123 | License may be brought only in the courts of a jurisdiction wherein the 124 | Licensor resides or in which Licensor conducts its primary business, and under 125 | the laws of that jurisdiction excluding its conflict-of-law provisions. The 126 | application of the United Nations Convention on Contracts for the 127 | International Sale of Goods is expressly excluded. Any use of the Original 128 | Work outside the scope of this License or after its termination shall be 129 | subject to the requirements and penalties of copyright or patent law in the 130 | appropriate jurisdiction. This section shall survive the termination of this 131 | License. 132 | 133 | 12) Attorneys' Fees. In any action to enforce the terms of this License or 134 | seeking damages relating thereto, the prevailing party shall be entitled to 135 | recover its costs and expenses, including, without limitation, reasonable 136 | attorneys' fees and costs incurred in connection with such action, including 137 | any appeal of such action. This section shall survive the termination of this 138 | License. 139 | 140 | 13) Miscellaneous. If any provision of this License is held to be 141 | unenforceable, such provision shall be reformed only to the extent necessary 142 | to make it enforceable. 143 | 144 | 14) Definition of "You" in This License. "You" throughout this License, 145 | whether in upper or lower case, means an individual or a legal entity 146 | exercising rights under, and complying with all of the terms of, this License. 147 | For legal entities, "You" includes any entity that controls, is controlled by, 148 | or is under common control with you. For purposes of this definition, 149 | "control" means (i) the power, direct or indirect, to cause the direction or 150 | management of such entity, whether by contract or otherwise, or (ii) ownership 151 | of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial 152 | ownership of such entity. 153 | 154 | 15) Right to Use. You may use the Original Work in all ways not otherwise 155 | restricted or conditioned by this License or by law, and Licensor promises not 156 | to interfere with or be responsible for such uses by You. 157 | 158 | 16) Modification of This License. This License is Copyright © 2005 Lawrence 159 | Rosen. Permission is granted to copy, distribute, or communicate this License 160 | without modification. Nothing in this License permits You to modify this 161 | License as applied to the Original Work or to Derivative Works. However, You 162 | may modify the text of this License and copy, distribute or communicate your 163 | modified version (the "Modified License") and apply it to other original works 164 | of authorship subject to the following conditions: (i) You may not indicate in 165 | any way that your Modified License is the "Open Software License" or "OSL" and 166 | you may not use those names in the name of your Modified License; (ii) You 167 | must replace the notice specified in the first paragraph above with the notice 168 | "Licensed under " or with a notice of your own 169 | that is not confusingly similar to the notice in this License; and (iii) You 170 | may not claim that your original works are open source software unless your 171 | Modified License has been approved by Open Source Initiative (OSI) and You 172 | comply with its license review and certification process. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "autoload": { 3 | "psr-4": { 4 | "JaxkDev\\ChatBridge\\": "src/" 5 | } 6 | }, 7 | "require": { 8 | "ext-yaml": "*" 9 | }, 10 | "require-dev": { 11 | "jaxkdev/discordbot": "^3", 12 | "phpstan/phpstan": "^1", 13 | "pocketmine/pocketmine-mp": "^5" 14 | }, 15 | "minimum-stability": "dev" 16 | } 17 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src 5 | checkMissingIterableValueType: false -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: ChatBridge 2 | version: 1.1.1 3 | author: JaxkDev 4 | main: JaxkDev\ChatBridge\Main 5 | src-namespace-prefix: JaxkDev\ChatBridge 6 | description: Plugin to bridge the chat between minecraft and discord using the DiscordBot API 7 | website: https://github.com/DiscordBot-PMMP/ChatBridge 8 | 9 | api: 5.0.0 10 | depend: 11 | - DiscordBot -------------------------------------------------------------------------------- /resources/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | presence: 5 | enabled: true 6 | status: "Idle" 7 | type: "Playing" 8 | message: "on a PMMP Server!" 9 | 10 | messages: 11 | discord: 12 | enabled: true 13 | from_channels: 14 | - "123456789" 15 | format: "[Discord] §a{NICKNAME}§r > §c{MESSAGE}" 16 | to_minecraft_worlds: "*" 17 | 18 | minecraft: 19 | enabled: true 20 | from_worlds: "*" 21 | format: 22 | text: "New Message" 23 | embed: 24 | enabled: true 25 | title: null 26 | description: "{USERNAME} ```{MESSAGE}```" 27 | url: null 28 | author: null 29 | footer: "ChatBridge" 30 | colour: 0xEE5959 31 | time: true 32 | fields: [] 33 | to_discord_channels: 34 | - "123456789" 35 | 36 | 37 | commands: 38 | minecraft: 39 | enabled: true 40 | from_worlds: "*" 41 | format: 42 | text: "Command executed" 43 | embed: 44 | enabled: true 45 | title: "{USERNAME}" 46 | description: "/{COMMAND} {ARGS}" 47 | author: "ChatBridge" 48 | url: null 49 | footer: "This is a footer" 50 | time: true 51 | colour: 0x49F9C1 52 | fields: 53 | - name: "Field Name" 54 | value: "Field Value" 55 | inline: false 56 | to_discord_channels: 57 | - "123456789" 58 | 59 | 60 | leave: 61 | discord: 62 | enabled: true 63 | servers: 64 | - "123456789" 65 | format: "§a{NICKNAME} §cHas left the discord server :(" 66 | to_minecraft_worlds: "*" 67 | 68 | minecraft: 69 | enabled: true 70 | ignore_transferred: true 71 | format: 72 | text: "" 73 | embed: 74 | enabled: true 75 | title: "Player Left" 76 | description: "- **{USERNAME}** left, reason: {REASON}" 77 | author: "ChatBridge" 78 | url: null 79 | footer: "This is a footer" 80 | time: true 81 | colour: 0xEE5959 82 | fields: 83 | - name: "Field Name" 84 | value: "Field Value" 85 | inline: false 86 | to_discord_channels: 87 | - "123456789" 88 | 89 | 90 | join: 91 | discord: 92 | enabled: true 93 | servers: 94 | - "123456789" 95 | format: "§a{USERNAME} §cHas joined the discord server :)" 96 | to_minecraft_worlds: "*" 97 | 98 | minecraft: 99 | enabled: true 100 | format: 101 | text: "" 102 | embed: 103 | enabled: true 104 | title: "Player Joined" 105 | description: "+ **{USERNAME}**" 106 | author: "ChatBridge" 107 | url: null 108 | footer: "This is a footer" 109 | time: true 110 | colour: 0xEE5959 111 | fields: 112 | - name: "Field Name" 113 | value: "Field Value" 114 | inline: false 115 | to_discord_channels: 116 | - "123456789" 117 | 118 | 119 | transferred: 120 | minecraft: 121 | enabled: true 122 | format: 123 | text: "" 124 | embed: 125 | enabled: true 126 | title: "Player Transferred" 127 | description: "**{USERNAME}** -> `{SERVER_ADDRESS}:{SERVER_PORT}`" 128 | author: "ChatBridge" 129 | url: null 130 | footer: "This is a footer" 131 | time: true 132 | colour: 0xEE5959 133 | fields: 134 | - name: "Field Name" 135 | value: "Field Value" 136 | inline: false 137 | to_discord_channels: 138 | - "123456789" 139 | 140 | ... -------------------------------------------------------------------------------- /resources/help/english.txt: -------------------------------------------------------------------------------- 1 | ======================================================================================================================== 2 | I am looking for translators, if you can translate this help document to another language please contact me via GitHub. 3 | PRs are the most optimal way to do this. (https://github.com/DiscordBot-PMMP/ChatBridge/pulls) 4 | ======================================================================================================================== 5 | 6 | English Help Document 7 | Revision | 15-10-2023 8 | Contributors | JaxkDev 9 | 10 | 11 | Below is a short help document to what you can change in the config.yml and what it will do. 12 | 13 | Messages section shows the most info, as the sections below are almost the exact same. 14 | If something is not covered assume it has been documented in another event section above (probably messages). 15 | 16 | -------- Config.yml documentation -------- 17 | 18 | version: 2 <- This is the version of the config.yml. DO NOT TOUCH, EVER. 19 | 20 | presence: <- Presence section allows you to update the bots 'status'/'presence' 21 | enabled: true/false <- Enables/disables the presence overwrite option. 22 | status: "Idle" <- This is the bots' status, can be one of the following (Idle, Online, Offline, Dnd) 23 | type: "Playing" <- This is the bots' activity type, can be one of the following (Playing, Streaming, Listening, Watching) 24 | message: "on a PMMP Server!" <- This is the message after the type, can be anything. 25 | 26 | 27 | 28 | messages: <- Messages section allows you to change the configuration for messages. 29 | discord: <- This is the config for messages sent FROM discord TO minecraft. 30 | enabled: true <- enables/disables the messages. 31 | from_channels: <- This is the channels that the bot will listen for messages from, CHANNEL ID's ONLY. 32 | - "12345678912345678" 33 | - "14284181241849897" <- To add more channels, just add more lines like this. 34 | format: "[Discord] §a{NICKNAME}§r > §c{MESSAGE}" <- This is the format of the message that will be sent to players in minecraft 35 | (optional keys: {NICKNAME}, {USERNAME}, {USER_DISCRIMINATOR}, {MESSAGE}, {SERVER}, {CHANNEL}, {TIME}, {TIME-2}) 36 | to_minecraft_worlds: "*" <- Which worlds the message will be sent to, can be "*" for all worlds, or a list of world names. 37 | 38 | 39 | to_minecraft_worlds: <- Example of multiple worlds being listed instead of "*" (all worlds) 40 | - "world1" <- World Names 41 | - "world2" 42 | - "world3" 43 | 44 | 45 | minecraft: <- This is the config for messages sent FROM minecraft TO discord. 46 | enabled: true <- enables/disables the messages. 47 | from_worlds: "*" <- Which worlds the plugin will listen for messages, can be "*" for all worlds, or a list of world names. (see top of document for example) 48 | format: <- Format of text being sent to discord, All string/text options can include the following variables: 49 | {USERNAME}, {DISPLAY_NAME}, {MESSAGE}, {XUID}, {UUID}, 50 | {ADDRESS}, {PORT}, {WORLD}, {TIME}, {TIME-2}, {TIME-3} 51 | text: "New Message" <- This is the message that will be sent to discord (above embed if enabled) (can be just "" if you want to send nothing but embed must be enabled) 52 | 53 | embed: <- See https://tinyurl.com/ChatBridgeEmbed for a visual aid of what goes where 54 | enabled: true <- Adds an embed to the message 55 | title: null <- null or string 56 | description: "{USERNAME} ```{MESSAGE}```" <- null or string 57 | url: null <- null or URL string (makes title clickable, useless if title is null.) 58 | author: null <- null or string 59 | footer: "ChatBridge" <- null or string 60 | colour: 0xEE5959 <- hex colour code (add zero-x (0x) before hex colour) (0x000000 = black, 0xFFFFFF = white etc, https://colors-picker.com/hex-color-picker/) 61 | time: true <- Display time of message in embed footer 62 | fields: [] <- array of fields in embed, can be empty array ([]) 63 | 64 | fields: <- Example of multiple fields, add up to 25 fields. 65 | - name: "Field Name" <- null or string - one is required, both cannot be null) 66 | description: "Field Description" <- null or string - ^ 67 | inline: true <- true or false, if true, field will be displayed inline 68 | - name: "Field Name" 69 | description: "Field Description" 70 | inline: false 71 | 72 | to_discord_channels: <- Discord channels to send the formatted message. 73 | - "12345678912345678" <- Discord channel IDs 74 | - "42356325325312412" 75 | 76 | 77 | commands: <- This is the config for commands run in minecraft. 78 | minecraft: 79 | enabled: true 80 | from_worlds: "*" 81 | format: <- Available variables: 82 | {COMMAND}, {ARGS}, {USERNAME}, {DISPLAY_NAME}, {MESSAGE}, {XUID}, 83 | {UUID}, {ADDRESS}, {PORT}, {WORLD}, {TIME}, {TIME-2}, {TIME-3} 84 | text: "Command executed" 85 | embed: 86 | enabled: true 87 | title: "{USERNAME}" 88 | description: "/{COMMAND} {ARGS}" 89 | author: "ChatBridge" 90 | url: null 91 | footer: "This is a footer" 92 | time: true 93 | colour: 0x49F9C1 94 | fields: 95 | - name: "Field Name" 96 | value: "Field Value" 97 | inline: false 98 | to_discord_channels: 99 | - "12345678912345678" 100 | 101 | 102 | leave: <- This is the config for messages sent when a player leaves a server. 103 | discord: <- This is the config for messages sent TO MINECRAFT, when a member leaves the DISCORD server. 104 | enabled: true 105 | servers: 106 | - "12345678912345678" 107 | format: "§a{NICKNAME} §cHas left :(" <- available variables: 108 | {NICKNAME}, {USERNAME}, {USER_DISCRIMINATOR}, {SERVER}, {TIME}, {TIME-2} 109 | to_minecraft_worlds: "*" 110 | 111 | minecraft: <- This is the config for messages sent TO DISCORD, when a member leaves the MINECRAFT server. 112 | enabled: true 113 | ignore_transferred: true <- true or false, if true it won't send leave messages if player transferred. 114 | format: <- available variables for all text/string options: 115 | {REASON}, {USERNAME}, {DISPLAY_NAME}, {XUID}, {UUID}, 116 | {ADDRESS}, {PORT}, {TIME}, {TIME-2}, {TIME-3} 117 | text: "" 118 | embed: 119 | enabled: true 120 | title: "Player Left" 121 | description: "- **{USERNAME}** left, reason: {REASON}" 122 | author: "ChatBridge" 123 | url: null 124 | footer: "This is a footer" 125 | time: true 126 | colour: 0xEE5959 127 | fields: 128 | - name: "Field Name" 129 | value: "Field Value" 130 | inline: false 131 | to_discord_channels: 132 | - "12345678912345678" 133 | 134 | 135 | join: 136 | discord: 137 | enabled: true 138 | servers: 139 | - "12345678912345678" 140 | format: "§a{USERNAME} §cHas joined the discord server :)" <- available variables: 141 | {NICKNAME}, {USERNAME}, {USER_DISCRIMINATOR}, {SERVER}, {TIME}, {TIME-2} 142 | to_minecraft_worlds: "*" 143 | 144 | minecraft: 145 | enabled: true 146 | format: <- Available variables for all text/string options: 147 | {USERNAME}, {DISPLAY_NAME}, {XUID}, {UUID}, 148 | {ADDRESS}, {PORT}, {TIME}, {TIME-2}, {TIME-3} 149 | text: "" 150 | embed: 151 | enabled: true 152 | title: "Player Joined" 153 | description: "+ **{USERNAME}**" 154 | author: "ChatBridge" 155 | url: null 156 | footer: "This is a footer" 157 | time: true 158 | colour: 0xEE5959 159 | fields: 160 | - name: "Field Name" 161 | value: "Field Value" 162 | inline: false 163 | to_discord_channels: 164 | - "12345678912345678" 165 | 166 | 167 | transferred: 168 | minecraft: 169 | enabled: true 170 | format: <- Available variables for all text/string options: 171 | {USERNAME}, {DISPLAY_NAME}, {XUID}, {UUID}, 172 | {ADDRESS}, {PORT}, {TIME}, {TIME-2}, {TIME-3}, 173 | {SERVER_ADDRESS}, {SERVER_PORT} 174 | text: "" 175 | embed: 176 | enabled: true 177 | title: "Player Transferred" 178 | description: "**{USERNAME}** -> `{SERVER_ADDRESS}:{SERVER_PORT}`" 179 | author: "ChatBridge" 180 | url: null 181 | footer: "This is a footer" 182 | time: true 183 | colour: 0xEE5959 184 | fields: 185 | - name: "Field Name" 186 | value: "Field Value" 187 | inline: false 188 | to_discord_channels: 189 | - "12345678912345678" 190 | 191 | ... -------------------------------------------------------------------------------- /resources/help/french.txt: -------------------------------------------------------------------------------- 1 | Document d'aide en Français 2 | Revision | 08-10-2023 3 | Traduction | Rover17 4 | 5 | Vous trouverez ci-dessous un court document d'aide expliquant ce que vous pouvez modifier dans le fichier config.yml et ce qu'il fera. 6 | 7 | Les sections de messages qui montrent la majorité des informations, comme les sections ci-dessous, sont presque exactement les mêmes. 8 | Si quelque chose n'est pas cité ci-dessous, il est potentiellement documenté dans une autre section d'événement ci-dessus (probablement dans celle des messages). 9 | 10 | -------- Config.yml documentation -------- 11 | 12 | version: 2 <- Il s'agit de la version du fichier config.yml NE JAMAIS LE MODIFIER 13 | 14 | presence: <- La section de présence vous permet de modifier LE 'statut'/'présence' de votre bot. 15 | enabled: true/false <- Active/désactive l'option de changement de la présence du bot. 16 | status: "Idle" <- Définit le statut de votre bot qui peut être l'un des suivants (Idle, Online, Offline, Dnd) 17 | type: "Playing" <- Définit le type d'activité de votre bot qui peut être l'une des suivantes (Playing, Streaming, Listening, Watching) 18 | message: "on a PMMP Server!" <- Définit le message après le type d'activité, peut-être n'importe quoi 19 | 20 | 21 | 22 | messages: <- La section de messages vous permet de modfier les messages. 23 | discord: <- Cette configuration vous permet de modfier les messages envoyé DEPUIS discord VERS minecraft. 24 | enabled: true <- Active/désactive les messages. 25 | from_channels: <- Il s'agit des channels desquels le bot verra les messages, ID DES CHANNELS UNIQUEMENT . 26 | - "12345678912345678" 27 | - "14284181241849897" <- Pour ajouter plus de channels, ajouter plusieurs lignes comme celle ci. 28 | format: "[Discord] §a{NICKNAME}§r > §c{MESSAGE}" <- Il s'agit du format duquel les messages seront envoyées de discord a minecraft 29 | (variables optionnelles : {NICKNAME}, {USERNAME}, {USER_DISCRIMINATOR}, {MESSAGE}, {SERVER}, {CHANNEL}, {TIME}, {TIME-2}) 30 | to_minecraft_worlds: "*" <- Les mondes auxquels le message sera envoyé, "*" pour tous les mondes ou une liste de noms de mondes 31 | 32 | to_minecraft_worlds: <- Exemple de beaucoup de mondes plutôt que "*" (tous les mondes) 33 | - "world1" <- Nom des mondes 34 | - "world2" 35 | - "world3" 36 | 37 | 38 | minecraft: <- Cette configuration vous permet de modifier les messages envoyés DEPUIS Minecraft VERS Discord. 39 | enabled: true <- Active/désactive les messages. 40 | from_worlds: "*" <- Les mondes auxquels le message sera envoyé, "*" pour tous les mondes, ou une liste de noms de mondes. (Voir l'exemple en haut) 41 | format: <- Il s'agit du format que les messages seront envoyés de Minecraft à discord, Les options string/text peuvent inclure les variables suivantes : 42 | {USERNAME}, {DISPLAY_NAME}, {MESSAGE}, {XUID}, {UUID}, 43 | {ADDRESS}, {PORT}, {WORLD}, {TIME}, {TIME-2}, {TIME-3} 44 | text: "New Message" <- C'est le message qui sera envoyé au Discord (Si l'embed est activé) (peut être juste "" si vous ne voulez rien envoyer, mais l'embed doit être activée) 45 | 46 | embed: <- Voir https://tinyurl.com/ChatBridgeEmbed pour une aide visuelle 47 | enabled: true <- Ajoute un embed au message 48 | title: null <- null ou string 49 | description: "{USERNAME} ```{MESSAGE}```" <- null ou string 50 | url: null <- null ou string URL (rend le titre cliquable, inutile si le titre est null.) 51 | author: null <- null ou string 52 | footer: "ChatBridge" <- null ou string 53 | colour: 0xEE5959 <- hex colour code (mettre zero-x (0x) avant le code hex) (0x000000 = blanc, 0xFFFFFF = blanc, etc, https://colors-picker.com/hex-color-picker/) 54 | time: true <- Afficher l'heure du message dans le footer de l'embed 55 | fields: [] <- array de champs intégrés, peut-être un array vide ([]) 56 | 57 | fields: <- Exemple de plusieurs champs, ajoutez jusqu'à 25 champs. 58 | - name: "Field Name" <- null ou string — au moins un string est requis, les deux ne peuvent pas être null 59 | description: "Field Description" <- null ou string - ^ 60 | inline: true <- true ou false, si true, le champ sera affiché en ligne 61 | - name: "Field Name" 62 | description: "Field Description" 63 | inline: false 64 | 65 | to_discord_channels: <- Channel discord ou le message sera envoyé 66 | - "12345678912345678" <- Id de channels discord 67 | - "42356325325312412" 68 | 69 | 70 | commands: <- Voici la configuration des commandes exécutées dans Minecraft. 71 | minecraft: 72 | enabled: true 73 | from_worlds: "*" 74 | format: <- Variables disponibles : 75 | {COMMAND}, {ARGS}, {USERNAME}, {DISPLAY_NAME}, {MESSAGE}, {XUID}, 76 | {UUID}, {ADDRESS}, {PORT}, {WORLD}, {TIME}, {TIME-2}, {TIME-3} 77 | text: "Command executed" 78 | embed: 79 | enabled: true 80 | title: "{USERNAME}" 81 | description: "/{COMMAND} {ARGS}" 82 | author: "ChatBridge" 83 | url: null 84 | footer: "This is a footer" 85 | time: true 86 | colour: 0x49F9C1 87 | fields: 88 | - name: "Field Name" 89 | value: "Field Value" 90 | inline: false 91 | to_discord_channels: 92 | - "12345678912345678" 93 | 94 | 95 | leave: <- Cette configuration est pour les messages envoyés lorsqu'un joueur quitte un serveur. 96 | discord: <- Cette configuration est pour les messages envoyés VERS MINECRAFT, lorsqu'un membre quitte le serveur DISCORD. 97 | enabled: true 98 | servers: 99 | - "12345678912345678" 100 | format: "§a{NICKNAME} §cHas left :(" <- Variables disponibles : 101 | {NICKNAME}, {USERNAME}, {USER_DISCRIMINATOR}, {SERVER}, {TIME}, {TIME-2} 102 | to_minecraft_worlds: "*" 103 | 104 | minecraft: <- Cette configuration est pour les messages envoyés VERS DISCORD, lorsqu'un membre quitte le serveur MINECRAFT. 105 | enabled: true 106 | ignore_transferred: true <- true ou false, si true il n'enverra pas de messages de deconection lorsque le joueur est transféré. 107 | format: <- Variables disponibles pour les options de texte/string: 108 | {REASON}, {USERNAME}, {DISPLAY_NAME}, {XUID}, {UUID}, 109 | {ADDRESS}, {PORT}, {TIME}, {TIME-2}, {TIME-3} 110 | text: "" 111 | embed: 112 | enabled: true 113 | title: "Player Left" 114 | description: "- **{USERNAME}** left, reason: {REASON}" 115 | author: "ChatBridge" 116 | url: null 117 | footer: "This is a footer" 118 | time: true 119 | colour: 0xEE5959 120 | fields: 121 | - name: "Field Name" 122 | value: "Field Value" 123 | inline: false 124 | to_discord_channels: 125 | - "12345678912345678" 126 | 127 | 128 | join: 129 | discord: 130 | enabled: true 131 | servers: 132 | - "12345678912345678" 133 | format: "§a{USERNAME} §cHas joined the discord server :)" <- Variables disponibles : 134 | {NICKNAME}, {USERNAME}, {USER_DISCRIMINATOR}, {SERVER}, {TIME}, {TIME-2} 135 | to_minecraft_worlds: "*" 136 | 137 | minecraft: 138 | enabled: true 139 | format: <- Variables disponibles pour les options de texte/string: 140 | {USERNAME}, {DISPLAY_NAME}, {XUID}, {UUID}, 141 | {ADDRESS}, {PORT}, {TIME}, {TIME-2}, {TIME-3} 142 | text: "" 143 | embed: 144 | enabled: true 145 | title: "Player Joined" 146 | description: "+ **{USERNAME}**" 147 | author: "ChatBridge" 148 | url: null 149 | footer: "This is a footer" 150 | time: true 151 | colour: 0xEE5959 152 | fields: 153 | - name: "Field Name" 154 | value: "Field Value" 155 | inline: false 156 | to_discord_channels: 157 | - "12345678912345678" 158 | 159 | 160 | transferred: 161 | minecraft: 162 | enabled: true 163 | format: <- Variables disponibles pour les options de texte/string: 164 | {USERNAME}, {DISPLAY_NAME}, {XUID}, {UUID}, 165 | {ADDRESS}, {PORT}, {TIME}, {TIME-2}, {TIME-3}, 166 | {SERVER_ADDRESS}, {SERVER_PORT} 167 | text: "" 168 | embed: 169 | enabled: true 170 | title: "Player Transferred" 171 | description: "**{USERNAME}** -> `{SERVER_ADDRESS}:{SERVER_PORT}`" 172 | author: "ChatBridge" 173 | url: null 174 | footer: "This is a footer" 175 | time: true 176 | colour: 0xEE5959 177 | fields: 178 | - name: "Field Name" 179 | value: "Field Value" 180 | inline: false 181 | to_discord_channels: 182 | - "12345678912345678" 183 | 184 | ... 185 | -------------------------------------------------------------------------------- /src/ConfigUtils.php: -------------------------------------------------------------------------------- 1 | "patch_1", 25 | ]; 26 | 27 | static public function update(array &$config): void{ 28 | for($i = (int)$config["version"]; $i < self::VERSION; $i += 1){ 29 | $config = forward_static_call([self::class, self::_PATCH_MAP[$i]], $config); 30 | } 31 | } 32 | 33 | /** 34 | * Verifies the config's keys and values, returning any keys and a relevant message. 35 | * @param array $config 36 | * @return string[] 37 | */ 38 | static public function verify(array $config): array{ 39 | $result = []; 40 | $keys = ["version", "presence", "messages", "commands", "leave", "join", "transferred"]; 41 | foreach(array_keys($config) as $event){ 42 | if(!in_array($event, $keys)){ 43 | //Event key is invalid. 44 | $result[] = "Invalid key '$event' found."; 45 | continue; 46 | } 47 | $keys = array_diff($keys, [$event]); 48 | switch($event){ 49 | case "version": 50 | if($config[$event] !== self::VERSION){ 51 | //Should never happen. 52 | $result[] = "Invalid version '$event' found."; 53 | } 54 | break; 55 | case "presence": 56 | if(!is_array($config[$event])){ 57 | $result[] = "Invalid value for '$event' found, array expected."; 58 | }else{ 59 | if(!isset($config[$event]["enabled"])){ 60 | $result[] = "Missing 'enabled' key for '$event' found."; 61 | }elseif(!in_array($config[$event]["enabled"], [true, false])){ 62 | $result[] = "Invalid value for 'enabled' key for '$event' found, boolean expected (true/false)."; 63 | } 64 | if(!isset($config[$event]["status"])){ 65 | $result[] = "Missing 'status' key for '$event' found."; 66 | }elseif(!in_array(strtolower($config[$event]["status"]), ["online", "idle", "dnd", "offline"])){ 67 | $result[] = "Invalid value for 'status' key for '$event' found, one of online,idle,dnd,offline expected."; 68 | } 69 | if(!isset($config[$event]["type"])){ 70 | $result[] = "Missing 'type' key for '$event' found."; 71 | }elseif(!in_array(strtolower($config[$event]["type"]), ["playing", "play", "listening", "listen", "watching", "watch"])){ 72 | $result[] = "Invalid value for 'type' key for '$event' found, one of playing,listening,watching expected."; 73 | } 74 | if(!isset($config[$event]["message"])){ 75 | $result[] = "Missing 'message' key for '$event' found."; 76 | }elseif(!is_string($config[$event]["message"])){ 77 | $result[] = "Invalid value for 'message' key for '$event' found, string expected."; 78 | } 79 | } 80 | break; 81 | case "messages": 82 | $result = array_merge($result, self::verify_discord($event, $config)); 83 | if(!isset($config[$event]["discord"]["from_channels"])){ 84 | $result[] = "Missing key '".$event.".discord.from_channels'"; 85 | }elseif(!is_array($config[$event]["discord"]["from_channels"])){ 86 | $result[] = "Invalid value for key '".$event.".discord.from_channels', expected array of channel ID's."; 87 | }else{ 88 | foreach($config[$event]["discord"]["from_channels"] as $cid){ 89 | if(!Utils::validDiscordSnowflake($cid)){ 90 | $result[] = "Invalid channel ID '$cid' found in key '".$event.".discord.from_channels'."; 91 | } 92 | } 93 | } 94 | $result = array_merge($result, self::verify_minecraft($event, $config)); 95 | if(!isset($config[$event]["minecraft"]["from_worlds"])){ 96 | $result[] = "Missing key '".$event.".minecraft.from_worlds'"; 97 | }elseif(!is_string($config[$event]["minecraft"]["from_worlds"]) and !is_array($config[$event]["minecraft"]["from_worlds"])){ 98 | $result[] = "Invalid value for key '".$event.".minecraft.from_worlds', expected array or string."; 99 | } 100 | break; 101 | case "join": 102 | $result = array_merge($result, self::verify_discord($event, $config)); 103 | if(!isset($config[$event]["discord"]["servers"])){ 104 | $result[] = "Missing key '".$event.".discord.servers'"; 105 | }elseif(!is_array($config[$event]["discord"]["servers"])){ 106 | $result[] = "Invalid value for key '".$event.".discord.servers', expected array of server ID's."; 107 | }else{ 108 | foreach($config[$event]["discord"]["servers"] as $sid){ 109 | if(!Utils::validDiscordSnowflake($sid)){ 110 | $result[] = "Invalid server ID '$sid' found in key '".$event.".discord.servers'."; 111 | } 112 | } 113 | } 114 | $result = array_merge($result, self::verify_minecraft($event, $config)); 115 | break; 116 | case "leave": 117 | $result = array_merge($result, self::verify_discord($event, $config)); 118 | if(!isset($config[$event]["discord"]["servers"])){ 119 | $result[] = "Missing key '".$event.".discord.servers'"; 120 | }elseif(!is_array($config[$event]["discord"]["servers"])){ 121 | $result[] = "Invalid value for key '".$event.".discord.servers', expected array of server ID's."; 122 | }else{ 123 | foreach($config[$event]["discord"]["servers"] as $sid){ 124 | if(!Utils::validDiscordSnowflake($sid)){ 125 | $result[] = "Invalid server ID '$sid' found in key '".$event.".discord.servers'."; 126 | } 127 | } 128 | } 129 | $result = array_merge($result, self::verify_minecraft($event, $config)); 130 | if(!isset($config[$event]["minecraft"]["ignore_transferred"])){ 131 | $result[] = "Missing key '".$event.".minecraft.ignore_transferred'"; 132 | }elseif(!is_bool($config[$event]["minecraft"]["ignore_transferred"])){ 133 | $result[] = "Invalid value for key '".$event.".minecraft.ignore_transferred', expected boolean."; 134 | } 135 | break; 136 | case "commands": 137 | case "transferred": 138 | $result = array_merge($result, self::verify_minecraft($event, $config)); 139 | break; 140 | default: 141 | $result[] = "Unknown key '$event' found."; 142 | break; 143 | } 144 | } 145 | 146 | if(sizeof($keys) !== 0){ 147 | $result[] = "Missing keys: '" . implode("', '", $keys)."'"; 148 | } 149 | 150 | return $result; 151 | } 152 | 153 | /** 154 | * Verifies the config's generic discord section. 155 | * @param string $event 156 | * @param array $config 157 | * @return string[] 158 | */ 159 | static private function verify_discord(string $event, array $config): array{ 160 | $result = []; 161 | $config = $config[$event]["discord"]; 162 | if(!isset($config["enabled"])){ 163 | $result[] = "Missing key: '". $event . ".discord.enabled'"; 164 | }elseif(!is_bool($config["enabled"])){ 165 | $result[] = "Invalid value for key: '". $event . ".discord.enabled', expected boolean (true/false)."; 166 | } 167 | if(!isset($config["format"])){ 168 | $result[] = "Missing key: '".$event.".discord.format'"; 169 | }elseif(!is_string($config["format"])){ 170 | $result[] = "Invalid value for key: '".$event.".discord.format', expected string."; 171 | } 172 | if(!isset($config["to_minecraft_worlds"])){ 173 | $result[] = "Missing key: '".$event.".discord.to_minecraft_worlds'"; 174 | }elseif(!is_array($config["to_minecraft_worlds"]) and !is_string($config["to_minecraft_worlds"])){ 175 | $result[] = "Invalid value for key: '".$event.".discord.to_minecraft_worlds', expected array or string."; 176 | } 177 | return $result; 178 | } 179 | 180 | /** 181 | * Verifies the config's generic minecraft section. 182 | * @param string $event 183 | * @param array $config 184 | * @return string[] 185 | */ 186 | static private function verify_minecraft(string $event, array $config): array{ 187 | $result = []; 188 | $config = $config[$event]["minecraft"]; 189 | if(!isset($config["enabled"])){ 190 | $result[] = "Missing key: '". $event . ".minecraft.enabled'"; 191 | }elseif(!is_bool($config["enabled"])){ 192 | $result[] = "Invalid value for key: '". $event . ".minecraft.enabled', expected boolean (true/false)."; 193 | } 194 | if(!isset($config["format"])){ 195 | $result[] = "Missing key: '".$event.".discord.format'"; 196 | }elseif(!is_array($config["format"])){ 197 | $result[] = "Invalid value for key: '".$event.".discord.format', expected array."; 198 | }else{ 199 | //format 200 | if(!isset($config["format"]["text"])){ 201 | $result[] = "Missing key: '".$event.".minecraft.format.text'"; 202 | }elseif(!is_string($config["format"]["text"])){ 203 | $result[] = "Invalid value for key: '".$event.".minecraft.format.text', expected string."; 204 | }elseif(strlen($config["format"]["text"]) > 2000){ 205 | $result[] = "Invalid value for key: '".$event.".minecraft.format.text', string is too long (max 2000 characters)."; 206 | } 207 | if(!isset($config["format"]["embed"])){ 208 | $result[] = "Missing key: '".$event.".minecraft.format.embed'"; 209 | }elseif(!is_array($config["format"]["embed"])){ 210 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed', expected array."; 211 | }else{ 212 | //embed 213 | if(!isset($config["format"]["embed"]["enabled"])){ 214 | $result[] = "Missing key: '".$event.".minecraft.format.embed.enabled'"; 215 | }elseif(!is_bool($config["format"]["embed"]["enabled"])){ 216 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.enabled', expected boolean (true/false)."; 217 | } 218 | if(!array_key_exists("title", $config["format"]["embed"])){ 219 | $result[] = "Missing key: '".$event.".minecraft.format.embed.title'"; 220 | }elseif(!is_string($config["format"]["embed"]["title"]) and !is_null($config["format"]["embed"]["title"])){ 221 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.title', expected string or null."; 222 | }elseif(strlen($config["format"]["embed"]["title"]??"") > 2048){ 223 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.title', string is too long (max 2048 characters)."; 224 | } 225 | if(!array_key_exists("description", $config["format"]["embed"])){ 226 | $result[] = "Missing key: '".$event.".minecraft.format.embed.description'"; 227 | }elseif(!is_string($config["format"]["embed"]["description"]) and !is_null($config["format"]["embed"]["description"])){ 228 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.description', expected string or null."; 229 | }elseif(strlen($config["format"]["embed"]["description"]??"") > 4096){ 230 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.description', string is too long (max 4096 characters)."; 231 | } 232 | if(!array_key_exists("author", $config["format"]["embed"])){ 233 | $result[] = "Missing key: '".$event.".minecraft.format.embed.author'"; 234 | }elseif(!is_string($config["format"]["embed"]["author"]) and !is_null($config["format"]["embed"]["author"])){ 235 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.author', expected string or null."; 236 | }elseif(strlen($config["format"]["embed"]["description"]??"") > 2048){ 237 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.author', string is too long (max 2048 characters)."; 238 | } 239 | if(!array_key_exists("url", $config["format"]["embed"])){ 240 | $result[] = "Missing key: '".$event.".minecraft.format.embed.url'"; 241 | }elseif(!is_string($config["format"]["embed"]["url"]) and !is_null($config["format"]["embed"]["url"])){ 242 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.url', expected string or null."; 243 | } 244 | if(!array_key_exists("footer", $config["format"]["embed"])){ 245 | $result[] = "Missing key: '".$event.".minecraft.format.embed.footer'"; 246 | }elseif(!is_string($config["format"]["embed"]["footer"]) and !is_null($config["format"]["embed"]["footer"])){ 247 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.footer', expected string or null."; 248 | }elseif(strlen($config["format"]["embed"]["footer"]??"") > 2048){ 249 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.footer', string is too long (max 2048 characters)."; 250 | } 251 | if(!isset($config["format"]["embed"]["time"])){ 252 | $result[] = "Missing key: '".$event.".minecraft.format.embed.time'"; 253 | }elseif(!is_bool($config["format"]["embed"]["time"])){ 254 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.time', expected bool (true/false)."; 255 | } 256 | if(!isset($config["format"]["embed"]["colour"])){ 257 | $result[] = "Missing key: '".$event.".minecraft.format.embed.colour'"; 258 | }elseif(!is_int($config["format"]["embed"]["colour"])){ 259 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.colour', expected hex colour (eg 0xFFFFFF)."; 260 | }elseif($config["format"]["embed"]["colour"] < 0 or $config["format"]["embed"]["colour"] > 0xFFFFFF){ 261 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.colour', expected hex colour 0x000000 - 0xFFFFFF."; 262 | } 263 | if(!isset($config["format"]["embed"]["fields"])){ 264 | $result[] = "Missing key: '".$event.".minecraft.format.embed.fields'"; 265 | }elseif(!is_array($config["format"]["embed"]["fields"])){ 266 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.fields', expected array (put '[]' for no fields)."; 267 | }else{ 268 | //Check fields 269 | foreach($config["format"]["embed"]["fields"] as $field){ 270 | if(!isset($field["name"])){ 271 | $result[] = "Missing key: '".$event.".minecraft.format.embed.fields.name'"; 272 | }elseif(!is_string($field["name"])){ 273 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.fields.name', expected string."; 274 | }elseif(strlen($field["name"]) > 256){ 275 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.fields.name', string is too long (max 256 characters)."; 276 | } 277 | if(!isset($field["value"])){ 278 | $result[] = "Missing key: '".$event.".minecraft.format.embed.fields.value'"; 279 | }elseif(!is_string($field["value"])){ 280 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.fields.value', expected string."; 281 | }elseif(strlen($field["value"]) > 2048){ 282 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.fields.value', string is too long (max 2048 characters)."; 283 | } 284 | if(!isset($field["inline"])){ 285 | $result[] = "Missing key: '".$event.".minecraft.format.embed.fields.inline'"; 286 | }elseif(!is_bool($field["inline"])){ 287 | $result[] = "Invalid value for key: '".$event.".minecraft.format.embed.fields.inline', expected bool (true/false)."; 288 | } 289 | } 290 | } 291 | } 292 | } 293 | if(!isset($config["to_discord_channels"])){ 294 | $result[] = "Missing key: '".$event.".minecraft.to_discord_channels'"; 295 | }elseif(!is_array($config["to_discord_channels"]) and !is_string($config["to_discord_channels"])){ 296 | $result[] = "Invalid value for key: '".$event.".minecraft.to_discord_channels', expected array or string."; 297 | }else{ 298 | foreach((is_array($config["to_discord_channels"]) ? $config["to_discord_channels"] : [$config["to_discord_channels"]]) as $cid){ 299 | if(!Utils::validDiscordSnowflake($cid)){ 300 | $result[] = "Invalid channel ID '$cid' found in key '".$event.".minecraft.to_discord_channels'."; 301 | } 302 | } 303 | } 304 | return $result; 305 | } 306 | 307 | static private function patch_1(array $config): array{ 308 | $config["version"] = 2; 309 | $config["presence"]["enabled"] = true; 310 | return $config; 311 | } 312 | } -------------------------------------------------------------------------------- /src/EventListener.php: -------------------------------------------------------------------------------- 1 | plugin = $plugin; 58 | $this->config = $this->plugin->getConfig()->getAll(); 59 | $this->api = $this->plugin->getDiscord()->getApi(); 60 | } 61 | 62 | public function onDiscordReady(DiscordReady $event): void{ 63 | $this->ready = true; 64 | 65 | //Update presence. 66 | $type = match(strtolower($this->config["presence"]["type"]??"Playing")){ 67 | 'listening', 'listen' => ActivityType::LISTENING, 68 | 'watching', 'watch' => ActivityType::WATCHING, 69 | default => ActivityType::GAME, 70 | }; 71 | $status = match(strtolower($this->config["presence"]["status"]??"Online")){ 72 | 'idle' => Status::IDLE, 73 | 'dnd' => Status::DND, 74 | 'offline' => Status::OFFLINE, 75 | default => Status::ONLINE 76 | }; 77 | $activity = Activity::create($this->config["presence"]["message"]??"on a PMMP Server!", $type); 78 | $event->setActivity($activity); 79 | $event->setStatus($status); 80 | } 81 | 82 | public function onDiscordClosed(DiscordClosed $event): void{ 83 | //This plugin can no longer function if discord closes, 84 | //note once closed it will never start again until server restarts. 85 | $this->plugin->getLogger()->critical("DiscordBot has closed, disabling plugin."); 86 | $this->plugin->getServer()->getPluginManager()->disablePlugin($this->plugin); 87 | } 88 | 89 | public function onDiscordDisabled(PluginDisableEvent $event): void{ 90 | //Sometimes discordbot can be disabled without it emitting discord closed event. (startup errors) 91 | if($event->getPlugin()->getName() === "DiscordBot"){ 92 | $this->plugin->getLogger()->critical("DiscordBot has been disabled, disabling plugin."); 93 | $this->plugin->getServer()->getPluginManager()->disablePlugin($this->plugin); 94 | } 95 | } 96 | 97 | //--- Minecraft Events -> Discord Server ---// 98 | 99 | /** 100 | * @priority MONITOR 101 | * @ignoreCancelled 102 | */ 103 | public function onMinecraftMessage(PlayerChatEvent $event): void{ 104 | if(!$this->ready){ 105 | //Unlikely to happen, discord will most likely be ready before anyone even joins. 106 | $this->plugin->getLogger()->debug("Ignoring chat event, discord is not ready."); 107 | return; 108 | } 109 | 110 | /** @var array $config */ 111 | $config = $this->config["messages"]["minecraft"]; 112 | if(!$config['enabled']){ 113 | return; 114 | } 115 | 116 | $player = $event->getPlayer(); 117 | $message = $event->getMessage(); 118 | $world = $player->getWorld()->getDisplayName(); 119 | 120 | $from_worlds = is_array($config["from_worlds"]) ? $config["from_worlds"] : [$config["from_worlds"]]; 121 | if(!in_array("*", $from_worlds)){ 122 | //Only specific worlds. 123 | if(!in_array($world, $from_worlds)){ 124 | $this->plugin->getLogger()->debug("Ignoring chat event, world '$world' is not listed."); 125 | return; 126 | } 127 | } 128 | 129 | $formatter = function(string $text) use ($player, $message, $world): string{ 130 | $text = str_replace(["{USERNAME}", "{username}", "{PLAYER}", "{player}"], $player->getName(), $text); 131 | $text = str_replace(["{DISPLAYNAME}", "{displayname}", "{DISPLAY_NAME}", "{display_name}", "{NICKNAME}", "{nickname}"], $player->getDisplayName(), $text); 132 | $text = str_replace(["{MESSAGE}", "{message}"], $message, $text); 133 | $text = str_replace(["{XUID}", "{xuid}"], $player->getXuid(), $text); 134 | $text = str_replace(["{UUID}", "{uuid}"], $player->getUniqueId()->toString(), $text); 135 | $text = str_replace(["{ADDRESS}", "{address}", "{IP}", "{ip}"], $player->getNetworkSession()->getIp(), $text); 136 | $text = str_replace(["{PORT}", "{port}"], strval($player->getNetworkSession()->getPort()), $text); 137 | $text = str_replace(["{WORLD}", "{world}", "{LEVEL}", "{level}"], $world, $text); 138 | $text = str_replace(["{TIME}", "{time}", "{TIME-1}", "{time-1}"], date("H:i:s", time()), $text); 139 | $text = str_replace(["{TIME-2}", "{time-2}"], date("H:i", time()), $text); 140 | return str_replace(["{TIME-3}", "{time-3}"], "", $text); //TODO Other formatted times supported by discord. 141 | }; 142 | 143 | $embeds = []; 144 | $embed_config = $config["format"]["embed"]; 145 | if($embed_config["enabled"]){ 146 | $fields = []; 147 | foreach($embed_config["fields"] as $field){ 148 | $fields[] = new Field($formatter($field["name"]), $formatter($field["value"]), $field["inline"]); 149 | } 150 | $embeds[] = new Embed(($embed_config["title"] === null ? null : $formatter($embed_config["title"])), 151 | ($embed_config["description"] === null ? null : $formatter($embed_config["description"])), 152 | $embed_config["url"], $embed_config["time"] ? time() : null, $embed_config["colour"], 153 | $embed_config["footer"] === null ? null : new Footer($formatter($embed_config["footer"])), null, 154 | null, null, null, $embed_config["author"] === null ? null : new Author($formatter($embed_config["author"])), $fields); 155 | } 156 | 157 | foreach($config["to_discord_channels"] as $cid){ 158 | $this->plugin->getDiscord()->getApi()->sendMessage(null, $cid, $formatter($config["format"]["text"]??""), null, $embeds)->otherwise(function(ApiRejection $rejection){ 159 | $this->plugin->getLogger()->warning("Failed to send discord message on minecraft message event, '{$rejection->getMessage()}'"); 160 | }); 161 | } 162 | } 163 | 164 | public function onMinecraftCommand(CommandEvent $event): void{ 165 | if(!$this->ready){ 166 | //Unlikely to happen, discord will most likely be ready before anyone even joins. 167 | $this->plugin->getLogger()->debug("Ignoring command event, discord is not ready."); 168 | return; 169 | } 170 | 171 | /** @var array $config */ 172 | $config = $this->config["commands"]["minecraft"]; 173 | if(!$config['enabled']){ 174 | return; 175 | } 176 | $player = $event->getSender(); 177 | if(!$player instanceof Player){ 178 | return; 179 | } 180 | 181 | $message = $event->getCommand(); 182 | $args = explode(" ", $message); 183 | $command = array_shift($args); 184 | $world = $player->getWorld()->getDisplayName(); 185 | 186 | $from_worlds = is_array($config["from_worlds"]) ? $config["from_worlds"] : [$config["from_worlds"]]; 187 | if(!in_array("*", $from_worlds)){ 188 | //Only specific worlds. 189 | if(!in_array($world, $from_worlds)){ 190 | $this->plugin->getLogger()->debug("Ignoring command event, world '$world' is not listed."); 191 | return; 192 | } 193 | } 194 | 195 | $formatter = function(string $text) use ($player, $message, $world, $command, $args): string{ 196 | $text = str_replace(["{USERNAME}", "{username}", "{PLAYER}", "{player}"], $player->getName(), $text); 197 | $text = str_replace(["{DISPLAYNAME}", "{displayname}", "{DISPLAY_NAME}", "{display_name}", "{NICKNAME}", "{nickname}"], $player->getDisplayName(), $text); 198 | $text = str_replace(["{MESSAGE}", "{message}"], $message, $text); 199 | $text = str_replace(["{COMMAND}", "{command}"], $command, $text); 200 | $text = str_replace(["{ARGS}", "{args}"], implode(" ", $args), $text); 201 | $text = str_replace(["{XUID}", "{xuid}"], $player->getXuid(), $text); 202 | $text = str_replace(["{UUID}", "{uuid}"], $player->getUniqueId()->toString(), $text); 203 | $text = str_replace(["{ADDRESS}", "{address}", "{IP}", "{ip}"], $player->getNetworkSession()->getIp(), $text); 204 | $text = str_replace(["{PORT}", "{port}"], strval($player->getNetworkSession()->getPort()), $text); 205 | $text = str_replace(["{WORLD}", "{world}", "{LEVEL}", "{level}"], $world, $text); 206 | $text = str_replace(["{TIME}", "{time}", "{TIME-1}", "{time-1}"], date("H:i:s", time()), $text); 207 | $text = str_replace(["{TIME-2}", "{time-2}"], date("H:i", time()), $text); 208 | return str_replace(["{TIME-3}", "{time-3}"], "", $text); //TODO Other formatted times supported by discord. 209 | }; 210 | 211 | $embeds = []; 212 | $embed_config = $config["format"]["embed"]; 213 | if($embed_config["enabled"]){ 214 | $fields = []; 215 | foreach($embed_config["fields"] as $field){ 216 | $fields[] = new Field($formatter($field["name"]), $formatter($field["value"]), $field["inline"]); 217 | } 218 | $embeds[] = new Embed(($embed_config["title"] === null ? null : $formatter($embed_config["title"])), 219 | ($embed_config["description"] === null ? null : $formatter($embed_config["description"])), 220 | $embed_config["url"], $embed_config["time"] ? time() : null, $embed_config["colour"], 221 | $embed_config["footer"] === null ? null : new Footer($formatter($embed_config["footer"])), null, 222 | null, null, null, $embed_config["author"] === null ? null : new Author($formatter($embed_config["author"])), $fields); 223 | } 224 | 225 | foreach($config["to_discord_channels"] as $cid){ 226 | $this->plugin->getDiscord()->getApi()->sendMessage(null, $cid, $formatter($config["format"]["text"]??""), null, $embeds)->otherwise(function(ApiRejection $rejection){ 227 | $this->plugin->getLogger()->warning("Failed to send discord message on minecraft command event, '{$rejection->getMessage()}'"); 228 | }); 229 | } 230 | } 231 | 232 | public function onMinecraftJoin(PlayerJoinEvent $event): void{ 233 | if(!$this->ready){ 234 | //Unlikely to happen, discord will most likely be ready before anyone even joins. 235 | $this->plugin->getLogger()->debug("Ignoring join event, discord is not ready."); 236 | return; 237 | } 238 | 239 | /** @var array $config */ 240 | $config = $this->config["join"]["minecraft"]; 241 | if(!$config['enabled']){ 242 | return; 243 | } 244 | 245 | $player = $event->getPlayer(); 246 | 247 | $formatter = function(string $text) use ($player): string{ 248 | $text = str_replace(["{USERNAME}", "{username}", "{PLAYER}", "{player}"], $player->getName(), $text); 249 | $text = str_replace(["{DISPLAYNAME}", "{displayname}", "{DISPLAY_NAME}", "{display_name}", "{NICKNAME}", "{nickname}"], $player->getDisplayName(), $text); 250 | $text = str_replace(["{XUID}", "{xuid}"], $player->getXuid(), $text); 251 | $text = str_replace(["{UUID}", "{uuid}"], $player->getUniqueId()->toString(), $text); 252 | $text = str_replace(["{ADDRESS}", "{address}", "{IP}", "{ip}"], $player->getNetworkSession()->getIp(), $text); 253 | $text = str_replace(["{PORT}", "{port}"], strval($player->getNetworkSession()->getPort()), $text); 254 | $text = str_replace(["{TIME}", "{time}", "{TIME-1}", "{time-1}"], date("H:i:s", time()), $text); 255 | $text = str_replace(["{TIME-2}", "{time-2}"], date("H:i", time()), $text); 256 | return str_replace(["{TIME-3}", "{time-3}"], "", $text); //TODO Other formatted times supported by discord. 257 | }; 258 | 259 | $embeds = []; 260 | $embed_config = $config["format"]["embed"]; 261 | if($embed_config["enabled"]){ 262 | $fields = []; 263 | foreach($embed_config["fields"] as $field){ 264 | $fields[] = new Field($formatter($field["name"]), $formatter($field["value"]), $field["inline"]); 265 | } 266 | $embeds[] = new Embed(($embed_config["title"] === null ? null : $formatter($embed_config["title"])), 267 | ($embed_config["description"] === null ? null : $formatter($embed_config["description"])), 268 | $embed_config["url"], $embed_config["time"] ? time() : null, $embed_config["colour"], 269 | $embed_config["footer"] === null ? null : new Footer($formatter($embed_config["footer"])), null, 270 | null, null, null, $embed_config["author"] === null ? null : new Author($formatter($embed_config["author"])), $fields); 271 | } 272 | 273 | foreach($config["to_discord_channels"] as $cid){ 274 | $this->plugin->getDiscord()->getApi()->sendMessage(null, $cid, $formatter($config["format"]["text"]??""), null, $embeds)->otherwise(function(ApiRejection $rejection){ 275 | $this->plugin->getLogger()->warning("Failed to send discord message on minecraft join event, '{$rejection->getMessage()}'"); 276 | }); 277 | } 278 | } 279 | 280 | public function onMinecraftLeave(PlayerQuitEvent $event): void{ 281 | if(!$this->ready){ 282 | //Unlikely to happen, discord will most likely be ready before anyone even joins. 283 | $this->plugin->getLogger()->debug("Ignoring leave event, discord is not ready."); 284 | return; 285 | } 286 | 287 | /** @var array $config */ 288 | $config = $this->config["leave"]["minecraft"]; 289 | if(!$config['enabled']){ 290 | return; 291 | } 292 | 293 | $player = $event->getPlayer(); 294 | $reason = $event->getQuitReason(); 295 | if($reason instanceof Translatable){ 296 | $reason = $reason->getText(); 297 | } 298 | if($reason === "transfer" and $config["ignore_transferred"]){ 299 | return; 300 | } 301 | 302 | $formatter = function(string $text) use ($player, $reason): string{ 303 | $text = str_replace(["{USERNAME}", "{username}", "{PLAYER}", "{player}"], $player->getName(), $text); 304 | $text = str_replace(["{DISPLAYNAME}", "{displayname}", "{DISPLAY_NAME}", "{display_name}", "{NICKNAME}", "{nickname}"], $player->getDisplayName(), $text); 305 | $text = str_replace(["{XUID}", "{xuid}"], $player->getXuid(), $text); 306 | $text = str_replace(["{UUID}", "{uuid}"], $player->getUniqueId()->toString(), $text); 307 | $text = str_replace(["{ADDRESS}", "{address}", "{IP}", "{ip}"], $player->getNetworkSession()->getIp(), $text); 308 | $text = str_replace(["{PORT}", "{port}"], strval($player->getNetworkSession()->getPort()), $text); 309 | $text = str_replace(["{REASON}", "{reason}"], $reason, $text); 310 | $text = str_replace(["{TIME}", "{time}", "{TIME-1}", "{time-1}"], date("H:i:s", time()), $text); 311 | $text = str_replace(["{TIME-2}", "{time-2}"], date("H:i", time()), $text); 312 | return str_replace(["{TIME-3}", "{time-3}"], "", $text); //TODO Other formatted times supported by discord. 313 | }; 314 | 315 | $embeds = []; 316 | $embed_config = $config["format"]["embed"]; 317 | if($embed_config["enabled"]){ 318 | $fields = []; 319 | foreach($embed_config["fields"] as $field){ 320 | $fields[] = new Field($formatter($field["name"]), $formatter($field["value"]), $field["inline"]); 321 | } 322 | $embeds[] = new Embed(($embed_config["title"] === null ? null : $formatter($embed_config["title"])), 323 | ($embed_config["description"] === null ? null : $formatter($embed_config["description"])), 324 | $embed_config["url"], $embed_config["time"] ? time() : null, $embed_config["colour"], 325 | $embed_config["footer"] === null ? null : new Footer($formatter($embed_config["footer"])), null, 326 | null, null, null, $embed_config["author"] === null ? null : new Author($formatter($embed_config["author"])), $fields); 327 | } 328 | 329 | foreach($config["to_discord_channels"] as $cid){ 330 | $this->plugin->getDiscord()->getApi()->sendMessage(null, $cid, $formatter($config["format"]["text"]??""), null, $embeds)->otherwise(function(ApiRejection $rejection){ 331 | $this->plugin->getLogger()->warning("Failed to send discord message on minecraft leave event, '{$rejection->getMessage()}'"); 332 | }); 333 | } 334 | } 335 | 336 | public function onMinecraftTransferred(PlayerTransferEvent $event): void{ 337 | if(!$this->ready){ 338 | //Unlikely to happen, discord will most likely be ready before anyone even joins. 339 | $this->plugin->getLogger()->debug("Ignoring transfer event, discord is not ready."); 340 | return; 341 | } 342 | 343 | /** @var array $config */ 344 | $config = $this->config["transferred"]["minecraft"]; 345 | if(!$config['enabled']){ 346 | return; 347 | } 348 | 349 | $player = $event->getPlayer(); 350 | $address = $event->getAddress(); 351 | $port = $event->getPort(); 352 | 353 | $formatter = function(string $text) use ($player, $address, $port): string{ 354 | $text = str_replace(["{USERNAME}", "{username}", "{PLAYER}", "{player}"], $player->getName(), $text); 355 | $text = str_replace(["{DISPLAYNAME}", "{displayname}", "{DISPLAY_NAME}", "{display_name}", "{NICKNAME}", "{nickname}"], $player->getDisplayName(), $text); 356 | $text = str_replace(["{XUID}", "{xuid}"], $player->getXuid(), $text); 357 | $text = str_replace(["{UUID}", "{uuid}"], $player->getUniqueId()->toString(), $text); 358 | $text = str_replace(["{ADDRESS}", "{address}", "{IP}", "{ip}"], $player->getNetworkSession()->getIp(), $text); 359 | $text = str_replace(["{PORT}", "{port}"], strval($player->getNetworkSession()->getPort()), $text); 360 | $text = str_replace(["{SERVER_ADDRESS}", "{server_address}"], $address, $text); 361 | $text = str_replace(["{SERVER_PORT}", "{server_port}"], strval($port), $text); 362 | $text = str_replace(["{TIME}", "{time}", "{TIME-1}", "{time-1}"], date("H:i:s", time()), $text); 363 | $text = str_replace(["{TIME-2}", "{time-2}"], date("H:i", time()), $text); 364 | return str_replace(["{TIME-3}", "{time-3}"], "", $text); //TODO Other formatted times supported by discord. 365 | }; 366 | 367 | $embeds = []; 368 | $embed_config = $config["format"]["embed"]; 369 | if($embed_config["enabled"]){ 370 | $fields = []; 371 | foreach($embed_config["fields"] as $field){ 372 | $fields[] = new Field($formatter($field["name"]), $formatter($field["value"]), $field["inline"]); 373 | } 374 | $embeds[] = new Embed(($embed_config["title"] === null ? null : $formatter($embed_config["title"])), 375 | ($embed_config["description"] === null ? null : $formatter($embed_config["description"])), 376 | $embed_config["url"], $embed_config["time"] ? time() : null, $embed_config["colour"], 377 | $embed_config["footer"] === null ? null : new Footer($formatter($embed_config["footer"])), null, 378 | null, null, null, $embed_config["author"] === null ? null : new Author($formatter($embed_config["author"])), $fields); 379 | } 380 | 381 | foreach($config["to_discord_channels"] as $cid){ 382 | $this->plugin->getDiscord()->getApi()->sendMessage(null, $cid, $formatter($config["format"]["text"]??""), null, $embeds)->otherwise(function(ApiRejection $rejection){ 383 | $this->plugin->getLogger()->warning("Failed to send discord message on minecraft transfer event, '{$rejection->getMessage()}'"); 384 | }); 385 | } 386 | } 387 | 388 | //--- Discord Events -> Minecraft Server ---// 389 | 390 | /** 391 | * @priority MONITOR 392 | */ 393 | public function onDiscordMemberJoin(MemberJoined $event): void{ 394 | /** @var array $config */ 395 | $config = $this->config["join"]["discord"]; 396 | if(!$config['enabled']){ 397 | return; 398 | } 399 | 400 | if(!in_array($event->getMember()->getGuildId(), $config["servers"])){ 401 | $this->plugin->getLogger()->debug("Ignoring discord member join event, discord guild '".$event->getMember()->getGuildId()."' is not in config list."); 402 | return; 403 | } 404 | 405 | $this->api->fetchGuild($event->getMember()->getGuildId())->then(function(ApiResolution $res) use($event, $config){ 406 | /** @var Guild $server */ 407 | $server = $res->getData()[0]; 408 | $this->api->fetchUser($event->getMember()->getUserId())->then(function(ApiResolution $res) use($server, $event, $config){ 409 | /** @var User $user */ 410 | $user = $res->getData()[0]; 411 | $member = $event->getMember(); 412 | 413 | //Format message. 414 | $message = str_replace(['{NICKNAME}', '{nickname}'], $member->getNickname()??$user->getUsername(), $config['format']); 415 | $message = str_replace(['{USERNAME}', '{username}'], $user->getUsername(), $message); 416 | $message = str_replace(['{USER_DISCRIMINATOR}', '{user_discriminator}', '{DISCRIMINATOR}', '{discriminator}'], $user->getDiscriminator(), $message); 417 | $message = str_replace(['{SERVER}', '{server}'], $server->getName(), $message); 418 | 419 | $message = str_replace(['{TIME}', '{time}', '{TIME-1}', '{time-1}'], date('G:i:s', time()), $message); 420 | $message = str_replace(['{TIME-2}', '{time-2}'], date('G:i', time()), $message); 421 | if(!is_string($message)){ 422 | throw new AssertionError("A string is always expected, got '".gettype($message)."'"); 423 | } 424 | 425 | //Broadcast. 426 | $this->plugin->getServer()->broadcastMessage($message, $this->getPlayersInWorlds($config['to_minecraft_worlds'])); 427 | }, function(ApiRejection $rej) use($event){ 428 | $this->plugin->getLogger()->warning("Failed to process discord member join event, failed to fetch user '".$event->getMember()->getUserId()."' - ".$rej->getMessage()); 429 | }); 430 | }, function(ApiRejection $rej) use($event){ 431 | $this->plugin->getLogger()->warning("Failed to process discord member join event, failed to fetch guild '".$event->getMember()->getGuildId()."' - ".$rej->getMessage()); 432 | }); 433 | } 434 | 435 | /** 436 | * Who doesn't like some copy/paste? 437 | * @priority MONITOR 438 | */ 439 | public function onDiscordMemberLeave(MemberLeft $event): void{ 440 | /** @var array $config */ 441 | $config = $this->config["leave"]["discord"]; 442 | if(!$config['enabled']){ 443 | return; 444 | } 445 | 446 | if(!in_array($event->getGuildId(), $config["servers"])){ 447 | $this->plugin->getLogger()->debug("Ignoring discord member leave event, discord guild '".$event->getGuildId()."' is not in config list."); 448 | return; 449 | } 450 | 451 | $this->api->fetchGuild($event->getGuildId())->then(function(ApiResolution $res) use($config, $event){ 452 | /** @var Guild $server */ 453 | $server = $res->getData()[0]; 454 | $this->api->fetchUser($event->getUserId())->then(function(ApiResolution $res) use($server, $event, $config){ 455 | /** @var User $user */ 456 | $user = $res->getData()[0]; 457 | $member = $event->getCachedMember(); 458 | if($member === null){ 459 | $this->api->fetchMember($event->getGuildId(), $event->getUserId())->then(function(ApiResolution $res) use ($server, $user, $config){ 460 | /** @var Member $member */ 461 | $member = $res->getData()[0]; 462 | 463 | //Format message. 464 | $message = str_replace(['{NICKNAME}', '{nickname}'], $member->getNickname()??$user->getUsername(), $config['format']); 465 | $message = str_replace(['{USERNAME}', '{username}'], $user->getUsername(), $message); 466 | $message = str_replace(['{USER_DISCRIMINATOR}', '{user_discriminator}', '{DISCRIMINATOR}', '{discriminator}'], $user->getDiscriminator(), $message); 467 | $message = str_replace(['{SERVER}', '{server}'], $server->getName(), $message); 468 | 469 | $message = str_replace(['{TIME}', '{time}', '{TIME-1}', '{time-1}'], date('G:i:s', time()), $message); 470 | $message = str_replace(['{TIME-2}', '{time-2}'], date('G:i', time()), $message); 471 | if(!is_string($message)){ 472 | throw new AssertionError("A string is always expected, got '".gettype($message)."'"); 473 | } 474 | 475 | //Broadcast. 476 | $this->plugin->getServer()->broadcastMessage($message, $this->getPlayersInWorlds($config['to_minecraft_worlds'])); 477 | }, function(ApiRejection $rej) use ($event){ 478 | $this->plugin->getLogger()->warning("Failed to process discord member leave event, failed to fetch member '".$event->getUserId()."' - ".$rej->getMessage()); 479 | }); 480 | }else{ 481 | //Format message. 482 | $message = str_replace(['{NICKNAME}', '{nickname}'], $member->getNickname()??$user->getUsername(), $config['format']); 483 | $message = str_replace(['{USERNAME}', '{username}'], $user->getUsername(), $message); 484 | $message = str_replace(['{USER_DISCRIMINATOR}', '{user_discriminator}', '{DISCRIMINATOR}', '{discriminator}'], $user->getDiscriminator(), $message); 485 | $message = str_replace(['{SERVER}', '{server}'], $server->getName(), $message); 486 | 487 | $message = str_replace(['{TIME}', '{time}', '{TIME-1}', '{time-1}'], date('G:i:s', time()), $message); 488 | $message = str_replace(['{TIME-2}', '{time-2}'], date('G:i', time()), $message); 489 | if(!is_string($message)){ 490 | throw new AssertionError("A string is always expected, got '".gettype($message)."'"); 491 | } 492 | 493 | //Broadcast. 494 | $this->plugin->getServer()->broadcastMessage($message, $this->getPlayersInWorlds($config['to_minecraft_worlds'])); 495 | } 496 | }, function(ApiRejection $rej) use($event){ 497 | $this->plugin->getLogger()->warning("Failed to process discord member leave event, failed to fetch user '".$event->getUserId()."' - ".$rej->getMessage()); 498 | }); 499 | }, function(ApiRejection $rej) use($event){ 500 | $this->plugin->getLogger()->warning("Failed to process discord member leave event, failed to fetch guild '".$event->getGuildId()."' - ".$rej->getMessage()); 501 | }); 502 | } 503 | 504 | /** 505 | * @priority MONITOR 506 | */ 507 | public function onDiscordMessage(MessageSent $event): void{ 508 | /** @var array $config */ 509 | $config = $this->config["messages"]["discord"]; 510 | if(!$config['enabled']){ 511 | return; 512 | } 513 | if(!in_array(($msg = $event->getMessage())->getType(), [MessageType::DEFAULT, MessageType::CHAT_INPUT_COMMAND, MessageType::REPLY])){ 514 | $this->plugin->getLogger()->debug("Ignoring message '{$msg->getId()}', not a valid type."); 515 | return; 516 | } 517 | 518 | if(!in_array($msg->getChannelId(), $config['from_channels'])){ 519 | $this->plugin->getLogger()->debug("Ignoring message from channel '{$msg->getChannelId()}', ID is not in list."); 520 | return; 521 | } 522 | 523 | if($msg->getAuthorId() === null){ 524 | $this->plugin->getLogger()->debug("Ignoring message '{$msg->getId()}', no author ID."); 525 | return; 526 | } 527 | 528 | if(strlen(trim($msg->getContent()??"")) === 0){ 529 | //Files or other type of messages. 530 | $this->plugin->getLogger()->debug("Ignoring message '{$msg->getId()}', No text content."); 531 | return; 532 | } 533 | 534 | $this->api->fetchChannel(null, $msg->getChannelId())->then(function(ApiResolution $res) use($msg, $config){ 535 | /** @var Channel $channel */ 536 | $channel = $res->getData()[0]; 537 | if($channel->getGuildId() === null){ 538 | $this->plugin->getLogger()->debug("Ignoring message '{$msg->getId()}', channel is not in a guild."); 539 | return; 540 | } 541 | $this->api->fetchGuild($channel->getGuildId())->then(function(ApiResolution $res) use ($channel, $msg, $config){ 542 | /** @var Guild $server */ 543 | $server = $res->getData()[0]; 544 | $this->api->fetchUser($msg->getAuthorId())->then(function(ApiResolution $res) use ($server, $channel, $msg, $config){ 545 | /** @var User $user */ 546 | $user = $res->getData()[0]; 547 | $this->api->fetchMember($channel->getGuildId(), $user->getId())->then(function(ApiResolution $res) use ($server, $user, $channel, $msg, $config){ 548 | /** @var Member $member */ 549 | $member = $res->getData()[0]; 550 | 551 | //Format message. 552 | $message = str_replace(['{NICKNAME}', '{nickname}'], $member->getNickname()??$user->getUsername(), $config['format']); 553 | $message = str_replace(['{USERNAME}', '{username}'], $user->getUsername(), $message); 554 | $message = str_replace(['{USER_DISCRIMINATOR}', '{user_discriminator}', '{DISCRIMINATOR}', '{discriminator}'], $user->getDiscriminator(), $message); 555 | $message = str_replace(['{MESSAGE}', '{message'], trim($msg->getContent()??""), $message); 556 | $message = str_replace(['{SERVER}', '{server}'], $server->getName(), $message); 557 | $message = str_replace(['{CHANNEL}', '{channel}'], $channel->getName()??"Unknown", $message); 558 | $message = str_replace(['{TIME}', '{time}', '{TIME-1}', '{time-1}'], date('G:i:s', $msg->getTimestamp()), $message); 559 | $message = str_replace(['{TIME-2}', '{time-2}'], date('G:i', $msg->getTimestamp()), $message); 560 | if(!is_string($message)){ 561 | throw new AssertionError("A string is always expected, got '".gettype($message)."'"); 562 | } 563 | 564 | //Broadcast. 565 | $this->plugin->getServer()->broadcastMessage($message, $this->getPlayersInWorlds($config['to_minecraft_worlds'])); 566 | }, function(ApiRejection $rej) use ($msg){ 567 | $this->plugin->getLogger()->warning("Failed to process discord message event, failed to fetch member '".$msg->getAuthorId()."' - ".$rej->getMessage()); 568 | }); 569 | }, function(ApiRejection $rej) use ($msg){ 570 | $this->plugin->getLogger()->warning("Failed to process discord message event, failed to fetch user '".$msg->getAuthorId()."' - ".$rej->getMessage()); 571 | }); 572 | }, function(ApiRejection $rej) use ($channel){ 573 | $this->plugin->getLogger()->warning("Failed to process discord message event, failed to fetch guild '".$channel->getGuildId()."' - ".$rej->getMessage()); 574 | }); 575 | }, function(ApiRejection $rej) use ($msg){ 576 | $this->plugin->getLogger()->warning("Failed to process discord message event, failed to fetch channel '".$msg->getChannelId()."' - ".$rej->getMessage()); 577 | }); 578 | } 579 | 580 | /** 581 | * Fetch players based on config worlds entry. 582 | * 583 | * @param string|string[] $worlds 584 | * @return Player[] 585 | */ 586 | private function getPlayersInWorlds(array|string $worlds): array{ 587 | $players = []; 588 | if($worlds === "*" or (is_array($worlds) and sizeof($worlds) === 1 and $worlds[0] === "*")){ 589 | $players = $this->plugin->getServer()->getOnlinePlayers(); 590 | }else{ 591 | foreach((is_array($worlds) ? $worlds : [$worlds]) as $world){ 592 | $world = $this->plugin->getServer()->getWorldManager()->getWorldByName($world); 593 | if($world === null){ 594 | $this->plugin->getLogger()->warning("World '$world' specified in config.yml does not exist."); 595 | }else{ 596 | $players = array_merge($players, $world->getPlayers()); 597 | } 598 | } 599 | } 600 | return $players; 601 | } 602 | } -------------------------------------------------------------------------------- /src/Main.php: -------------------------------------------------------------------------------- 1 | checkPrerequisites(); 28 | $this->saveAllResources(); 29 | // TODO Option for showing console commands in Discord not just player executed commands. 30 | } 31 | 32 | public function onEnable(): void{ 33 | if(!$this->loadConfig()){ 34 | return; 35 | } 36 | $listener = new EventListener($this); 37 | $this->getServer()->getPluginManager()->registerEvents($listener, $this); 38 | } 39 | 40 | private function saveAllResources(): void{ 41 | //Help files. 42 | $dir = scandir(Phar::running() . "/resources/help"); 43 | if($dir === false){ 44 | throw new PluginException("Failed to get help resources internal path."); 45 | } 46 | foreach($dir as $file){ 47 | $this->saveResource("help/" . $file, true); 48 | } 49 | $this->saveResource("config.yml"); 50 | } 51 | 52 | private function checkPrerequisites(): void{ 53 | //Phar 54 | if(Phar::running() === ""){ 55 | throw new PluginException("Plugin running from source, please use a release phar."); 56 | } 57 | 58 | //DiscordBot 59 | $discordBot = $this->getServer()->getPluginManager()->getPlugin("DiscordBot"); 60 | if($discordBot === null){ 61 | return; //Will never happen. 62 | } 63 | if($discordBot->getDescription()->getWebsite() !== "https://github.com/DiscordBot-PMMP/DiscordBot"){ 64 | throw new PluginException("Incompatible dependency 'DiscordBot' detected, see https://github.com/DiscordBot-PMMP/DiscordBot/releases for the correct plugin."); 65 | } 66 | $ver = new VersionString($discordBot->getDescription()->getVersion()); 67 | if($ver->getMajor() !== 3){ 68 | throw new PluginException("Incompatible dependency 'DiscordBot' detected, v3.x.y is required however v{$ver->getBaseVersion()}) is installed, see https://github.com/DiscordBot-PMMP/DiscordBot/releases for downloads."); 69 | } 70 | if(!$discordBot instanceof DiscordBot){ 71 | throw new PluginException("Incompatible dependency 'DiscordBot' detected."); 72 | } 73 | $this->discord = $discordBot; 74 | } 75 | 76 | private function loadConfig(): bool{ 77 | $this->getLogger()->debug("Loading configuration..."); 78 | 79 | /** @var array $config */ 80 | $config = yaml_parse_file($this->getDataFolder()."config.yml"); 81 | if(!$config or !is_int($config["version"]??"")){ 82 | $this->getLogger()->critical("Failed to parse config.yml"); 83 | $this->getServer()->getPluginManager()->disablePlugin($this); 84 | return false; 85 | } 86 | $this->getLogger()->debug("Config loaded, version: ".$config["version"]); 87 | 88 | if(intval($config["version"]) !== ConfigUtils::VERSION){ 89 | $old = $config["version"]; 90 | $this->getLogger()->info("Updating your config from v".$old." to v".ConfigUtils::VERSION); 91 | ConfigUtils::update($config); 92 | rename($this->getDataFolder()."config.yml", $this->getDataFolder()."config.yml.v".$old); 93 | yaml_emit_file($this->getDataFolder()."config.yml", $config); 94 | $this->getLogger()->notice("Config updated, old config was saved to '{$this->getDataFolder()}config.yml.v".$old."'"); 95 | } 96 | 97 | $this->getLogger()->debug("Verifying config..."); 98 | $result_raw = ConfigUtils::verify($config); 99 | if(sizeof($result_raw) !== 0){ 100 | $result = TextFormat::RED."There were some problems with your config.yml, see below:\n".TextFormat::RESET; 101 | foreach($result_raw as $value){ 102 | $result .= "$value\n"; 103 | } 104 | $this->getLogger()->error(rtrim($result)); 105 | $this->getServer()->getPluginManager()->disablePlugin($this); 106 | return false; 107 | } 108 | $this->getLogger()->debug("Config verified."); 109 | 110 | //Config is now updated and verified. 111 | return true; 112 | } 113 | 114 | public function getDiscord(): DiscordBot{ 115 | return $this->discord; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /test/github/get-php.sh: -------------------------------------------------------------------------------- 1 | PHP=$"8.1" #Change this to void cache, 002 2 | PM=$"5" 3 | GIT_TAG=$"php-$PHP-latest" 4 | 5 | wget -q -O - "https://github.com/pmmp/PHP-Binaries/releases/download/$GIT_TAG/PHP-Linux-x86_64-PM$PM.tar.gz" | tar -zx > /dev/null 2>&1 6 | chmod +x ./bin/php7/bin/* 7 | 8 | EXTENSION_DIR=$(find "$(pwd)/bin" -name *debug-zts*) #make sure this only captures from `bin` in case the user renamed their old binary folder 9 | #Modify extension_dir directive if it exists, otherwise add it 10 | LF=$'\n' 11 | grep -q '^extension_dir' bin/php7/bin/php.ini && sed -i'bak' "s{^extension_dir=.*{extension_dir=\"$EXTENSION_DIR\"{" bin/php7/bin/php.ini || sed -i'bak' "1s{^{extension_dir=\"$EXTENSION_DIR\"\\$LF{" bin/php7/bin/php.ini --------------------------------------------------------------------------------