├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/jsonSchemas.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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
--------------------------------------------------------------------------------