Compare commits

...

145 commits

Author SHA1 Message Date
Norbi Peti 9eb2354b3f
Fix Core loading in test... somehow
I just tried stuff until it started working
2023-07-20 00:16:49 +02:00
Norbi Peti 5615bbcb22
FIx things and attempt to add test
The test can't find a YAML method
2023-07-01 20:13:07 +02:00
Norbi Peti cde0d44b04
Readd check after changing the underlying list 2023-06-30 03:20:40 +02:00
Norbi Peti 0d59dff86e
Reverted back to Scala 2.13
- The IntelliJ plugin still isn't working properly after 2 years so it's really annoying to dev using Scala 3
- (Probably) fixed DPUtils.reply not sending a message if a Mono.empty() was passed as a channel
- Moved the Discord user property from LastMessageData to CustomLMD as we don't need the user anymore to check permissions (we don't need to create a fake sender)
- ...even if we do, it should store the sender itself maybe

- Spent hours reverting and debugging Scala 2 issues because apaprently that's also very buggy
- Implemented Player interface directly in some senders because it was required for... reasons
- Moved the commenter to a separate class after a looot of debugging (API was provided)
2023-06-30 02:29:01 +02:00
Norbi Peti 222d393420
Rename DiscordPlayer to DiscordUser
About time
It's been like that since I started this project in 2016
It's not representing an in-game player, it's representing a DC user
2023-06-29 02:16:39 +02:00
Norbi Peti 02e05e360e
Update config options and fix other issues and update Scala 2023-06-29 02:10:20 +02:00
Norbi Peti 6bd1de6217
Convert config fields to methods 2023-05-06 14:33:12 +02:00
Norbi Peti 5d0bd37b28
Fixed all compile errors
- Removed all hacky code (for now)
- Removed other dependencies because I kept getting load errors, mostly related to the examination api
- Added some dependencies because I kept getting load errors, mostly related to the examination api
- Removed use of Channel.extraIdentifiers because it would not accept it, claiming that the ListConfigData class file is broken
2023-04-24 23:53:00 +02:00
Norbi Peti f909eb4779
Attempted to update to latest API, lots of errors (9 remains)
- Removed Reactor Scala extensions in an attempt to get this thing to compile but it doesn't seem to help
- Removed heavily version-dependent stuff
2023-04-24 04:42:24 +02:00
Norbi Peti 8d63394b55
Fix connect command test 2022-07-21 02:34:03 +02:00
Norbi Peti ac3e6655e6
Update sender to support slash commands 2022-07-21 02:24:53 +02:00
Norbi Peti 0610ee434b
Fix broadcast message check (#134)
- Now it keeps track of Bukkit broadcast messages instead of checking the stack trace
2022-07-07 01:13:23 +02:00
Norbi Peti a4f1463314
Add one half of broadcasted message check and move mcchat classes 2022-07-06 23:55:02 +02:00
Norbi Peti 6183c034c6
Merge branch 'scala'
# Conflicts:
#	pom.xml
#	src/main/java/buttondevteam/discordplugin/mcchat/MCListener.java
#	src/main/scala/buttondevteam/discordplugin/ChromaBot.java
#	src/main/scala/buttondevteam/discordplugin/ChromaBot.scala
#	src/main/scala/buttondevteam/discordplugin/DiscordPlugin.java
#	src/main/scala/buttondevteam/discordplugin/DiscordPlugin.scala
#	src/main/scala/buttondevteam/discordplugin/listeners/CommandListener.java
2022-01-05 01:35:34 +01:00
Norbi Peti 2a985509fb
Remove custom command handling and start slash commands
They don't work yet, but show up on Discord so that's something
2022-01-05 01:31:24 +01:00
Norbi Peti cafd8096fa
Fix even more returns and Discord->MC chat 2021-12-31 00:38:28 +01:00
Norbi Peti 9f3ca37929
Fix compilation issues and startup exceptions
- Removed test class because it errors, and I don't know how to fix it
- Updated dependencies
- Fixed SMono.whenDelayError() causing a crash on Scala 3.0.0
- Fixed subscribe method calls being ambiguous
- Fixed returns returning the wrong things
- Converted onGetInfo() to use no returns
2021-12-30 21:25:50 +01:00
Norbi Peti 263c652d68
Fix subcommand detection, fix returns 2021-08-26 02:03:06 +02:00
Norbi Peti 7b27ec0ea3
Update to Scala 3.0.0 and update dependencies 2021-07-08 23:04:17 +02:00
Norbi Peti c49fac5ce5
Fix command parameter name saving and other things 2021-04-06 23:17:31 +02:00
Norbi Peti fd63e995ff
Add command parameter name saving 2021-04-06 22:32:12 +02:00
Norbi Peti ad3bd451ba
Save all of the config comments and include in JAR 2021-04-06 02:25:23 +02:00
Norbi Peti 74bce1ecf9
Save config comments (not all of them apparently) 2021-04-06 01:05:58 +02:00
Norbi Peti d80703ac68
Obtain config comments from sources 2021-04-06 00:35:59 +02:00
Norbi Peti 470212411d
Successfully made an unnecessary subproject work 2021-04-05 18:57:40 +02:00
Norbi Peti 5146fdf368
Add Reflections 2021-04-05 03:47:56 +02:00
Norbi Peti 860dd66431
Add task to read source files 2021-04-05 02:45:28 +02:00
Norbi Peti efa1dcfc8f
Convert to SBT project (manually), including shading 2021-04-04 00:32:27 +02:00
Norbi Peti 3f6135f427
Revert "Convert some classes to Scala"
This reverts commit 261725dc0f.
2021-03-20 14:35:15 +01:00
Norbi Peti 1b1a592a1e
It compiles! Remove LPInjector for now
The IDE and the Scala compiler don't agree on what is or isn't needed
2021-03-09 23:55:02 +01:00
Norbi Peti a84cd4e8e3
Fix all Scala errors! 2021-03-09 03:41:04 +01:00
Norbi Peti a0a7f756c4
Implicit classes for conversion, more fixing
Added 'extension methods' to convert to Scala-friendly formats easily
2021-03-09 02:47:11 +01:00
Norbi Peti 7296ebd2f8
Tailrec announcer method, fix some compile issues 2021-03-06 01:27:21 +01:00
Norbi Peti d416eef144
Make some small functions 2021-03-02 01:40:58 +01:00
Norbi Peti fce6b91b97
Use Scala version of Reactor & data types 2021-03-02 01:18:20 +01:00
Norbi Peti c57ac26b2d
All classes converted that I wanted 2021-03-01 02:07:40 +01:00
Norbi Peti 9f47509dcb
Converted mcchat classes to Scala 2021-02-26 02:27:59 +01:00
Norbi Peti 428361c46c
Convert some more classes to Scala
Actually, a lot of them
2021-02-25 01:44:43 +01:00
Norbi Peti 261725dc0f
Convert some classes to Scala
Because why not
Except... It doesn't work. Yet.
2021-02-15 22:24:14 +01:00
Norbi Peti b18f6beba9
Add more relocations to fix compatibility issues 2021-02-09 23:00:40 +01:00
Norbi Peti cbc9728c02
Try using a different path for the script 2020-11-01 19:14:47 +01:00
Norbi Peti 9576c0ba1d
Use install-jdk.sh for Java 8 as well 2020-11-01 18:57:25 +01:00
Norbi Peti 1fe367a96c
Use Java 8 for Spigot, Java 11 for others (CI) 2020-11-01 18:42:04 +01:00
Norbi Peti 28cff3ed43
Fix mcchat crash on config issue 2020-11-01 13:54:07 +01:00
Norbi Peti 491b5e4ee9
Fix commands not working in some cases
#98
Also unregistering DC commands
Also removed the command string from the unknown command message
2020-10-30 23:50:08 +01:00
Norbi Peti 64994ee44e
Fix some things, disable some modules by default 2020-10-30 00:56:08 +01:00
Norbi Peti e57974ebcd
Remove debug msg and set version 2020-10-28 00:32:59 +01:00
Norbi Peti 324f5e756c
Detect restarts by reading *everything* logged
The server uses sout to print the message we're interested in...
Hopefully a check like this won't put any significant load on the server
2020-10-27 15:02:42 +01:00
Norbi Peti 2b549227a6
Aw man 2020-10-26 23:05:03 +01:00
Norbi Peti 61986c9b51
Fix server start message not being displayed 2020-10-26 22:56:13 +01:00
Norbi Peti 4d234cf832
Fix vanish player count and no crash on fast disable
#130
2020-10-26 21:16:08 +01:00
Norbi Peti 7866ddbe06
Config conversion 2020-10-26 20:01:00 +01:00
Norbi Peti fdcab1acb2
Fix LPInjector and player data stuff 2020-10-25 21:49:09 +01:00
Norbi Peti 40fe1093e0
Player data things, LPInjector update start 2020-10-25 01:59:10 +02:00
Norbi Peti d3ae53cd46
Merge pull request #131 from TBMCPlugins/dependabot/maven/junit-junit-4.13.1
Bump junit from 3.8.1 to 4.13.1
2020-10-14 19:48:25 +02:00
dependabot[bot] 67a66c0c44
Bump junit from 3.8.1 to 4.13.1
Bumps [junit](https://github.com/junit-team/junit4) from 3.8.1 to 4.13.1.
- [Release notes](https://github.com/junit-team/junit4/releases)
- [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.13.1.md)
- [Commits](https://github.com/junit-team/junit4/commits/r4.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-10-13 06:37:54 +00:00
Norbi Peti a27a262858
LPInjector and mcchat fixes
Fixed LPInjector registering to the Core
Stop MCChatListener from having multiple active instances
MinecraftChatModule.state instead of all the flags
Showing MinecraftChatModule enable/disable on Discord
/discord reset --> restart
Wait for each shutdown message to send on shutdown (although it hasn't really been an issue so far)
This means using Mono<?> in a lot of places
Also added a contract (IntelliJ) to warn if not subscribed
Faking getOnlinePlayers() is unnecessary and causes too much trouble
Today's work
2020-10-10 00:29:21 +02:00
Norbi Peti ccc15aa048
Log exceptions using the plugin/component logger
And fix LPInjector loading multiple times / once
2020-10-09 00:10:36 +02:00
Norbi Peti 891be91d69
Fix some mcchat and a reset issue
Using ConcurrentHashMaps (#62)
Add custom /list to hide vanished players (#120)
Fixed /discord reset for non-Paper servers (#103)
2020-10-08 00:02:49 +02:00
Norbi Peti 56d13ebf9f
Update to Discord4J v3.1.1
It wasn't as complicated as I expected
2020-10-07 22:27:20 +02:00
Norbi Peti 7cebb74835
Switch back to mocking
The ByteBuddy solution complains about adding/removing fields
But making a delegating mock maker is easy enough
2020-10-07 20:09:35 +02:00
Norbi Peti 3f2fa286fb
Attempt to use ByteBuddy directly for ServerWatcher 2020-10-07 00:52:59 +02:00
Norbi Peti beae6e6ce0
Config and such 2020-09-12 01:54:45 +02:00
Norbi Peti cd2132ba45
Fix ServerWatcher, mcchat works 2020-09-11 22:27:22 +02:00
Norbi Peti 666f05ff12
Attempts at mocking the server, fixes 2020-09-11 01:20:12 +02:00
Norbi Peti d784d8b1e2
Fix /mcchat not using the configured prefix 2020-09-07 01:09:39 +02:00
Norbi Peti 3ee1eb3dec
An even smaller fix could go an even longer way 2020-08-31 23:55:46 +02:00
Norbi Peti 80a0312b1f
Role color config, removed role debug, role fixes 2020-08-30 02:23:21 +02:00
Norbi Peti 6bf91afab9
Fixes, fix vanilla command handling on 1.16
And on unsupported versions too
2020-07-31 01:43:02 +02:00
Norbi Peti 6b60135867
NorbiPeti has completed the challenge [Bullseye]
Using the custom event to detect player /stop as now restarts can come from there too (btw it defaults to stop unless a command is ran)
Added support for advancements in 1.16:
- Now each player gets effectively a reference to the player list for advancements, and since I simply call the method on the original object, it will pass that on, instead of my mock
- I tried calling the method that sends the reference on the mock in that case, but that just results in Mockito's version being called which means infinite recursion or something like that (I didn't get a StackOverflowError actually but the server didn't respond)
- I tried implementing the method myself but actually I never tested it because at that point I was convinced I can call the original the right way
- I had an educated guess that the mock is a subclass of the original class, so I just need to call super.method() right?
- But how do you call that reflectively? Apparently Method.invoke() will always respect overriden methods; but thanks to StackOverflow I found out about MethodHandles, and it has the perfect thing I need, findSpecial / unreflectSpecial
- Only problem is, kind of like how you can only use super.method() inside the class, you can only use the 'special' methods inside the class... So how could I make it run inside the mocked class? I have no idea since I can only supply an Answer object which has no connection to it, but apparently all the lookup() method actually does is call a constructor with the caller's class - so let's call the constructor! Which is, of course private
- So now I have a reflection call creating a Lookup object which can get a handle to the method without checking any overrides and then using that handle to call the original method with the 'this' parameter being the mock
#128
2020-07-30 01:54:58 +02:00
Norbi Peti 50500e87b9
Component logging and toggle for vanilla cmds
Config option to enable or disable vanilla command support in mcchat
2020-06-30 00:47:46 +02:00
Norbi Peti ce71ff2dd6
Add support for 1.16, mostly 2020-06-27 03:02:32 +02:00
Norbi Peti 4ecd32f0ad
Update D4J and fix LuckPerms support 2020-05-27 15:45:14 +02:00
Norbi Peti a9c71a3384
Possibly fix the Minecraft role bug
So it seems like there is a Minecraft role in another server as well
#95
2020-04-08 11:52:21 +02:00
Norbi Peti cce7f59f4a
Fix issue with VanillaCommandListener on <1.15 2020-04-08 00:13:19 +02:00
Norbi Peti b484fe6f64
Fix vanish player update
There's still no message on vanish so it can be figured out from the chat history
2020-03-20 02:02:37 +01:00
Norbi Peti 50cc0c8e61
Use reflection for VanillaCommandListener
Moved error handling to the wrapper
Fixed commands on Discord getting executed even if the preprocess event got cancelled
2020-03-15 23:55:02 +01:00
Norbi Peti 1b747ab99f
Use command channels and fix dependencies 2020-03-15 03:10:27 +01:00
Norbi Peti 45a1ba4fe1 Refactor DC->MC into more methods, remove channel handling
The permCheck won't be used for chat commands, needs fixing
2020-03-11 12:35:23 +01:00
Norbi Peti 037ec3b9dd Use VCL14 for 1.15, player sender mock
Using VanillaCommandListener14 for 1.15 as well
Using a mock for the DiscordPlayerSender too, to reduce the amount of code
Made the mocks stub-only, which should lower memory usage
2020-03-11 12:00:20 +01:00
Norbi Peti 454265cd6f Update player list on (un)vanish
#120
2020-02-18 17:18:17 +01:00
Norbi Peti ffdf5a2f18 Fix custom chat PL update NPE (#124) 2020-02-17 13:02:15 +01:00
Norbi Peti 1fa2635317 Set up GitHub Releases deploy
TBMCPlugins/ChromaCore#76
2020-02-10 12:56:48 +01:00
Norbi Peti d58a7e819a Some documentation and updates 2020-02-05 16:49:07 +01:00
Norbi Peti 26971459ac Rename 2020-02-04 17:44:59 +01:00
Norbi Peti 940b601061 Fixed custom chat player list update
The problem was that for some reason I created a field that was already present in the parent class and it just happened to be pretty much never used before the PL update
Also updated EssentialsX dependency
2020-02-03 13:20:38 +01:00
Norbi Peti bcd7f7b810
Merge pull request #123 from TBMCPlugins/dev
Fixed many issues, default config values, improvements
2020-02-01 20:15:57 +01:00
Norbi Peti de07503bc3
Documentation, split messages that are too big
#122
Removed some default values
Disallowing MC commands that could error when not loaded (#121)
Other fixes
2020-02-01 19:10:13 +01:00
Norbi Peti bdb7381ab4
Finished some issues
Fixed join messages appearing in addition to custom ones (#119)
For real this time
Not saying the game role color is the default one (#118)
Fixed role listing (#80)
2020-01-18 03:54:17 +01:00
Norbi Peti 703f1f8cd5
Finished some of the half-completed issues and others
Processing custom emotes (#48)
Made role listing fancier (#80)
Trying to reload config before reset (#113)
Allowing /discord reset if the login fails, clarified how to get a token (#111)
2019-12-27 23:45:49 +01:00
Norbi Peti b481bb0aa9
Fixed join msgs, vanished players in desc. and others
Update D4J
Fixed join messages appearing when they shouldn't (#119)
Only showing players who can see the channel (#91)
Fixed vanished players appearing in the channel descriptions (#120)
2019-12-27 21:14:52 +01:00
Norbi Peti 3a94b6191b
Update D4J and some bugfixes
The update fixes the numerous errors about a missing status constant
Remove test check (#114)
Improvements and checking for admin permission (#115)
It also checks for channel perms now
2019-11-28 00:16:58 +01:00
Norbi Peti 19463963e3
Make channels default to 0, profile URL config
Fix URL not-escaping
Made the plugin only attempt to access channels that are not set to 0
#110
2019-11-16 01:52:49 +01:00
Norbi Peti 02f60c2162
Fix client ID race condition, attempt to fix URL escaping 2019-11-07 00:21:58 +01:00
Norbi Peti 30e2da094a
Merge pull request #108 from TBMCPlugins/dev
1.14 support, better error handling
2019-10-30 19:43:30 +01:00
Norbi Peti 0e4a9ff7e0
Merge branch 'master' into dev 2019-10-30 19:41:29 +01:00
Norbi Peti 9caf4c54ed
Fixed that last error
#106
2019-10-30 15:05:31 +01:00
Norbi Peti 98ee2ce771
Fixed the MassiveCore error but there's more
#106
2019-10-27 23:27:54 +01:00
Norbi Peti 73fc4aedcc
Fixed DCP inventory, have no idea about MassiveCore
Essentials error is fixed
#106
2019-10-27 02:33:13 +01:00
Norbi Peti 88c1d100e9
Using parent & hopefully fixed #109 2019-10-23 02:40:34 +02:00
Norbi Peti f1cec2ced1
Update to Java 10/11 & fake player remove
Apparently
2019-10-20 20:50:29 +02:00
Norbi Peti 3bd7b879c4
Hopefully fixed the #106 issue
Also removed mock check a while ago
2019-09-15 03:34:02 +02:00
Norbi Peti ef44aa8830
Fix build (Java 8) 2019-09-11 15:33:02 +02:00
Norbi Peti acd45ce4ae
Hopefully fixed the issue of multiple ready events
#107
2019-09-11 15:30:07 +02:00
Norbi Peti ea0ef88068
Automatically get client ID for link, improvements
Literally 2 improvements
2019-08-26 22:26:10 +02:00
Norbi Peti e4d5dcd0a2
Update D4J & role debug refined & fixed a )
PLW tested and confirmed working
Moved a ) to fix #105
2019-08-26 00:57:16 +02:00
Norbi Peti 91f1c8c4f7
Probably fixed PLW
Mostly yesterday
2019-08-25 03:23:14 +02:00
Norbi Peti 2963990b5e
Check if Minecraft is a game role 2019-08-23 21:52:52 +02:00
Norbi Peti c456b24333
Command handling improvement, PLW work
More correct fallback for command handling
Getting closer to finish PlayerListWatcher
2019-08-23 01:58:47 +02:00
Norbi Peti 42e91409c7
Work on 1.14 PLW support 2019-08-14 21:29:19 +02:00
Norbi Peti e88684a564
Error handling, 1.14 vanilla command support
Error handling (not today)
Added support for vanilla commands on 1.14
2019-08-14 00:53:51 +02:00
Norbi Peti 7a9e7de138
Made player list, custom and private chat toggleable
The last parts of #51
2019-08-08 14:31:01 +02:00
Norbi Peti 480032a3d6
Fix of getInfo and role adding/removing
Fixed getInfo if the player isn't on the DC server
Fixed message after adding or removing a game role
2019-08-08 13:45:40 +02:00
Norbi Peti 7db3b17090
Made DCP version-independent & msg reset
Some things may not be implemented yet
Also added a wrapper around the vanilla cmd listener so Mockito doesn't complain about the missing class
Msg reset fixed (#101)
2019-07-22 22:35:15 +02:00
Norbi Peti 5a5f653b86
Added DC user mention tabcomplete...
But only for commands, because that's how it works now apparently
#16
Also might have made it 1.14 ready, though I switched the dependency back to 1.12
Oh it's 1.12.2...
2019-07-08 02:00:08 +02:00
Norbi Peti 4082c2abbf
Fixes, error handling
Sync events
Vanilla command error handling
(Older changes ^)
Fixed exception Coder pinging
2019-06-30 22:05:33 +02:00
Norbi Peti 0e24344efd
PLW error handling 2019-06-09 23:33:23 +02:00
Norbi Peti 27ddad537f
Only one ready event allowed 2019-06-08 22:44:03 +02:00
Norbi Peti bc160a21b7
Merge pull request #99 from TBMCPlugins/dev
Updated to Discord4J v3, permission injection, improvements
2019-06-06 22:45:21 +02:00
Norbi Peti 34e3bb0ead
Debug cmd fix, mcchat fix, config error fix
Also removed debug messages
The Minecraft chat didn't run if the command channel was different
The debug command didn't run if the mod role didn't exist
2019-06-06 19:40:15 +02:00
Norbi Peti 9edfcf6a3d
Main server fix, clean test OK
WTF ID fixed
2019-06-05 00:29:34 +02:00
Norbi Peti 939c6f4970
Server ready fix & main server fix attempt 2019-06-01 02:27:40 +02:00
Norbi Peti 5472929113
Added, 5xFixed
Added mcchat login/logout messages
Only printing timings messages if debug is on

Fixed #93
F i x e d
Fixed command handled status
Fixed debug command
Fixed error reporting limit
Fixed Discord YEEHAW
2019-05-31 00:38:28 +02:00
Norbi Peti 545b8130e0
Added timings and a line that solves everything
cb.setStoreService(new JdkStoreService());
2019-05-30 18:45:44 +02:00
Norbi Peti bf538d9bd0
LP injecting, fake player fixes
#96
LuckPerms
Added support for excluding plugins from certain events from code
Also might have fixed some message timeouts by relocating netty
2019-05-25 01:48:06 +02:00
Norbi Peti 2bdba0af22
Calling login events
#96 isn't fixed by this
2019-05-20 23:35:22 +02:00
Norbi Peti ac61225730
Update, bot cmd fix, debugging 2019-05-20 21:51:28 +02:00
Norbi Peti 9e8f988ea6
Renaming config methods to match keys 2019-05-20 13:07:20 +02:00
Norbi Peti b396ec47b4
Some fixes, some debugging
#93
2019-05-11 00:15:50 +02:00
Norbi Peti beab400873
Some debugging, some fixes
#93
2019-05-10 21:39:25 +02:00
Norbi Peti f266924a9a More reacts 2019-05-06 15:47:01 +02:00
Norbi Peti b68456e6f4
Even more fixes
Actually made the channel data return a Mono and used that
Used the read only config stuff
#93
2019-05-04 03:11:03 +02:00
Norbi Peti 4881f6bdd2
Some more fixes
Made the channel data return a Mono and used that
#93
2019-05-01 00:50:24 +02:00
Norbi Peti eeb1955ebe
Bunch of fixes
#93
2019-04-25 19:52:43 +02:00
Norbi Peti e2e8a58c4e
It compiles!
And I thought this is gonna be easy
#93
2019-04-25 02:50:55 +02:00
Norbi Peti 2500572e0d Even more refactoring 2019-04-24 17:50:13 +02:00
Norbi Peti 59066ce09a Refactoring... 2019-04-24 16:50:00 +02:00
Norbi Peti 038cb98f1a Refactoring & made mcchat teleport config 2019-04-24 13:29:52 +02:00
Norbi Peti 95af050517 The refactoring continues 2019-04-17 17:54:08 +02:00
Norbi Peti 12ca6fbfb5 More converting 2019-04-17 16:51:42 +02:00
Norbi Peti d8d75c063b Started converting to new D4J
#93
2019-04-17 13:52:01 +02:00
Norbi Peti 55c61cef98 New command sys for /discord & inv link 2019-04-15 15:36:48 +02:00
Irinyi Kabinet Felhasználó c8680ae92c Merge branch 'master' into dev 2019-04-15 14:51:45 +02:00
Norbi Peti d2aea8559a Server ready conf, doc 2019-04-10 13:50:26 +02:00
Norbi Peti 8e2538e553
Ignore case for role command 2019-04-05 17:26:10 +02:00
107 changed files with 4136 additions and 7577 deletions

View file

@ -4,6 +4,7 @@ end_of_line = lf
insert_final_newline = false
indent_style = space
indent_size = 4
ij_any_field_annotation_wrap = off
[*.json]
indent_style = space
@ -12,6 +13,8 @@ indent_size = 2
[*.java]
indent_style = tab
tab_width = 4
ij_java_do_not_wrap_after_single_annotation = true
ij_java_field_annotation_wrap = off
[{*.yml, *.yaml}]
indent_style = space

451
.gitignore vendored
View file

@ -1,225 +1,226 @@
#################
## Eclipse
#################
*.pydevproject
.metadata/
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
target/
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
build/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.scc
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.Publish.xml
*.pubxml
*.publishproj
# NuGet Packages Directory
## TO!DO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/
# Windows Azure Build Output
csx
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
#############
## Windows detritus
#############
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac crap
.DS_Store
#############
## Python
#############
*.py[cod]
# Packages
*.egg
*.egg-info
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
.metadata/*
TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar
*.iml
*.name
.idea/compiler.xml
*.xml
Token.txt
#################
## Eclipse
#################
*.pydevproject
.metadata/
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
target/
.project
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
x64/
build/
[Bb]in/
[Oo]bj/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.scc
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
*.ncrunch*
.*crunch*.local.xml
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.Publish.xml
*.pubxml
*.publishproj
# NuGet Packages Directory
## TO!DO: If you have NuGet Package Restore enabled, uncomment the next line
#packages/
# Windows Azure Build Output
csx
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.[Pp]ublish.xml
*.pfx
*.publishsettings
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
App_Data/*.mdf
App_Data/*.ldf
#############
## Windows detritus
#############
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac crap
.DS_Store
#############
## Python
#############
*.py[cod]
# Packages
*.egg
*.egg-info
dist/
build/
eggs/
parts/
var/
sdist/
develop-eggs/
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
.metadata/*
TheButtonAutoFlair/out/artifacts/Autoflair/Autoflair.jar
*.iml
*.name
.idea/compiler.xml
*.xml
Token.txt
.bsp

View file

@ -2,25 +2,24 @@ cache:
directories:
- $HOME/.m2/repository/org/
before_install: | # Wget BuildTools and run if cached folder not found
export JAVA_HOME=$HOME/oraclejdk8
~/bin/install-jdk.sh --install oraclejdk8 --target $JAVA_HOME
if [ ! -d "$HOME/.m2/repository/org/spigotmc/spigot/1.12.2-R0.1-SNAPSHOT" ]; then
wget -O BuildTools.jar https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar
# grep so that download counts don't appear in log files
java -jar BuildTools.jar --rev 1.12.2 | grep -vE "[^/ ]*/[^/ ]*\s*KB\s*$" | grep -v "^\s*$"
fi
export JAVA_HOME=$HOME/oraclejdk11
~/bin/install-jdk.sh --install oraclejdk11 --target $JAVA_HOME
language: java
jdk:
- oraclejdk8
- oraclejdk11
sudo: true
deploy:
# deploy develop to the staging environment
- provider: script
script: chmod +x deploy.sh && sh deploy.sh staging
- provider: releases
api_key:
secure: "AnGhHbIFAdfFEtxuAv3Bue5n80JJi9FMyXc2rstg1zzBbuvaYqCUWcL7lB34Ffs9E6Mo5wfANnBRyXtdSfXWLLsb0k78vvlAwJyLP2s0V7r9LTvQWJEflslSDX6ySF0YIc3LR+XMYv+qj+e5XnWr6Kj6gRkaQPDzSIYRgvfKZv9fbo4lSqnp2OcOrn4sYyeV6+sLhARLaNTvbDOdLiMRP3JZUrKB4OCt8+XhrVMlwKhglJY8JVKx1uAFxeljfZ3WJ1WnJ6L8coyVR18HAl1/VHwn+a/jeGp9AygTPF2yJL3+TJs37afqG9jjbL0pneSUoyNenKkjD19pM2RgjGmW25kPEvnmGMaWn8UN8bbilB5k8sh27z+/6qNnvYbhJxJi9RJNASpd6JQ1cGZoBziKsvhkLXErRNCJ92wlgGOqE78UWf6dvzmVkQD/vaqXhHlmcVhpoUZUqsXGWw5gOw/Kxh90IUwsV6A2JXf1Q3YRPIfDin/tVpud6eB0hiRW2uLnAqDMUUsH6n8cqd2rzg/02uKD5rF5+amxfdUSf3m/Fh6lqklNlO1188rFULUPpyFRas59UpEIRqT2Ae9OJPg+1QrVln2Gd3l059MHumX2tidQl00GzXx4nB1NkxpKLl9JLf/n6A4MQodY5CkRELUTB8K3zHGhJdj1Dtmis5YwhXk="
file: 'target/Chroma-Discord.jar'
on:
branch: dev
skip_cleanup: true
# deploy master to production
- provider: script
script: chmod +x deploy.sh && sh deploy.sh production
on:
branch: master
tags: true
skip_cleanup: true

View file

@ -1,11 +1,10 @@
# DiscordPlugin
A plugin that controls the ChromaBot Discord bot and provides Minecraft chat functionality and other features.
# Chroma-Discord
A plugin that provides Minecraft chat functionality and other features.
## Features
### Announce new posts from /r/ChromaGamers
If it's a (distinguished) moderator post, it'll be posted to the \#announcements channel, otherwise it'll be posted and pinned to \#general.
## Setup
This plugin needs Chroma-Core to work. If you have that and this plugin, start the server, and follow the instructions.
You'll need a Discord application made, and a bot account created for it.
You can restart the plugin using /discord restart without having to restart the whole server.
### Announce server restarts
It announces server starts/stops and restarts, as well as if the server shut down unexpectedly.
**For more, see:** http://chromapedia.wikia.com/wiki/ChromaBot
## Building
Maven is used to build this project with all of its dependencies. You will need Spigot 1.12.2 and 1.14.4 built using BuildTools.

67
build.sbt Normal file
View file

@ -0,0 +1,67 @@
name := "Chroma-Discord"
version := "1.1"
scalaVersion := "2.13.11"
resolvers += "jitpack.io" at "https://jitpack.io"
resolvers += "paper-repo" at "https://papermc.io/repo/repository/maven-public/"
resolvers += Resolver.mavenLocal
// assembly / assemblyOption := (assembly / assemblyOption).value.copy(includeScala = false)
libraryDependencies ++= Seq(
"io.papermc.paper" % "paper-api" % "1.19-R0.1-SNAPSHOT" % Provided,
"com.discord4j" % "discord4j-core" % "3.2.3",
"com.vdurmont" % "emoji-java" % "5.1.1",
"io.projectreactor" % "reactor-scala-extensions_2.13" % "0.8.0",
"com.github.TBMCPlugins.ChromaCore" % "Chroma-Core" % "v2.0.0-SNAPSHOT" % Provided,
"net.ess3" % "EssentialsX" % "2.17.1" % Provided,
// https://mvnrepository.com/artifact/com.mojang/brigadier
"com.mojang" % "brigadier" % "1.0.500" % "provided",
// https://mvnrepository.com/artifact/net.kyori/examination-api
"net.kyori" % "examination-api" % "1.3.0" % "provided",
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib
//"org.jetbrains.kotlin" % "kotlin-stdlib" % "1.8.20" % "provided",
// https://mvnrepository.com/artifact/org.scalatest/scalatest
"org.scalatest" %% "scalatest" % "3.2.16" % Test,
// https://mvnrepository.com/artifact/com.github.seeseemelk/MockBukkit-v1.19
"com.github.seeseemelk" % "MockBukkit-v1.19" % "2.29.0" % Test,
"com.github.milkbowl" % "vault" % "master-SNAPSHOT" % Test
)
assembly / assemblyJarName := "Chroma-Discord.jar"
assembly / assemblyShadeRules := Seq(
"io.netty", "com.fasterxml", "org.slf4j"
).map { p =>
ShadeRule.rename(s"$p.**" -> "btndvtm.dp.@0").inAll
}
assembly / assemblyMergeStrategy := {
case PathList("META-INF", "io.netty.versions.properties") => MergeStrategy.concat
// https://stackoverflow.com/a/55557287/457612
case "module-info.class" => MergeStrategy.discard
case "META-INF/versions/9/module-info.class" => MergeStrategy.discard
case x => (assembly / assemblyMergeStrategy).value(x)
}
val saveConfigComments = TaskKey[Seq[File]]("saveConfigComments")
saveConfigComments := {
Commenter.saveConfigComments((Compile / sources).value)
}
Compile / resourceGenerators += saveConfigComments
//scalacOptions ++= Seq("-release", "17", "--verbose")
scalacOptions ++= Seq("-release", "17")
//Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat
Test / fork := true // This changes the tests ran through sbt to work like IntelliJ tests, fixes mocking issues
/*excludeDependencies ++= Seq(
ExclusionRule("org.bukkit"),
ExclusionRule("io.papermc.paper"),
ExclusionRule("com.destroystokyo")
)*/
excludeDependencies ++= Seq(
ExclusionRule("net.milkbowl.vault", "VaultAPI")
)

View file

@ -1 +0,0 @@
lombok.var.flagUsage = ALLOW

246
pom.xml
View file

@ -1,246 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.TBMCPlugins</groupId>
<artifactId>DiscordPlugin</artifactId>
<version>master-SNAPSHOT</version>
<packaging>jar</packaging>
<name>DiscordPlugin</name>
<url>http://maven.apache.org</url>
<build>
<!-- <sourceDirectory>target/generated-sources/delombok</sourceDirectory>
<testSourceDirectory>target/generated-test-sources/delombok</testSourceDirectory> -->
<sourceDirectory>src/main/java</sourceDirectory>
<resources>
<resource>
<directory>src</directory>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>*.properties</include>
<include>*.yml</include>
<include>*.csv</include>
<include>*.txt</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
<finalName>DiscordPlugin</finalName>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<excludes>
<exclude>org.spigotmc:spigot-api</exclude>
<exclude>com.github.TBMCPlugins.ButtonCore:ButtonCore</exclude>
<exclude>net.ess3:Essentials</exclude>
</excludes> <!-- http://stackoverflow.com/questions/28458058/maven-shade-plugin-exclude-a-dependency-and-all-its-transitive-dependencies -->
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>copy</id>
<phase>compile</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>target</outputDirectory>
<resources>
<resource>
<directory>resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!-- <plugin> <groupId>org.projectlombok</groupId> <artifactId>lombok-maven-plugin</artifactId>
<version>1.16.16.0</version> <executions> <execution> <id>delombok</id> <phase>generate-sources</phase>
<goals> <goal>delombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory>
<sourceDirectory>src/main/java</sourceDirectory> <verbose>true</verbose>
</configuration> </execution> <execution> <id>test-delombok</id> <phase>generate-test-sources</phase>
<goals> <goal>testDelombok</goal> </goals> <configuration> <addOutputDirectory>false</addOutputDirectory>
<sourceDirectory>src/test/java</sourceDirectory> </configuration> </execution>
</executions> </plugin> -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<useSystemClassLoader>false
</useSystemClassLoader> <!-- https://stackoverflow.com/a/53012553/2703239 -->
</configuration>
</plugin>
</plugins>
</build>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<branch>
master
</branch> <!-- Should be master if building ButtonCore locally - the CI will overwrite it (see below) -->
</properties>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository> <!-- This repo fixes issues with transitive dependencies -->
<id>jcenter</id>
<url>http://jcenter.bintray.com</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<!-- <repository>
<id>vault-repo</id>
<url>http://nexus.hc.to/content/repositories/pub_releases</url>
</repository> -->
<repository>
<id>Essentials</id>
<url>http://repo.ess3.net/content/repositories/essrel/</url>
</repository>
<repository>
<id>projectlombok.org</id>
<url>http://projectlombok.org/mavenrepo</url>
</repository>
<!-- <repository>
<id>pex-repo</id>
<url>http://pex-repo.aoeu.xyz</url>
</repository> -->
</repositories>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.12-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot</artifactId>
<version>1.12.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.discord4j/Discord4J -->
<dependency>
<groupId>com.discord4j</groupId>
<artifactId>Discord4J</artifactId>
<version>2.10.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>com.github.TBMCPlugins.ButtonCore</groupId>
<artifactId>ButtonCore</artifactId>
<version>${branch}-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.milkbowl</groupId> <!-- net.milkbowl.vault -->
<artifactId>VaultAPI</artifactId>
<version>master-SNAPSHOT</version> <!-- 1.6 -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.ess3</groupId>
<artifactId>Essentials</artifactId>
<version>2.13.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.xaanit</groupId>
<artifactId>D4J-OAuth</artifactId>
<version>master-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
<scope>provided</scope>
</dependency>
<!-- <dependency>
<groupId>ru.tehkode</groupId>
<artifactId>PermissionsEx</artifactId>
<version>1.23.1</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
</exclusion>
</exclusions>
</dependency> -->
<!-- https://mvnrepository.com/artifact/org.objenesis/objenesis -->
<dependency>
<groupId>org.objenesis</groupId>
<artifactId>objenesis</artifactId>
<version>2.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.vdurmont</groupId>
<artifactId>emoji-java</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
<profiles>
<profile>
<id>ci</id>
<activation>
<property>
<name>env.TRAVIS_BRANCH</name>
</property>
</activation>
<properties>
<!-- Override only if necessary -->
<branch>${env.TRAVIS_BRANCH}</branch>
</properties>
</profile>
</profiles>
</project>

96
project/Commenter.scala Normal file
View file

@ -0,0 +1,96 @@
import java.util.regex.Pattern
import sbt.*
import org.bukkit.configuration.file.YamlConfiguration
import scala.io.Source
import scala.util.Using
object Commenter {
def saveConfigComments(sv: Seq[File]): Seq[File] = {
val cdataRegex = Pattern.compile("(?:def|val|var) (\\w+)(?::[^=]+)? = (?:getI?Config|DPUtils.\\w+Data)") //Hack: DPUtils
val clRegex = Pattern.compile("class (\\w+).* extends ((?:\\w|\\d)+)")
val objRegex = Pattern.compile("object (\\w+)")
val subRegex = Pattern.compile("def `?(\\w+)`?")
val subParamRegex = Pattern.compile("((?:\\w|\\d)+): ((?:\\w|\\d)+)")
val configConfig = new YamlConfiguration()
val commandConfig = new YamlConfiguration()
def getConfigComments(line: String, clKey: String, comment: String, justCommented: Boolean): (String, String, Boolean) = {
val clMatcher = clRegex.matcher(line)
if (clKey == null && clMatcher.find()) { //First occurrence
(if (clMatcher.group(2).contains("Component"))
"components." + clMatcher.group(1)
else "global", comment, justCommented)
} else if (line.contains("/**")) {
(clKey, "", false)
} else if (line.contains("*/") && comment != null)
(clKey, comment, true)
else if (comment != null) {
if (justCommented) {
if (clKey != null) {
val matcher = cdataRegex.matcher(line)
if (matcher.find())
configConfig.set(s"$clKey.${matcher.group(1)}", comment.trim)
}
else {
val matcher = objRegex.matcher(line)
if (matcher.find()) {
val clName = matcher.group(1)
val compKey = if (clName.contains("Module")) s"component.$clName"
else if (clName.contains("Plugin")) "global"
else null
if (compKey != null)
configConfig.set(s"$compKey.generalDescriptionInsteadOfAConfig", comment.trim)
}
}
(clKey, null, false)
}
else (clKey, comment + line.replaceFirst("^\\s*\\*\\s+", "") + "\n", justCommented)
}
else (clKey, comment, justCommented)
}
for (file <- sv) {
Using(Source.fromFile(file)) { src =>
var clKey: String = null
var comment: String = null
var justCommented: Boolean = false
var subCommand = false
var pkg: String = null
var clName: String = null
for (line <- src.getLines) {
val (clk, c, jc) = getConfigComments(line, clKey, comment, justCommented)
clKey = clk; comment = c; justCommented = jc
val objMatcher = objRegex.matcher(line)
val clMatcher = clRegex.matcher(line)
if (pkg == null && line.startsWith("package "))
pkg = line.substring("package ".length)
else if (clName == null && objMatcher.find())
clName = objMatcher.group(1)
else if (clName == null && clMatcher.find())
clName = clMatcher.group(1)
val subMatcher = subRegex.matcher(line)
val subParamMatcher = subParamRegex.matcher(line)
val sub = line.contains("@Subcommand") || line.contains("@Command2.Subcommand")
if (sub) subCommand = true
else if (line.contains("}")) subCommand = false
if (subCommand && subMatcher.find()) {
/*val groups = (2 to subMatcher.groupCount()).map(subMatcher.group)
val pairs = for (i <- groups.indices by 2) yield (groups(i), groups(i + 1))*/
val mname = subMatcher.group(1)
val params = Iterator.continually(()).takeWhile(_ => subParamMatcher.find())
.map(_ => subParamMatcher.group(1)).drop(1)
val section = commandConfig.createSection(s"$pkg.$clName.$mname")
section.set("method", s"$mname()")
section.set("params", params.mkString(" "))
subCommand = false
}
}
configConfig.save("target/configHelp.yml")
commandConfig.save("target/commands.yml")
}.recover[Unit]({ case t => t.printStackTrace() })
}
Seq(file("target/configHelp.yml"), file("target/commands.yml"))
}
}

2
project/build.properties Normal file
View file

@ -0,0 +1,2 @@
sbt.version=1.8.2
scala.version=2.13.11

7
project/build.sbt Normal file
View file

@ -0,0 +1,7 @@
//lazy val commenter = RootProject(file("../commenter"))
//lazy val root = (project in file(".")).dependsOn(commenter)
resolvers += Resolver.mavenLocal
resolvers += "paper-repo" at "https://papermc.io/repo/repository/maven-public/"
libraryDependencies += "io.papermc.paper" % "paper-api" % "1.19.4-R0.1-SNAPSHOT" % Compile

1
project/plugins.sbt Normal file
View file

@ -0,0 +1 @@
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")

View file

@ -1,27 +0,0 @@
package buttondevteam.discordplugin;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@RequiredArgsConstructor
public class AsyncDiscordEvent<T extends sx.blah.discord.api.events.Event> extends Event implements Cancellable {
private final @Getter T event;
@Getter
@Setter
private boolean cancelled;
private static final HandlerList handlers = new HandlerList();
@Override
public HandlerList getHandlers() {
return handlers;
}
public static HandlerList getHandlerList() {
return handlers;
}
}

View file

@ -1,15 +0,0 @@
package buttondevteam.discordplugin;
public enum ChannelconBroadcast {
JOINLEAVE,
AFK,
RESTART,
DEATH,
BROADCAST;
public final int flag;
ChannelconBroadcast() {
this.flag = 1 << this.ordinal();
}
}

View file

@ -1,146 +0,0 @@
package buttondevteam.discordplugin;
import buttondevteam.discordplugin.mcchat.MCChatUtils;
import lombok.Getter;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitScheduler;
import sx.blah.discord.api.internal.json.objects.EmbedObject;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.util.EmbedBuilder;
import javax.annotation.Nullable;
import java.awt.*;
public class ChromaBot {
/**
* May be null if it's not initialized. Initialization happens after the server is done loading (using {@link BukkitScheduler#runTaskAsynchronously(org.bukkit.plugin.Plugin, Runnable)})
*/
private static @Getter ChromaBot instance;
private DiscordPlugin dp;
/**
* This will set the instance field.
*
* @param dp
* The Discord plugin
*/
ChromaBot(DiscordPlugin dp) {
instance = this;
this.dp = dp;
}
static void delete() {
instance = null;
}
/**
* Send a message to the chat channel and private chats.
*
* @param message
* The message to send, duh
*/
public void sendMessage(String message) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message));
}
/**
* Send a message to the chat channels and private chats.
*
* @param message
* The message to send, duh
* @param embed
* Custom fancy stuff, use {@link EmbedBuilder} to create one
*/
public void sendMessage(String message, EmbedObject embed) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed));
}
/**
* Send a message to the chat channels, private chats and custom chats.
*
* @param message The message to send, duh
* @param embed Custom fancy stuff, use {@link EmbedBuilder} to create one
* @param toggle The toggle type for channelcon
*/
public void sendMessageCustomAsWell(String message, EmbedObject embed, @Nullable ChannelconBroadcast toggle) {
MCChatUtils.forCustomAndAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, embed), toggle, false);
}
/**
* Send a message to an arbitrary channel. This will not send it to the private chats.
*
* @param channel
* The channel to send to, use the channel variables in {@link DiscordPlugin}
* @param message
* The message to send, duh
* @param embed
* Custom fancy stuff, use {@link EmbedBuilder} to create one
*/
public void sendMessage(IChannel channel, String message, EmbedObject embed) {
DiscordPlugin.sendMessageToChannel(channel, message, embed);
}
/**
* Send a fancy message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
*/
public void sendMessage(String message, Color color) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message,
new EmbedBuilder().withTitle(message).withColor(color).build()));
}
/**
* Send a fancy message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
* @param mcauthor
* The name of the Minecraft player who is the author of this message
*/
public void sendMessage(String message, Color color, String mcauthor) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message,
DPUtils.embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), mcauthor).build()));
}
/**
* Send a fancy message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
* @param authorname
* The name of the author of this message
* @param authorimg
* The URL of the avatar image for this message's author
*/
public void sendMessage(String message, Color color, String authorname, String authorimg) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, new EmbedBuilder()
.withTitle(message).withColor(color).withAuthorName(authorname).withAuthorIcon(authorimg).build()));
}
/**
* Send a message to the chat channels. This will show a bold text with a colored line.
*
* @param message
* The message to send, duh
* @param color
* The color of the line before the text
* @param sender
* The player who sends this message
*/
public void sendMessage(String message, Color color, Player sender) {
MCChatUtils.forAllMCChat(ch -> DiscordPlugin.sendMessageToChannel(ch, message, DPUtils
.embedWithHead(new EmbedBuilder().withTitle(message).withColor(color), sender.getName()).build()));
}
public void updatePlayerList() {
MCChatUtils.updatePlayerList();
}
}

View file

@ -1,171 +0,0 @@
package buttondevteam.discordplugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.architecture.IHaveConfig;
import lombok.val;
import org.bukkit.Bukkit;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IIDLinkedObject;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.util.EmbedBuilder;
import sx.blah.discord.util.RequestBuffer;
import sx.blah.discord.util.RequestBuffer.IRequest;
import sx.blah.discord.util.RequestBuffer.IVoidRequest;
import javax.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import java.util.regex.Matcher;
public final class DPUtils {
public static EmbedBuilder embedWithHead(EmbedBuilder builder, String playername) {
return builder.withAuthorIcon("https://minotar.net/avatar/" + playername + "/32.png");
}
/**
* Removes §[char] colour codes from strings & escapes them for Discord <br>
* Ensure that this method only gets called once (escaping)
*/
public static String sanitizeString(String string) {
return escape(sanitizeStringNoEscape(string));
}
/**
* Removes §[char] colour codes from strings
*/
public static String sanitizeStringNoEscape(String string) {
String sanitizedString = "";
boolean random = false;
for (int i = 0; i < string.length(); i++) {
if (string.charAt(i) == '§') {
i++;// Skips the data value, the 4 in "§4Alisolarflare"
random = string.charAt(i) == 'k';
} else {
if (!random) // Skip random/obfuscated characters
sanitizedString += string.charAt(i);
}
}
return sanitizedString;
}
/**
* Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode.
*/
@Nullable
public static <T> T perform(IRequest<T> action, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException {
if (DiscordPlugin.SafeMode)
return null;
if (Bukkit.isPrimaryThread()) // TODO: Ignore shutdown message <--
// throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag.");
getLogger().warning("Waiting for a Discord request on the main thread!");
return RequestBuffer.request(action).get(timeout, unit); // Let the pros handle this
}
/**
* Performs Discord actions, retrying when ratelimited. May return null if action fails too many times or in safe mode.
*/
@Nullable
public static <T> T perform(IRequest<T> action) {
if (DiscordPlugin.SafeMode)
return null;
if (Bukkit.isPrimaryThread()) // TODO: Ignore shutdown message <--
// throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag.");
getLogger().warning("Waiting for a Discord request on the main thread!");
return RequestBuffer.request(action).get(); // Let the pros handle this
}
/**
* Performs Discord actions, retrying when ratelimited.
*/
public static Void perform(IVoidRequest action) {
if (DiscordPlugin.SafeMode)
return null;
if (Bukkit.isPrimaryThread())
throw new RuntimeException("Tried to wait for a Discord request on the main thread. This could cause lag.");
return RequestBuffer.request(action).get(); // Let the pros handle this
}
public static void performNoWait(IVoidRequest action) {
if (DiscordPlugin.SafeMode)
return;
RequestBuffer.request(action);
}
public static <T> void performNoWait(IRequest<T> action) {
if (DiscordPlugin.SafeMode)
return;
RequestBuffer.request(action);
}
public static String escape(String message) {
return message.replaceAll("([*_~])", Matcher.quoteReplacement("\\")+"$1");
}
public static Logger getLogger() {
if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger() == null)
return Logger.getLogger("DiscordPlugin");
return DiscordPlugin.plugin.getLogger();
}
public static ConfigData<IChannel> channelData(IHaveConfig config, String key, long defID) {
return config.getDataPrimDef(key, defID, id -> DiscordPlugin.dc.getChannelByID((long) id), IIDLinkedObject::getLongID); //We can afford to search for the channel in the cache once (instead of using mainServer)
}
public static ConfigData<IRole> roleData(IHaveConfig config, String key, String defName) {
return roleData(config, key, defName, DiscordPlugin.mainServer);
}
public static ConfigData<IRole> roleData(IHaveConfig config, String key, String defName, IGuild guild) {
return config.getDataPrimDef(key, defName, name -> {
if (!(name instanceof String)) return null;
val roles = guild.getRolesByName((String) name);
return roles.size() > 0 ? roles.get(0) : null;
}, IIDLinkedObject::getLongID);
}
/**
* Mentions the <b>bot channel</b>. Useful for help texts.
*
* @return The string for mentioning the channel
*/
public static String botmention() {
IChannel channel;
if (DiscordPlugin.plugin == null
|| (channel = DiscordPlugin.plugin.CommandChannel().get()) == null) return "#bot";
return channel.mention();
}
/**
* Disables the component if one of the given configs return null. Useful for channel/role configs.
*
* @param component The component to disable if needed
* @param configs The configs to check for null
* @return Whether the component got disabled and a warning logged
*/
public static boolean disableIfConfigError(@Nullable Component<DiscordPlugin> component, ConfigData<?>... configs) {
for (val config : configs) {
if (config.get() == null) {
String path = null;
try {
if (component != null)
Component.setComponentEnabled(component, false);
val f = ConfigData.class.getDeclaredField("path");
f.setAccessible(true); //Hacking my own plugin
path = (String) f.get(config);
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to disable component after config error!", e);
}
getLogger().warning("The config value " + path + " isn't set correctly " + (component == null ? "in global settings!" : "for component " + component.getClass().getSimpleName() + "!"));
getLogger().warning("Set the correct ID in the config" + (component == null ? "" : " or disable this component") + " to remove this message.");
return true;
}
}
return false;
}
}

View file

@ -1,20 +0,0 @@
package buttondevteam.discordplugin;
import buttondevteam.discordplugin.playerfaker.DiscordFakePlayer;
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
import lombok.Getter;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.UUID;
public class DiscordConnectedPlayer extends DiscordFakePlayer implements IMCPlayer<DiscordConnectedPlayer> {
private static int nextEntityId = 10000;
private @Getter VanillaCommandListener<DiscordConnectedPlayer> vanillaCmdListener;
public DiscordConnectedPlayer(IUser user, IChannel channel, UUID uuid, String mcname) {
super(user, channel, nextEntityId++, uuid, mcname);
vanillaCmdListener = new VanillaCommandListener<>(this);
}
}

View file

@ -1,28 +0,0 @@
package buttondevteam.discordplugin;
import buttondevteam.discordplugin.mcchat.MCChatPrivate;
import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.UserClass;
@UserClass(foldername = "discord")
public class DiscordPlayer extends ChromaGamerBase {
private String did;
// private @Getter @Setter boolean minecraftChatEnabled;
public DiscordPlayer() {
}
public String getDiscordID() {
if (did == null)
did = plugindata.getString(getFolder() + "_id");
return did;
}
/**
* Returns true if player has the private Minecraft chat enabled. For setting the value, see
* {@link MCChatPrivate#privateMCChat(sx.blah.discord.handle.obj.IChannel, boolean, sx.blah.discord.handle.obj.IUser, DiscordPlayer)}
*/
public boolean isMinecraftChatEnabled() {
return MCChatPrivate.isMinecraftChatEnabled(this);
}
}

View file

@ -1,336 +0,0 @@
package buttondevteam.discordplugin;
import buttondevteam.discordplugin.announcer.AnnouncerModule;
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule;
import buttondevteam.discordplugin.commands.*;
import buttondevteam.discordplugin.exceptions.ExceptionListenerModule;
import buttondevteam.discordplugin.fun.FunModule;
import buttondevteam.discordplugin.listeners.CommonListeners;
import buttondevteam.discordplugin.listeners.MCListener;
import buttondevteam.discordplugin.mcchat.MCChatPrivate;
import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import buttondevteam.discordplugin.mccommands.DiscordMCCommandBase;
import buttondevteam.discordplugin.mccommands.ResetMCCommand;
import buttondevteam.discordplugin.role.GameRoleModule;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.chat.TBMCChatAPI;
import buttondevteam.lib.player.ChromaGamerBase;
import com.google.common.io.Files;
import lombok.Getter;
import lombok.val;
import net.milkbowl.vault.permission.Permission;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.scheduler.BukkitTask;
import sx.blah.discord.api.ClientBuilder;
import sx.blah.discord.api.IDiscordClient;
import sx.blah.discord.api.events.IListener;
import sx.blah.discord.api.internal.json.objects.EmbedObject;
import sx.blah.discord.handle.impl.events.ReadyEvent;
import sx.blah.discord.handle.impl.obj.ReactionEmoji;
import sx.blah.discord.handle.obj.*;
import sx.blah.discord.util.EmbedBuilder;
import sx.blah.discord.util.RequestBuffer;
import java.awt.*;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
public class DiscordPlugin extends ButtonPlugin implements IListener<ReadyEvent> {
public static IDiscordClient dc;
public static DiscordPlugin plugin;
public static boolean SafeMode = true;
@Getter
private Command2DC manager;
public ConfigData<Character> Prefix() {
return getIConfig().getData("prefix", '/', str -> ((String) str).charAt(0), Object::toString);
}
public static char getPrefix() {
if (plugin == null) return '/';
return plugin.Prefix().get();
}
public ConfigData<IGuild> MainServer() {
return getIConfig().getDataPrimDef("mainServer", 219529124321034241L, id -> dc.getGuildByID((long) id), IIDLinkedObject::getLongID);
}
public ConfigData<IChannel> CommandChannel() {
return DPUtils.channelData(getIConfig(), "commandChannel", 239519012529111040L);
}
public ConfigData<IRole> ModRole() {
return DPUtils.roleData(getIConfig(), "modRole", "Moderator");
}
@Override
public void pluginEnable() {
try {
getLogger().info("Initializing...");
plugin = this;
manager = new Command2DC();
ClientBuilder cb = new ClientBuilder();
File tokenFile = new File("TBMC", "Token.txt");
if (tokenFile.exists()) //Legacy support
//noinspection UnstableApiUsage
cb.withToken(Files.readFirstLine(tokenFile, StandardCharsets.UTF_8));
else {
File privateFile = new File(getDataFolder(), "private.yml");
val conf = YamlConfiguration.loadConfiguration(privateFile);
String token = conf.getString("token");
if (token == null) {
conf.set("token", "Token goes here");
conf.save(privateFile);
getLogger().severe("Token not found! Set it in private.yml");
Bukkit.getPluginManager().disablePlugin(this);
return;
} else
cb.withToken(token);
}
dc = cb.login();
dc.getDispatcher().registerListener(this);
} catch (Exception e) {
e.printStackTrace();
Bukkit.getPluginManager().disablePlugin(this);
}
}
public static IGuild mainServer;
private static volatile BukkitTask task;
private static volatile boolean sent = false;
@Override
public void handle(ReadyEvent event) {
try {
dc.changePresence(StatusType.DND, ActivityType.PLAYING, "booting");
val tries = new AtomicInteger();
task = Bukkit.getScheduler().runTaskTimerAsynchronously(this, () -> {
tries.incrementAndGet();
if (tries.get() > 10) { //5 seconds
task.cancel();
getLogger().severe("Main server not found! Invite the bot and do /discord reset");
//getIConfig().getConfig().set("mainServer", 219529124321034241L); //Needed because it won't save as long as it's null - made it save
saveConfig(); //Put default there
return;
}
mainServer = MainServer().get(); //Shouldn't change afterwards
if (mainServer == null) {
val guilds = dc.getGuilds();
if (guilds.size() == 0)
return; //If there are no guilds in cache, retry
mainServer = guilds.get(0);
getLogger().warning("Main server set to first one: " + mainServer.getName());
MainServer().set(mainServer); //Save in config
}
if (!TBMCCoreAPI.IsTestServer()) { //Don't change conditions here, see mainServer=devServer=null in onDisable()
dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "Minecraft");
} else {
dc.changePresence(StatusType.ONLINE, ActivityType.PLAYING, "testing");
}
SafeMode = false;
if (task != null)
task.cancel();
if (!sent) {
DPUtils.disableIfConfigError(null, CommandChannel(), ModRole()); //Won't disable, just prints the warning here
Component.registerComponent(this, new GeneralEventBroadcasterModule());
Component.registerComponent(this, new MinecraftChatModule());
Component.registerComponent(this, new ExceptionListenerModule());
Component.registerComponent(this, new GameRoleModule()); //Needs the mainServer to be set
Component.registerComponent(this, new AnnouncerModule());
Component.registerComponent(this, new FunModule());
new ChromaBot(this).updatePlayerList(); //Initialize ChromaBot - The MCCHatModule is tested to be enabled
getManager().registerCommand(new VersionCommand());
getManager().registerCommand(new UserinfoCommand());
getManager().registerCommand(new HelpCommand());
getManager().registerCommand(new DebugCommand());
getManager().registerCommand(new ConnectCommand());
if (ResetMCCommand.resetting) //These will only execute if the chat is enabled
ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.CYAN)
.withTitle("Discord plugin restarted - chat connected.").build(), ChannelconBroadcast.RESTART); //Really important to note the chat, hmm
else if (getConfig().getBoolean("serverup", false)) {
ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.YELLOW)
.withTitle("Server recovered from a crash - chat connected.").build(), ChannelconBroadcast.RESTART);
val thr = new Throwable(
"The server shut down unexpectedly. See the log of the previous run for more details.");
thr.setStackTrace(new StackTraceElement[0]);
TBMCCoreAPI.SendException("The server crashed!", thr);
} else
ChromaBot.getInstance().sendMessageCustomAsWell("", new EmbedBuilder().withColor(Color.GREEN)
.withTitle("Server started - chat connected.").build(), ChannelconBroadcast.RESTART);
ResetMCCommand.resetting = false; //This is the last event handling this flag
getConfig().set("serverup", true);
saveConfig();
sent = true;
if (TBMCCoreAPI.IsTestServer() && !dc.getOurUser().getName().toLowerCase().contains("test")) {
TBMCCoreAPI.SendException(
"Won't load because we're in testing mode and not using a separate account.",
new Exception(
"The plugin refuses to load until you change the token to a testing account. (The account needs to have \"test\" in it's name.)"));
Bukkit.getPluginManager().disablePlugin(this);
}
TBMCCoreAPI.SendUnsentExceptions();
TBMCCoreAPI.SendUnsentDebugMessages();
}
}, 0, 10);
for (IListener<?> listener : CommonListeners.getListeners())
dc.getDispatcher().registerListener(listener);
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(), this);
TBMCChatAPI.AddCommands(this, DiscordMCCommandBase.class);
TBMCCoreAPI.RegisterUserClass(DiscordPlayer.class);
ChromaGamerBase.addConverter(sender -> Optional.ofNullable(sender instanceof DiscordSenderBase
? ((DiscordSenderBase) sender).getChromaUser() : null));
setupProviders();
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while enabling DiscordPlugin!", e);
}
}
/**
* Always true, except when running "stop" from console
*/
public static boolean Restart;
@Override
public void pluginPreDisable() {
if (ChromaBot.getInstance() == null) return; //Failed to load
EmbedObject embed;
if (ResetMCCommand.resetting)
embed = new EmbedBuilder().withColor(Color.ORANGE).withTitle("Discord plugin restarting").build();
else
embed = new EmbedBuilder().withColor(Restart ? Color.ORANGE : Color.RED)
.withTitle(Restart ? "Server restarting" : "Server stopping")
.withDescription(
Bukkit.getOnlinePlayers().size() > 0
? (DPUtils
.sanitizeString(Bukkit.getOnlinePlayers().stream()
.map(Player::getDisplayName).collect(Collectors.joining(", ")))
+ (Bukkit.getOnlinePlayers().size() == 1 ? " was " : " were ")
+ "kicked the hell out.") //TODO: Make configurable
: "") //If 'restart' is disabled then this isn't shown even if joinleave is enabled
.build();
MCChatUtils.forCustomAndAllMCChat(ch -> {
try {
DiscordPlugin.sendMessageToChannelWait(ch, "",
embed, 5, TimeUnit.SECONDS);
} catch (TimeoutException | InterruptedException e) {
e.printStackTrace();
}
}, ChannelconBroadcast.RESTART, false);
ChromaBot.getInstance().updatePlayerList();
}
@Override
public void pluginDisable() {
MCChatPrivate.logoutAll();
getConfig().set("serverup", false);
if (ChromaBot.getInstance() == null) return; //Failed to load
saveConfig();
try {
SafeMode = true; // Stop interacting with Discord
ChromaBot.delete();
dc.changePresence(StatusType.IDLE, ActivityType.PLAYING, "Chromacraft"); //No longer using the same account for testing
dc.logout();
//Configs are emptied so channels and servers are fetched again
sent = false;
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e);
}
}
public static final ReactionEmoji DELIVERED_REACTION = ReactionEmoji.of("");
public static void sendMessageToChannel(IChannel channel, String message) {
sendMessageToChannel(channel, message, null);
}
public static void sendMessageToChannel(IChannel channel, String message, EmbedObject embed) {
try {
sendMessageToChannel(channel, message, embed, false);
} catch (TimeoutException | InterruptedException e) {
e.printStackTrace(); //Shouldn't happen, as we're not waiting on the result
}
}
public static IMessage sendMessageToChannelWait(IChannel channel, String message) throws TimeoutException, InterruptedException {
return sendMessageToChannelWait(channel, message, null);
}
public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed) throws TimeoutException, InterruptedException {
return sendMessageToChannel(channel, message, embed, true);
}
public static IMessage sendMessageToChannelWait(IChannel channel, String message, EmbedObject embed, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException {
return sendMessageToChannel(channel, message, embed, true, timeout, unit);
}
private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait) throws TimeoutException, InterruptedException {
return sendMessageToChannel(channel, message, embed, wait, -1, null);
}
private static IMessage sendMessageToChannel(IChannel channel, String message, EmbedObject embed, boolean wait, long timeout, TimeUnit unit) throws TimeoutException, InterruptedException {
if (message.length() > 1980) {
message = message.substring(0, 1980);
DPUtils.getLogger()
.warning("Message was too long to send to discord and got truncated. In " + channel.getName());
}
try {
MCChatUtils.resetLastMessage(channel); // If this is a chat message, it'll be set again
final String content = message;
RequestBuffer.IRequest<IMessage> r = () -> embed == null ? channel.sendMessage(content)
: channel.sendMessage(content, embed, false);
if (wait) {
if (unit != null)
return DPUtils.perform(r, timeout, unit);
else
return DPUtils.perform(r);
} else {
if (unit != null)
plugin.getLogger().warning("Tried to set timeout for non-waiting call.");
else
DPUtils.performNoWait(r);
return null;
}
} catch (TimeoutException | InterruptedException e) {
throw e;
} catch (Exception e) {
DPUtils.getLogger().warning(
"Failed to deliver message to Discord! Channel: " + channel.getName() + " Message: " + message);
throw new RuntimeException(e);
}
}
public static Permission perms;
public boolean setupProviders() {
try {
Class.forName("net.milkbowl.vault.permission.Permission");
Class.forName("net.milkbowl.vault.chat.Chat");
} catch (ClassNotFoundException e) {
return false;
}
RegisteredServiceProvider<Permission> permsProvider = Bukkit.getServer().getServicesManager()
.getRegistration(Permission.class);
perms = permsProvider.getProvider();
return perms != null;
}
}

View file

@ -1,10 +0,0 @@
package buttondevteam.discordplugin;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.MissingPermissionsException;
import sx.blah.discord.util.RateLimitException;
@FunctionalInterface
public interface DiscordRunnable {
public abstract void run() throws DiscordException, RateLimitException, MissingPermissionsException;
}

View file

@ -1,112 +0,0 @@
package buttondevteam.discordplugin;
import org.bukkit.Bukkit;
import org.bukkit.Server;
import org.bukkit.command.CommandSender;
import org.bukkit.permissions.PermissibleBase;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment;
import org.bukkit.permissions.PermissionAttachmentInfo;
import org.bukkit.plugin.Plugin;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.Set;
public class DiscordSender extends DiscordSenderBase implements CommandSender {
private PermissibleBase perm = new PermissibleBase(this);
private String name;
public DiscordSender(IUser user, IChannel channel) {
super(user, channel);
name = user == null ? "Discord user" : user.getDisplayName(DiscordPlugin.mainServer);
}
public DiscordSender(IUser user, IChannel channel, String name) {
super(user, channel);
this.name = name;
}
@Override
public boolean isPermissionSet(String name) {
return perm.isPermissionSet(name);
}
@Override
public boolean isPermissionSet(Permission perm) {
return this.perm.isPermissionSet(perm);
}
@Override
public boolean hasPermission(String name) {
if (name.contains("essentials") && !name.equals("essentials.list"))
return false;
return perm.hasPermission(name);
}
@Override
public boolean hasPermission(Permission perm) {
return this.perm.hasPermission(perm);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value) {
return perm.addAttachment(plugin, name, value);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin) {
return perm.addAttachment(plugin);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, String name, boolean value, int ticks) {
return perm.addAttachment(plugin, name, value, ticks);
}
@Override
public PermissionAttachment addAttachment(Plugin plugin, int ticks) {
return perm.addAttachment(plugin, ticks);
}
@Override
public void removeAttachment(PermissionAttachment attachment) {
perm.removeAttachment(attachment);
}
@Override
public void recalculatePermissions() {
perm.recalculatePermissions();
}
@Override
public Set<PermissionAttachmentInfo> getEffectivePermissions() {
return perm.getEffectivePermissions();
}
@Override
public boolean isOp() {
return false;
}
@Override
public void setOp(boolean value) {
}
@Override
public Server getServer() {
return Bukkit.getServer();
}
@Override
public String getName() {
return name;
}
@Override
public Spigot spigot() {
return new CommandSender.Spigot();
}
}

View file

@ -1,75 +0,0 @@
package buttondevteam.discordplugin;
import buttondevteam.lib.TBMCCoreAPI;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.scheduler.BukkitTask;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
public abstract class DiscordSenderBase implements CommandSender {
/**
* May be null.
*/
protected IUser user;
protected IChannel channel;
protected DiscordSenderBase(IUser user, IChannel channel) {
this.user = user;
this.channel = channel;
}
private volatile String msgtosend = "";
private volatile BukkitTask sendtask;
/**
* Returns the user. May be null.
*
* @return The user or null.
*/
public IUser getUser() {
return user;
}
public IChannel getChannel() {
return channel;
}
private DiscordPlayer chromaUser;
/**
* Loads the user data on first query.
*
* @return A Chroma user of Discord or a Discord user of Chroma
*/
public DiscordPlayer getChromaUser() {
if (chromaUser == null) chromaUser = DiscordPlayer.getUser(user.getStringID(), DiscordPlayer.class);
return chromaUser;
}
@Override
public void sendMessage(String message) {
try {
final boolean broadcast = new Exception().getStackTrace()[2].getMethodName().contains("broadcast");
//if (broadcast && DiscordPlugin.hooked) - TODO: What should happen if unhooked
if (broadcast)
return;
final String sendmsg = DPUtils.sanitizeString(message);
msgtosend += "\n" + sendmsg;
if (sendtask == null)
sendtask = Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> {
DiscordPlugin.sendMessageToChannel(channel,
(!broadcast && user != null ? user.mention() + "\n" : "") + msgtosend.trim());
sendtask = null;
msgtosend = "";
}, 4); // Waits a 0.2 second to gather all/most of the different messages
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while sending message to DiscordSender", e);
}
}
@Override
public void sendMessage(String[] messages) {
sendMessage(String.join("\n", messages));
}
}

View file

@ -1,11 +0,0 @@
package buttondevteam.discordplugin;
import sx.blah.discord.handle.obj.IDiscordObject;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.MissingPermissionsException;
import sx.blah.discord.util.RateLimitException;
@FunctionalInterface
public interface DiscordSupplier<T extends IDiscordObject<T>> {
public abstract T get() throws DiscordException, RateLimitException, MissingPermissionsException;
}

View file

@ -1,8 +0,0 @@
package buttondevteam.discordplugin;
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
import org.bukkit.entity.Player;
public interface IMCPlayer<T extends DiscordSenderBase & IMCPlayer<T>> extends Player {
VanillaCommandListener<T> getVanillaCmdListener();
}

View file

@ -1,142 +0,0 @@
package buttondevteam.discordplugin.announcer;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.player.ChromaGamerBase;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.val;
import org.bukkit.configuration.file.YamlConfiguration;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IMessage;
import java.io.File;
import java.util.List;
public class AnnouncerModule extends Component<DiscordPlugin> {
public ConfigData<IChannel> channel() {
return DPUtils.channelData(getConfig(), "channel", 239519012529111040L);
}
public ConfigData<IChannel> modChannel() {
return DPUtils.channelData(getConfig(), "modChannel", 239519012529111040L);
}
/**
* Set to 0 or >50 to disable
*/
public ConfigData<Short> keepPinned() {
return getConfig().getData("keepPinned", (short) 40);
}
private ConfigData<Long> lastannouncementtime() {
return getConfig().getData("lastAnnouncementTime", 0L);
}
private ConfigData<Long> lastseentime() {
return getConfig().getData("lastSeenTime", 0L);
}
private static final String SubredditURL = "https://www.reddit.com/r/ChromaGamers";
private static boolean stop = false;
@Override
protected void enable() {
if (DPUtils.disableIfConfigError(this, channel(), modChannel())) return;
stop = false; //If not the first time
DPUtils.performNoWait(() -> {
try {
val keepPinned = keepPinned().get();
if (keepPinned == 0) return;
val channel = channel().get();
List<IMessage> msgs = channel.getPinnedMessages();
for (int i = msgs.size() - 1; i >= keepPinned; i--) { // Unpin all pinned messages except the newest 10
channel.unpin(msgs.get(i));
Thread.sleep(10);
}
} catch (InterruptedException ignore) {
}
});
val yc = YamlConfiguration.loadConfiguration(new File("plugins/DiscordPlugin", "config.yml")); //Name change
if (lastannouncementtime().get() == 0) //Load old data
lastannouncementtime().set(yc.getLong("lastannouncementtime"));
if (lastseentime().get() == 0)
lastseentime().set(yc.getLong("lastseentime"));
new Thread(this::AnnouncementGetterThreadMethod).start();
}
@Override
protected void disable() {
stop = true;
}
private void AnnouncementGetterThreadMethod() {
while (!stop) {
try {
if (!isEnabled()) {
Thread.sleep(10000);
continue;
}
String body = TBMCCoreAPI.DownloadString(SubredditURL + "/new/.json?limit=10");
JsonArray json = new JsonParser().parse(body).getAsJsonObject().get("data").getAsJsonObject()
.get("children").getAsJsonArray();
StringBuilder msgsb = new StringBuilder();
StringBuilder modmsgsb = new StringBuilder();
long lastanntime = lastannouncementtime().get();
for (int i = json.size() - 1; i >= 0; i--) {
JsonObject item = json.get(i).getAsJsonObject();
final JsonObject data = item.get("data").getAsJsonObject();
String author = data.get("author").getAsString();
JsonElement distinguishedjson = data.get("distinguished");
String distinguished;
if (distinguishedjson.isJsonNull())
distinguished = null;
else
distinguished = distinguishedjson.getAsString();
String permalink = "https://www.reddit.com" + data.get("permalink").getAsString();
long date = data.get("created_utc").getAsLong();
if (date > lastseentime().get())
lastseentime().set(date);
else if (date > lastannouncementtime().get()) {
do {
val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit");
if (reddituserclass == null)
break;
val user = ChromaGamerBase.getUser(author, reddituserclass);
String id = user.getConnectedID(DiscordPlayer.class);
if (id != null)
author = "<@" + id + ">";
} while (false);
if (!author.startsWith("<"))
author = "/u/" + author;
(distinguished != null && distinguished.equals("moderator") ? modmsgsb : msgsb)
.append("A new post was submitted to the subreddit by ").append(author).append("\n")
.append(permalink).append("\n");
lastanntime = date;
}
}
if (msgsb.length() > 0)
channel().get().pin(DiscordPlugin.sendMessageToChannelWait(channel().get(), msgsb.toString()));
if (modmsgsb.length() > 0)
DiscordPlugin.sendMessageToChannel(modChannel().get(), modmsgsb.toString());
if (lastannouncementtime().get() != lastanntime) {
lastannouncementtime().set(lastanntime); // If sending succeeded
getPlugin().saveConfig(); //TODO: Won't be needed if I implement auto-saving
}
} catch (Exception e) {
e.printStackTrace();
}
try {
Thread.sleep(10000);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
}

View file

@ -1,35 +0,0 @@
package buttondevteam.discordplugin.broadcaster;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
import lombok.Getter;
public class GeneralEventBroadcasterModule extends Component<DiscordPlugin> {
private static @Getter boolean hooked = false;
@Override
protected void enable() {
try {
PlayerListWatcher.hookUp();
DPUtils.getLogger().info("Finished hooking into the player list");
hooked = true;
} catch (Exception e) {
TBMCCoreAPI.SendException("Error while hacking the player list!", e);
}
}
@Override
protected void disable() {
try {
if (PlayerListWatcher.hookDown())
DPUtils.getLogger().info("Finished unhooking the player list!");
else
DPUtils.getLogger().info("Didn't have the player list hooked.");
hooked = false;
} catch (Exception e) {
TBMCCoreAPI.SendException("Error while hacking the player list!", e);
}
}
}

View file

@ -1,362 +0,0 @@
package buttondevteam.discordplugin.broadcaster;
import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.lib.TBMCCoreAPI;
import com.mojang.authlib.GameProfile;
import lombok.val;
import net.minecraft.server.v1_12_R1.*;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_12_R1.CraftServer;
import org.bukkit.craftbukkit.v1_12_R1.util.CraftChatMessage;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.objenesis.ObjenesisStd;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.UUID;
public class PlayerListWatcher extends DedicatedPlayerList {
private DedicatedPlayerList plist;
public PlayerListWatcher(DedicatedServer minecraftserver) {
super(minecraftserver); // <-- Does some init stuff and calls Bukkit.setServer() so we have to use Objenesis
}
public void sendAll(Packet<?> packet) {
plist.sendAll(packet);
try { // Some messages get sent by directly constructing a packet
if (packet instanceof PacketPlayOutChat) {
Field msgf = PacketPlayOutChat.class.getDeclaredField("a");
msgf.setAccessible(true);
MCChatUtils.forAllMCChat(MCChatUtils.send(((IChatBaseComponent) msgf.get(packet)).toPlainText()));
}
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to broadcast message sent to all players - hacking failed.", e);
}
}
@Override
public void sendMessage(IChatBaseComponent ichatbasecomponent, boolean flag) { // Needed so it calls the overriden method
plist.getServer().sendMessage(ichatbasecomponent);
ChatMessageType chatmessagetype = flag ? ChatMessageType.SYSTEM : ChatMessageType.CHAT;
// CraftBukkit start - we run this through our processor first so we can get web links etc
this.sendAll(new PacketPlayOutChat(CraftChatMessage.fixComponent(ichatbasecomponent), chatmessagetype));
// CraftBukkit end
}
@Override
public void sendMessage(IChatBaseComponent ichatbasecomponent) { // Needed so it calls the overriden method
this.sendMessage(ichatbasecomponent, true);
}
@Override
public void sendMessage(IChatBaseComponent[] iChatBaseComponents) { // Needed so it calls the overridden method
for (IChatBaseComponent component : iChatBaseComponents) {
sendMessage(component, true);
}
}
static void hookUp() throws Exception {
Field conf = CraftServer.class.getDeclaredField("console");
conf.setAccessible(true);
val server = (MinecraftServer) conf.get(Bukkit.getServer());
val plw = new ObjenesisStd().newInstance(PlayerListWatcher.class); // Cannot call super constructor
plw.plist = (DedicatedPlayerList) server.getPlayerList();
plw.maxPlayers = plw.plist.getMaxPlayers();
Field plf = plw.getClass().getField("players");
plf.setAccessible(true);
Field modf = plf.getClass().getDeclaredField("modifiers");
modf.setAccessible(true);
modf.set(plf, plf.getModifiers() & ~Modifier.FINAL);
plf.set(plw, plw.plist.players);
server.a(plw);
Field pllf = CraftServer.class.getDeclaredField("playerList");
pllf.setAccessible(true);
pllf.set(Bukkit.getServer(), plw);
}
static boolean hookDown() throws Exception {
Field conf = CraftServer.class.getDeclaredField("console");
conf.setAccessible(true);
val server = (MinecraftServer) conf.get(Bukkit.getServer());
val plist = (DedicatedPlayerList) server.getPlayerList();
if (!(plist instanceof PlayerListWatcher))
return false;
server.a(((PlayerListWatcher) plist).plist);
Field pllf = CraftServer.class.getDeclaredField("playerList");
pllf.setAccessible(true);
pllf.set(Bukkit.getServer(), ((PlayerListWatcher) plist).plist);
return true;
}
public void a(EntityHuman entityhuman, IChatBaseComponent ichatbasecomponent) {
plist.a(entityhuman, ichatbasecomponent);
}
public void a(EntityPlayer entityplayer, int i) {
plist.a(entityplayer, i);
}
public void a(EntityPlayer entityplayer, WorldServer worldserver) {
plist.a(entityplayer, worldserver);
}
public NBTTagCompound a(EntityPlayer entityplayer) {
return plist.a(entityplayer);
}
public void a(int i) {
plist.a(i);
}
public void a(NetworkManager networkmanager, EntityPlayer entityplayer) {
plist.a(networkmanager, entityplayer);
}
public void a(Packet<?> packet, int i) {
plist.a(packet, i);
}
public EntityPlayer a(UUID uuid) {
return plist.a(uuid);
}
public void addOp(GameProfile gameprofile) {
plist.addOp(gameprofile);
}
public void addWhitelist(GameProfile gameprofile) {
plist.addWhitelist(gameprofile);
}
public EntityPlayer attemptLogin(LoginListener loginlistener, GameProfile gameprofile, String hostname) {
return plist.attemptLogin(loginlistener, gameprofile, hostname);
}
public String b(boolean flag) {
return plist.b(flag);
}
public void b(EntityHuman entityhuman, IChatBaseComponent ichatbasecomponent) {
plist.b(entityhuman, ichatbasecomponent);
}
public void b(EntityPlayer entityplayer, WorldServer worldserver) {
plist.b(entityplayer, worldserver);
}
public List<EntityPlayer> b(String s) {
return plist.b(s);
}
public Location calculateTarget(Location enter, World target) {
return plist.calculateTarget(enter, target);
}
public void changeDimension(EntityPlayer entityplayer, int i, TeleportCause cause) {
plist.changeDimension(entityplayer, i, cause);
}
public void changeWorld(Entity entity, int i, WorldServer worldserver, WorldServer worldserver1) {
plist.changeWorld(entity, i, worldserver, worldserver1);
}
public int d() {
return plist.d();
}
public void d(EntityPlayer entityplayer) {
plist.d(entityplayer);
}
public String disconnect(EntityPlayer entityplayer) {
return plist.disconnect(entityplayer);
}
public boolean equals(Object obj) {
return plist.equals(obj);
}
public String[] f() {
return plist.f();
}
public void f(EntityPlayer entityplayer) {
plist.f(entityplayer);
}
public boolean f(GameProfile gameprofile) {
return plist.f(gameprofile);
}
public GameProfile[] g() {
return plist.g();
}
public boolean getHasWhitelist() {
return plist.getHasWhitelist();
}
public IpBanList getIPBans() {
return plist.getIPBans();
}
public int getMaxPlayers() {
return plist.getMaxPlayers();
}
public OpList getOPs() {
return plist.getOPs();
}
public EntityPlayer getPlayer(String s) {
return plist.getPlayer(s);
}
public int getPlayerCount() {
return plist.getPlayerCount();
}
public GameProfileBanList getProfileBans() {
return plist.getProfileBans();
}
public String[] getSeenPlayers() {
return plist.getSeenPlayers();
}
public DedicatedServer getServer() {
return plist.getServer();
}
public WhiteList getWhitelist() {
return plist.getWhitelist();
}
public String[] getWhitelisted() {
return plist.getWhitelisted();
}
public AdvancementDataPlayer h(EntityPlayer entityplayer) {
return plist.h(entityplayer);
}
public int hashCode() {
return plist.hashCode();
}
public boolean isOp(GameProfile gameprofile) {
return plist.isOp(gameprofile);
}
public boolean isWhitelisted(GameProfile gameprofile) {
return plist.isWhitelisted(gameprofile);
}
public EntityPlayer moveToWorld(EntityPlayer entityplayer, int i, boolean flag, Location location,
boolean avoidSuffocation) {
return plist.moveToWorld(entityplayer, i, flag, location, avoidSuffocation);
}
public EntityPlayer moveToWorld(EntityPlayer entityplayer, int i, boolean flag) {
return plist.moveToWorld(entityplayer, i, flag);
}
public String[] n() {
return plist.n();
}
public void onPlayerJoin(EntityPlayer entityplayer, String joinMessage) {
plist.onPlayerJoin(entityplayer, joinMessage);
}
public EntityPlayer processLogin(GameProfile gameprofile, EntityPlayer player) {
return plist.processLogin(gameprofile, player);
}
public void reload() {
plist.reload();
}
public void reloadWhitelist() {
plist.reloadWhitelist();
}
public void removeOp(GameProfile gameprofile) {
plist.removeOp(gameprofile);
}
public void removeWhitelist(GameProfile gameprofile) {
plist.removeWhitelist(gameprofile);
}
public void repositionEntity(Entity entity, Location exit, boolean portal) {
plist.repositionEntity(entity, exit, portal);
}
public int s() {
return plist.s();
}
public void savePlayers() {
plist.savePlayers();
}
@SuppressWarnings("rawtypes")
public void sendAll(Packet packet, EntityHuman entityhuman) {
plist.sendAll(packet, entityhuman);
}
@SuppressWarnings("rawtypes")
public void sendAll(Packet packet, World world) {
plist.sendAll(packet, world);
}
public void sendPacketNearby(EntityHuman entityhuman, double d0, double d1, double d2, double d3, int i,
Packet<?> packet) {
plist.sendPacketNearby(entityhuman, d0, d1, d2, d3, i, packet);
}
public void sendScoreboard(ScoreboardServer scoreboardserver, EntityPlayer entityplayer) {
plist.sendScoreboard(scoreboardserver, entityplayer);
}
public void setHasWhitelist(boolean flag) {
plist.setHasWhitelist(flag);
}
public void setPlayerFileData(WorldServer[] aworldserver) {
plist.setPlayerFileData(aworldserver);
}
public NBTTagCompound t() {
return plist.t();
}
public void tick() {
plist.tick();
}
public String toString() {
return plist.toString();
}
public void u() {
plist.u();
}
public void updateClient(EntityPlayer entityplayer) {
plist.updateClient(entityplayer);
}
public List<EntityPlayer> v() {
return plist.v();
}
public ServerStatisticManager getStatisticManager(EntityPlayer entityhuman) {
return plist.getStatisticManager(entityhuman);
}
}

View file

@ -1,17 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.chat.Command2;
public class Command2DC extends Command2<ICommand2DC, Command2DCSender> {
@Override
public void registerCommand(ICommand2DC command) {
super.registerCommand(command, DiscordPlugin.getPrefix()); //Needs to be configurable for the helps
}
@Override
public boolean hasPermission(Command2DCSender sender, ICommand2DC command) {
//return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.ModRole().get()); //TODO: ModRole may be null; more customisable way?
return true;
}
}

View file

@ -1,25 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.lib.chat.Command2Sender;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import sx.blah.discord.handle.obj.IMessage;
@RequiredArgsConstructor
public class Command2DCSender implements Command2Sender {
private final @Getter IMessage message;
@Override
public void sendMessage(String message) {
if (message.length() == 0) return;
message = DPUtils.sanitizeString(message);
message = Character.toLowerCase(message.charAt(0)) + message.substring(1);
this.message.reply(message);
}
@Override
public void sendMessage(String[] message) {
sendMessage(String.join("\n", message));
}
}

View file

@ -1,62 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.TBMCPlayer;
import buttondevteam.lib.player.TBMCPlayerBase;
import com.google.common.collect.HashBiMap;
import lombok.val;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
@CommandClass(helpText = {
"Connect command", //
"This command lets you connect your account with a Minecraft account. This allows using the Minecraft chat and other things.", //
})
public class ConnectCommand extends ICommand2DC {
/**
* Key: Minecraft name<br>
* Value: Discord ID
*/
public static HashBiMap<String, String> WaitingToConnect = HashBiMap.create();
@Command2.Subcommand
public boolean def(Command2DCSender sender, String Minecraftname) {
val message = sender.getMessage();
if (WaitingToConnect.inverse().containsKey(message.getAuthor().getStringID())) {
DiscordPlugin.sendMessageToChannel(message.getChannel(),
"Replacing " + WaitingToConnect.inverse().get(message.getAuthor().getStringID()) + " with " + Minecraftname);
WaitingToConnect.inverse().remove(message.getAuthor().getStringID());
}
@SuppressWarnings("deprecation")
OfflinePlayer p = Bukkit.getOfflinePlayer(Minecraftname);
if (p == null) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), "The specified Minecraft player cannot be found");
return true;
}
try (TBMCPlayer pl = TBMCPlayerBase.getPlayer(p.getUniqueId(), TBMCPlayer.class)) {
DiscordPlayer dp = pl.getAs(DiscordPlayer.class);
if (dp != null && message.getAuthor().getStringID().equals(dp.getDiscordID())) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), "You already have this account connected.");
return true;
}
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while connecting a Discord account!", e);
DiscordPlugin.sendMessageToChannel(message.getChannel(), "An internal error occured!\n" + e);
}
WaitingToConnect.put(p.getName(), message.getAuthor().getStringID());
DiscordPlugin.sendMessageToChannel(message.getChannel(),
"Alright! Now accept the connection in Minecraft from the account " + Minecraftname
+ " before the next server restart. You can also adjust the Minecraft name you want to connect to with the same command.");
if (p.isOnline())
((Player) p).sendMessage("§bTo connect with the Discord account " + message.getAuthor().getName() + "#"
+ message.getAuthor().getDiscriminator() + " do /discord accept");
return true;
}
}

View file

@ -1,20 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.listeners.CommonListeners;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
@CommandClass(helpText = {
"Switches debug mode."
})
public class DebugCommand extends ICommand2DC {
@Command2.Subcommand
public boolean def(Command2DCSender sender, String args) {
if (sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.ModRole().get()))
sender.sendMessage("debug " + (CommonListeners.debug() ? "enabled" : "disabled"));
else
sender.sendMessage("you need to be a moderator to use this command.");
return true;
}
}

View file

@ -1,18 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.lib.chat.CommandClass;
@CommandClass(helpText = {
"Help command", //
"Shows some info about a command or lists the available commands.", //
})
public class HelpCommand extends ICommand2DC {
@Override
public boolean def(Command2DCSender sender, String args) {
if (args.length() == 0)
sender.sendMessage(getManager().getCommandsText());
else
sender.sendMessage("Soon:tm:"); //TODO
return true;
}
}

View file

@ -1,20 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.ICommand2;
import lombok.Getter;
import lombok.val;
public abstract class ICommand2DC extends ICommand2<Command2DCSender> {
public <T extends ICommand2> ICommand2DC() {
super(DiscordPlugin.plugin.getManager());
val ann = getClass().getAnnotation(CommandClass.class);
if (ann == null)
modOnly = false;
else
modOnly = ann.modOnly();
}
private final @Getter boolean modOnly;
}

View file

@ -1,91 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.ChromaGamerBase.InfoTarget;
import lombok.val;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IUser;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@CommandClass(helpText = {
"User information", //
"Shows some information about users, from Discord, from Minecraft or from Reddit if they have these accounts connected.", //
"If used without args, shows your info.", //
})
public class UserinfoCommand extends ICommand2DC {
@Command2.Subcommand
public boolean def(Command2DCSender sender, @Command2.OptionalArg @Command2.TextArg String user) {
val message = sender.getMessage();
IUser target = null;
if (user == null || user.length() == 0)
target = message.getAuthor();
else {
final Optional<IUser> firstmention = message.getMentions().stream()
.filter(m -> !m.getStringID().equals(DiscordPlugin.dc.getOurUser().getStringID())).findFirst();
if (firstmention.isPresent())
target = firstmention.get();
else if (user.contains("#")) {
String[] targettag = user.split("#");
final List<IUser> targets = getUsers(message, targettag[0]);
if (targets.size() == 0) {
DiscordPlugin.sendMessageToChannel(message.getChannel(),
"The user cannot be found (by name): " + user);
return true;
}
for (IUser ptarget : targets) {
if (ptarget.getDiscriminator().equalsIgnoreCase(targettag[1])) {
target = ptarget;
break;
}
}
if (target == null) {
DiscordPlugin.sendMessageToChannel(message.getChannel(),
"The user cannot be found (by discriminator): " + user + "(Found " + targets.size()
+ " users with the name.)");
return true;
}
} else {
final List<IUser> targets = getUsers(message, user);
if (targets.size() == 0) {
DiscordPlugin.sendMessageToChannel(message.getChannel(),
"The user cannot be found on Discord: " + user);
return true;
}
if (targets.size() > 1) {
DiscordPlugin.sendMessageToChannel(message.getChannel(),
"Multiple users found with that (nick)name. Please specify the whole tag, like ChromaBot#6338 or use a ping.");
return true;
}
target = targets.get(0);
}
}
try (DiscordPlayer dp = ChromaGamerBase.getUser(target.getStringID(), DiscordPlayer.class)) {
StringBuilder uinfo = new StringBuilder("User info for ").append(target.getName()).append(":\n");
uinfo.append(dp.getInfo(InfoTarget.Discord));
DiscordPlugin.sendMessageToChannel(message.getChannel(), uinfo.toString());
} catch (Exception e) {
DiscordPlugin.sendMessageToChannel(message.getChannel(), "An error occured while getting the user!");
TBMCCoreAPI.SendException("Error while getting info about " + target.getName() + "!", e);
}
return true;
}
private List<IUser> getUsers(IMessage message, String args) {
final List<IUser> targets;
if (message.getChannel().isPrivate())
targets = DiscordPlugin.dc.getUsers().stream().filter(u -> u.getName().equalsIgnoreCase(args))
.collect(Collectors.toList());
else
targets = message.getGuild().getUsersByName(args, true);
return targets;
}
}

View file

@ -1,26 +0,0 @@
package buttondevteam.discordplugin.commands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import lombok.val;
@CommandClass(helpText = {
"Version",
"Returns the plugin's version"
})
public class VersionCommand extends ICommand2DC {
@Command2.Subcommand
public boolean def(Command2DCSender sender) {
sender.sendMessage(getVersion());
return true;
}
public static String[] getVersion() {
val desc = DiscordPlugin.plugin.getDescription();
return new String[]{ //
desc.getFullName(), //
desc.getWebsite() //
};
}
}

View file

@ -1,32 +0,0 @@
package buttondevteam.discordplugin.exceptions;
import buttondevteam.core.ComponentManager;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCDebugMessageEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public class DebugMessageListener implements Listener{
@EventHandler
public void onDebugMessage(TBMCDebugMessageEvent e) {
SendMessage(e.getDebugMessage());
e.setSent();
}
private static void SendMessage(String message) {
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(ExceptionListenerModule.class))
return;
try {
StringBuilder sb = new StringBuilder();
sb.append("```").append("\n");
if (message.length() > 2000)
message = message.substring(0, 2000);
sb.append(message).append("\n");
sb.append("```");
DiscordPlugin.sendMessageToChannel(ExceptionListenerModule.getChannel(), sb.toString());
} catch (Exception ex) {
ex.printStackTrace();
}
}
}

View file

@ -1,97 +0,0 @@
package buttondevteam.discordplugin.exceptions;
import buttondevteam.core.ComponentManager;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCExceptionEvent;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IGuild;
import sx.blah.discord.handle.obj.IRole;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ExceptionListenerModule extends Component<DiscordPlugin> implements Listener {
private List<Throwable> lastthrown = new ArrayList<>();
private List<String> lastsourcemsg = new ArrayList<>();
@EventHandler
public void onException(TBMCExceptionEvent e) {
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass()))
return;
if (lastthrown.stream()
.anyMatch(ex -> Arrays.equals(e.getException().getStackTrace(), ex.getStackTrace())
&& (e.getException().getMessage() == null ? ex.getMessage() == null
: e.getException().getMessage().equals(ex.getMessage()))) // e.Exception.Message==ex.Message
&& lastsourcemsg.contains(e.getSourceMessage()))
return;
SendException(e.getException(), e.getSourceMessage());
if (lastthrown.size() >= 10)
lastthrown.remove(0);
if (lastsourcemsg.size() >= 10)
lastsourcemsg.remove(0);
lastthrown.add(e.getException());
lastsourcemsg.add(e.getSourceMessage());
e.setHandled();
}
private static void SendException(Throwable e, String sourcemessage) {
if (instance == null) return;
try {
IChannel channel = getChannel();
assert channel != null;
IRole coderRole = instance.pingRole(channel.getGuild()).get();
StringBuilder sb = TBMCCoreAPI.IsTestServer() ? new StringBuilder()
: new StringBuilder(coderRole == null ? "" : coderRole.mention()).append("\n");
sb.append(sourcemessage).append("\n");
sb.append("```").append("\n");
String stackTrace = Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n"))
.filter(s -> !s.contains("\tat ") || s.contains("\tat buttondevteam."))
.collect(Collectors.joining("\n"));
if (stackTrace.length() > 1800)
stackTrace = stackTrace.substring(0, 1800);
sb.append(stackTrace).append("\n");
sb.append("```");
DiscordPlugin.sendMessageToChannel(channel, sb.toString()); //Instance isn't null here
} catch (Exception ex) {
ex.printStackTrace();
}
}
private static ExceptionListenerModule instance;
public static IChannel getChannel() {
if (instance != null) return instance.channel().get();
return null;
}
private ConfigData<IChannel> channel() {
return DPUtils.channelData(getConfig(), "channel", 239519012529111040L);
}
private ConfigData<IRole> pingRole(IGuild guild) {
return DPUtils.roleData(getConfig(), "pingRole", "Coder", guild);
}
@Override
protected void enable() {
if (DPUtils.disableIfConfigError(this, channel())) return;
instance = this;
Bukkit.getPluginManager().registerEvents(new ExceptionListenerModule(), getPlugin());
TBMCCoreAPI.RegisterEventsForExceptions(new DebugMessageListener(), getPlugin());
}
@Override
protected void disable() {
instance = null;
}
}

View file

@ -1,149 +0,0 @@
package buttondevteam.discordplugin.fun;
import buttondevteam.core.ComponentManager;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import com.google.common.collect.Lists;
import lombok.val;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent;
import sx.blah.discord.handle.obj.*;
import sx.blah.discord.util.EmbedBuilder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class FunModule extends Component<DiscordPlugin> implements Listener {
private static final String[] serverReadyStrings = new String[]{"In one week from now", // Ali
"Between now and the heat-death of the universe.", // Ghostise
"Soon™", "Ask again this time next month", // Ghostise
"In about 3 seconds", // Nicolai
"After we finish 8 plugins", // Ali
"Tomorrow.", // Ali
"After one tiiiny feature", // Ali
"Next commit", // Ali
"After we finish strangling Towny", // Ali
"When we kill every *fucking* bug", // Ali
"Once the server stops screaming.", // Ali
"After HL3 comes out", // Ali
"Next time you ask", // Ali
"When will *you* be open?" // Ali
};
private ConfigData<Boolean> serverReady() {
return getConfig().getData("serverReady", true);
}
private ConfigData<ArrayList<String>> serverReadyAnswers() {
return getConfig().getData("serverReadyAnswers", () -> Lists.newArrayList(serverReadyStrings)); //TODO: Test
}
private static final String[] serverReadyQuestions = new String[]{"when will the server be open",
"when will the server be ready", "when will the server be done", "when will the server be complete",
"when will the server be finished", "when's the server ready", "when's the server open",
"Vhen vill ze server be open?"};
private static final Random serverReadyRandom = new Random();
private static final ArrayList<Short> usableServerReadyStrings = new ArrayList<>(0);
private void createUsableServerReadyStrings() {
IntStream.range(0, serverReadyAnswers().get().size())
.forEach(i -> FunModule.usableServerReadyStrings.add((short) i));
}
@Override
protected void enable() {
registerListener(this);
}
@Override
protected void disable() {
lastlist = lastlistp = ListC = 0;
}
private static short lastlist = 0;
private static short lastlistp = 0;
private static short ListC = 0;
public static boolean executeMemes(IMessage message) {
val fm = ComponentManager.getIfEnabled(FunModule.class);
if (fm == null) return false;
String msglowercased = message.getContent().toLowerCase();
lastlist++;
if (lastlist > 5) {
ListC = 0;
lastlist = 0;
}
if (msglowercased.equals("list") && Bukkit.getOnlinePlayers().size() == lastlistp && ListC++ > 2) // Lowered already
{
message.reply("Stop it. You know the answer.");
lastlist = 0;
lastlistp = (short) Bukkit.getOnlinePlayers().size();
return true; //Handled
}
lastlistp = (short) Bukkit.getOnlinePlayers().size(); //Didn't handle
if (fm.serverReady().get()) {
if (!TBMCCoreAPI.IsTestServer()
&& Arrays.stream(serverReadyQuestions).anyMatch(msglowercased::contains)) {
int next;
if (usableServerReadyStrings.size() == 0)
fm.createUsableServerReadyStrings();
next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size()));
DiscordPlugin.sendMessageToChannel(message.getChannel(), serverReadyStrings[next]);
return false; //Still process it as a command/mcchat if needed
}
}
return false;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
ListC = 0;
}
private ConfigData<IRole> fullHouseDevRole(IGuild guild) {
return DPUtils.roleData(getConfig(), "fullHouseDevRole", "Developer", guild);
}
private ConfigData<IChannel> fullHouseChannel() {
return DPUtils.channelData(getConfig(), "fullHouseChannel", 219626707458457603L);
}
private static long lasttime = 0;
public static void handleFullHouse(PresenceUpdateEvent event) {
val fm = ComponentManager.getIfEnabled(FunModule.class);
if (fm == null) return;
val channel = fm.fullHouseChannel().get();
if (channel == null) return;
val devrole = fm.fullHouseDevRole(channel.getGuild()).get();
if (devrole == null) return;
if (event.getOldPresence().getStatus().equals(StatusType.OFFLINE)
&& !event.getNewPresence().getStatus().equals(StatusType.OFFLINE)
&& event.getUser().getRolesForGuild(channel.getGuild()).stream()
.anyMatch(r -> r.getLongID() == devrole.getLongID())
&& channel.getGuild().getUsersByRole(devrole).stream()
.noneMatch(u -> u.getPresence().getStatus().equals(StatusType.OFFLINE))
&& lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime())
&& Calendar.getInstance().get(Calendar.DAY_OF_MONTH) % 5 == 0) {
DiscordPlugin.sendMessageToChannel(channel, "Full house!",
new EmbedBuilder()
.withImage(
"https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png")
.build());
lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime());
}
}
}

View file

@ -1,73 +0,0 @@
package buttondevteam.discordplugin.listeners;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.Command2DCSender;
import buttondevteam.lib.TBMCCoreAPI;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IRole;
public class CommandListener {
/**
* Runs a ChromaBot command. If mentionedonly is false, it will only execute the command if it was in #bot with the correct prefix or in private.
*
* @param message The Discord message
* @param mentionedonly Only run the command if ChromaBot is mentioned at the start of the message
* @return Whether it ran the command
*/
public static boolean runCommand(IMessage message, boolean mentionedonly) {
if (message.getContent().length() == 0)
return false; //Pin messages and such, let the mcchat listener deal with it
final IChannel channel = message.getChannel();
if (!mentionedonly) { //mentionedonly conditions are in CommonListeners
if (!message.getChannel().isPrivate()
&& !(message.getContent().charAt(0) == DiscordPlugin.getPrefix()
&& channel.getStringID().equals(DiscordPlugin.plugin.CommandChannel().get().getStringID()))) //
return false;
message.getChannel().setTypingStatus(true); // Fun
}
final StringBuilder cmdwithargs = new StringBuilder(message.getContent());
final String mention = DiscordPlugin.dc.getOurUser().mention(false);
final String mentionNick = DiscordPlugin.dc.getOurUser().mention(true);
boolean gotmention = checkanddeletemention(cmdwithargs, mention, message);
gotmention = checkanddeletemention(cmdwithargs, mentionNick, message) || gotmention;
for (String mentionRole : (Iterable<String>) message.getRoleMentions().stream().filter(r -> DiscordPlugin.dc.getOurUser().hasRole(r)).map(IRole::mention)::iterator)
gotmention = checkanddeletemention(cmdwithargs, mentionRole, message) || gotmention; // Delete all mentions
if (mentionedonly && !gotmention) {
message.getChannel().setTypingStatus(false);
return false;
}
message.getChannel().setTypingStatus(true);
String cmdwithargsString = cmdwithargs.toString();
try {
if (!DiscordPlugin.plugin.getManager().handleCommand(new Command2DCSender(message), cmdwithargsString))
message.reply("Unknown command. Do " + DiscordPlugin.getPrefix() + "help for help.\n" + cmdwithargsString);
} catch (Exception e) {
TBMCCoreAPI.SendException("Failed to process Discord command: " + cmdwithargsString, e);
}
message.getChannel().setTypingStatus(false);
return true;
}
private static boolean checkanddeletemention(StringBuilder cmdwithargs, String mention, IMessage message) {
if (message.getContent().startsWith(mention)) // TODO: Resolve mentions: Compound arguments, either a mention or text
if (cmdwithargs.length() > mention.length() + 1) {
int i = cmdwithargs.indexOf(" ", mention.length());
if (i == -1)
i = mention.length();
else
//noinspection StatementWithEmptyBody
for (; i < cmdwithargs.length() && cmdwithargs.charAt(i) == ' '; i++)
; //Removes any space before the command
cmdwithargs.delete(0, i);
cmdwithargs.insert(0, DiscordPlugin.getPrefix()); //Always use the prefix for processing
} else
cmdwithargs.replace(0, cmdwithargs.length(), DiscordPlugin.getPrefix() + "help");
else {
return false; //Don't treat / as mention, mentions can be used in public mcchat
}
if (cmdwithargs.length() == 0)
cmdwithargs.replace(0, cmdwithargs.length(), DiscordPlugin.getPrefix() + "help");
return true;
}
}

View file

@ -1,77 +0,0 @@
package buttondevteam.discordplugin.listeners;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.fun.FunModule;
import buttondevteam.discordplugin.mcchat.MinecraftChatModule;
import buttondevteam.discordplugin.role.GameRoleModule;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.Component;
import lombok.val;
import sx.blah.discord.api.events.IListener;
import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent;
import sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent;
public class CommonListeners {
/*
MentionEvent:
- CommandListener (starts with mention, only 'channelcon' and not in #bot)
MessageReceivedEvent:
- v CommandListener (starts with mention, in #bot or a connected chat)
- Minecraft chat (is enabled in the channel and message isn't [/]mcchat)
- CommandListener (with the correct prefix in #bot, or in private)
*/
public static IListener<?>[] getListeners() {
return new IListener[]{new IListener<MessageReceivedEvent>() {
@Override
public void handle(MessageReceivedEvent event) {
if (DiscordPlugin.SafeMode)
return;
if (event.getMessage().getAuthor().isBot())
return;
if (FunModule.executeMemes(event.getMessage()))
return;
try {
boolean handled = false;
val commandChannel = DiscordPlugin.plugin.CommandChannel().get();
if ((commandChannel != null && event.getChannel().getLongID() == commandChannel.getLongID()) //If mentioned, that's higher than chat
|| event.getMessage().getContent().contains("channelcon")) //Only 'channelcon' is allowed in other channels
handled = CommandListener.runCommand(event.getMessage(), true); //#bot is handled here
if (handled) return;
val mcchat = Component.getComponents().get(MinecraftChatModule.class);
if (mcchat != null && mcchat.isEnabled()) //ComponentManager.isEnabled() searches the component again
handled = ((MinecraftChatModule) mcchat).getListener().handleDiscord(event); //Also runs Discord commands in chat channels
if (!handled)
handled = CommandListener.runCommand(event.getMessage(), false);
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while handling a message!", e);
}
}
}, new IListener<sx.blah.discord.handle.impl.events.user.PresenceUpdateEvent>() {
@Override
public void handle(PresenceUpdateEvent event) {
if (DiscordPlugin.SafeMode)
return;
FunModule.handleFullHouse(event);
}
}, (IListener<RoleCreateEvent>) GameRoleModule::handleRoleEvent, //
(IListener<RoleDeleteEvent>) GameRoleModule::handleRoleEvent, //
(IListener<RoleUpdateEvent>) GameRoleModule::handleRoleEvent};
}
private static boolean debug = false;
public static void debug(String debug) {
if (CommonListeners.debug) //Debug
DPUtils.getLogger().info(debug);
}
public static boolean debug() {
return debug = !debug;
}
}

View file

@ -1,4 +0,0 @@
package buttondevteam.discordplugin.listeners;
public interface DiscordListener {
}

View file

@ -1,43 +0,0 @@
package buttondevteam.discordplugin.listeners;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.ConnectCommand;
import buttondevteam.lib.player.TBMCPlayerGetInfoEvent;
import buttondevteam.lib.player.TBMCPlayerJoinEvent;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.server.ServerCommandEvent;
import sx.blah.discord.handle.obj.IUser;
public class MCListener implements Listener {
@EventHandler
public void onPlayerJoin(TBMCPlayerJoinEvent e) {
if (ConnectCommand.WaitingToConnect.containsKey(e.GetPlayer().PlayerName().get())) {
@SuppressWarnings("ConstantConditions") IUser user = DiscordPlugin.dc
.getUserByID(Long.parseLong(ConnectCommand.WaitingToConnect.get(e.GetPlayer().PlayerName().get())));
e.getPlayer().sendMessage("§bTo connect with the Discord account @" + user.getName() + "#" + user.getDiscriminator()
+ " do /discord accept");
e.getPlayer().sendMessage("§bIf it wasn't you, do /discord decline");
}
}
@EventHandler
public void onGetInfo(TBMCPlayerGetInfoEvent e) {
if (DiscordPlugin.SafeMode)
return;
DiscordPlayer dp = e.getPlayer().getAs(DiscordPlayer.class);
if (dp == null || dp.getDiscordID() == null || dp.getDiscordID().equals(""))
return;
IUser user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID()));
e.addInfo("Discord tag: " + user.getName() + "#" + user.getDiscriminator());
e.addInfo(user.getPresence().getStatus().toString());
if (user.getPresence().getActivity().isPresent() && user.getPresence().getText().isPresent())
e.addInfo(user.getPresence().getActivity().get() + ": " + user.getPresence().getText().get());
}
@EventHandler
public void onServerCommand(ServerCommandEvent e) {
DiscordPlugin.Restart = !e.getCommand().equalsIgnoreCase("stop"); // The variable is always true except if stopped
}
}

View file

@ -1,150 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.component.channel.Channel;
import buttondevteam.core.component.channel.ChatRoom;
import buttondevteam.discordplugin.*;
import buttondevteam.discordplugin.commands.Command2DCSender;
import buttondevteam.discordplugin.commands.ICommand2DC;
import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.TBMCPlayer;
import lombok.val;
import org.bukkit.Bukkit;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.Permissions;
import sx.blah.discord.util.PermissionUtils;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@CommandClass(helpText = {"Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
"You need to have access to the MC channel and have manage permissions on the Discord channel.", //
"You also need to have your Minecraft account connected. In #bot use /connect <mcname>.", //
"Call this command from the channel you want to use.", //
"Usage: @Bot channelcon <mcchannel>", //
"Use the ID (command) of the channel, for example `g` for the global chat.", //
"To remove a connection use @ChromaBot channelcon remove in the channel.", //
"Mentioning the bot is needed in this case because the / prefix only works in #bot.", //
"Invite link: <https://discordapp.com/oauth2/authorize?client_id=226443037893591041&scope=bot&permissions=268509264>" //
})
public class ChannelconCommand extends ICommand2DC {
@Command2.Subcommand
public boolean remove(Command2DCSender sender) {
val message = sender.getMessage();
if (checkPerms(message)) return true;
if (MCChatCustom.removeCustomChat(message.getChannel()))
message.reply("channel connection removed.");
else
message.reply("this channel isn't connected.");
return true;
}
@Command2.Subcommand
public boolean toggle(Command2DCSender sender, @Command2.OptionalArg String toggle) {
val message = sender.getMessage();
if (checkPerms(message)) return true;
val cc = MCChatCustom.getCustomChat(message.getChannel());
if (cc == null)
return respond(sender, "this channel isn't connected.");
Supplier<String> togglesString = () -> Arrays.stream(ChannelconBroadcast.values()).map(t -> t.toString().toLowerCase() + ": " + ((cc.toggles & t.flag) == 0 ? "disabled" : "enabled")).collect(Collectors.joining("\n"))
+ "\n\n" + TBMCSystemChatEvent.BroadcastTarget.stream().map(target -> target.getName() + ": " + (cc.brtoggles.contains(target) ? "enabled" : "disabled")).collect(Collectors.joining("\n"));
if (toggle == null) {
message.reply("toggles:\n" + togglesString.get());
return true;
}
String arg = toggle.toUpperCase();
val b = Arrays.stream(ChannelconBroadcast.values()).filter(t -> t.toString().equals(arg)).findAny();
if (!b.isPresent()) {
val bt = TBMCSystemChatEvent.BroadcastTarget.get(arg);
if (bt == null) {
message.reply("cannot find toggle. Toggles:\n" + togglesString.get());
return true;
}
final boolean add;
if (add = !cc.brtoggles.contains(bt))
cc.brtoggles.add(bt);
else
cc.brtoggles.remove(bt);
return respond(sender, "'" + bt.getName() + "' " + (add ? "en" : "dis") + "abled");
}
//A B | F
//------- A: original - B: mask - F: new
//0 0 | 0
//0 1 | 1
//1 0 | 1
//1 1 | 0
// XOR
cc.toggles ^= b.get().flag;
message.reply("'" + b.get().toString().toLowerCase() + "' " + ((cc.toggles & b.get().flag) == 0 ? "disabled" : "enabled"));
return true;
}
@Command2.Subcommand
public boolean def(Command2DCSender sender, String channelID) {
val message = sender.getMessage();
if (checkPerms(message)) return true;
if (MCChatCustom.hasCustomChat(message.getChannel()))
return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it.");
val chan = Channel.getChannels().filter(ch -> ch.ID.equalsIgnoreCase(channelID) || (Arrays.stream(ch.IDs().get()).anyMatch(cid -> cid.equalsIgnoreCase(channelID)))).findAny();
if (!chan.isPresent()) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW)
message.reply("MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.");
return true;
}
val dp = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class);
val chp = dp.getAs(TBMCPlayer.class);
if (chp == null) {
message.reply("you need to connect your Minecraft account. On our server in " + DPUtils.botmention() + " do " + DiscordPlugin.getPrefix() + "connect <MCname>");
return true;
}
DiscordConnectedPlayer dcp = new DiscordConnectedPlayer(message.getAuthor(), message.getChannel(), chp.getUUID(), Bukkit.getOfflinePlayer(chp.getUUID()).getName());
//Using a fake player with no login/logout, should be fine for this event
String groupid = chan.get().getGroupID(dcp);
if (groupid == null && !(chan.get() instanceof ChatRoom)) { //ChatRooms don't allow it unless the user joins, which happens later
message.reply("sorry, you cannot use that Minecraft channel.");
return true;
}
if (chan.get() instanceof ChatRoom) { //ChatRooms don't work well
message.reply("chat rooms are not supported yet.");
return true;
}
/*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) {
message.reply("sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm.");
return true;
}*/ //TODO: "Channel admins" that can connect channels?
MCChatCustom.addCustomChat(message.getChannel(), groupid, chan.get(), message.getAuthor(), dcp, 0, new HashSet<>());
if (chan.get() instanceof ChatRoom)
message.reply("alright, connection made to the room!");
else
message.reply("alright, connection made to group `" + groupid + "`!");
return true;
}
private boolean checkPerms(IMessage message) {
if (!PermissionUtils.hasPermissions(message.getChannel(), message.getAuthor(), Permissions.MANAGE_CHANNEL)) {
message.reply("you need to have manage permissions for this channel!");
return true;
}
return false;
}
@Override
public String[] getHelpText(Method method, Command2.Subcommand ann) {
return new String[]{ //
"Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).", //
"You need to have access to the MC channel and have manage permissions on the Discord channel.", //
"You also need to have your Minecraft account connected. In " + DPUtils.botmention() + " use " + DiscordPlugin.getPrefix() + "connect <mcname>.", //
"Call this command from the channel you want to use.", //
"Usage: @" + DiscordPlugin.dc.getOurUser().getName() + " channelcon <mcchannel>", //
"Use the ID (command) of the channel, for example `g` for the global chat.", //
"To remove a connection use @ChromaBot channelcon remove in the channel.", //
"Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix() + " prefix only works in " + DPUtils.botmention() + ".", //
"Invite link: <https://discordapp.com/oauth2/authorize?client_id=226443037893591041&scope=bot&permissions=268509264>" // TODO: Set correct client ID
};
}
}

View file

@ -1,39 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.Command2DCSender;
import buttondevteam.discordplugin.commands.ICommand2DC;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import lombok.val;
@CommandClass(helpText = {
"MC Chat",
"This command enables or disables the Minecraft chat in private messages.", //
"It can be useful if you don't want your messages to be visible, for example when talking in a private channel.", //
"You can also run all of the ingame commands you have access to using this command, if you have your accounts connected." //
})
public class MCChatCommand extends ICommand2DC {
@Command2.Subcommand
public boolean def(Command2DCSender sender) {
val message = sender.getMessage();
if (!message.getChannel().isPrivate()) {
message.reply("this command can only be issued in a direct message with the bot.");
return true;
}
try (final DiscordPlayer user = DiscordPlayer.getUser(message.getAuthor().getStringID(), DiscordPlayer.class)) {
boolean mcchat = !user.isMinecraftChatEnabled();
MCChatPrivate.privateMCChat(message.getChannel(), mcchat, message.getAuthor(), user);
message.reply("Minecraft chat " + (mcchat //
? "enabled. Use '" + DiscordPlugin.getPrefix() + "mcchat' again to turn it off." //
: "disabled."));
} catch (Exception e) {
TBMCCoreAPI.SendException("Error while setting mcchat for user" + message.getAuthor().getName(), e);
}
return true;
} // TODO: Pin channel switching to indicate the current channel
}

View file

@ -1,74 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.component.channel.Channel;
import buttondevteam.core.component.channel.ChatRoom;
import buttondevteam.discordplugin.DiscordConnectedPlayer;
import buttondevteam.lib.TBMCSystemChatEvent;
import lombok.NonNull;
import lombok.val;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class MCChatCustom {
/**
* Used for town or nation chats or anything else
*/
static ArrayList<CustomLMD> lastmsgCustom = new ArrayList<>();
public static void addCustomChat(IChannel channel, String groupid, Channel mcchannel, IUser user, DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) {
if (mcchannel instanceof ChatRoom) {
((ChatRoom) mcchannel).joinRoom(dcp);
if (groupid == null) groupid = mcchannel.getGroupID(dcp);
}
val lmd = new CustomLMD(channel, user, groupid, mcchannel, dcp, toggles, brtoggles);
lastmsgCustom.add(lmd);
}
public static boolean hasCustomChat(IChannel channel) {
return lastmsgCustom.stream().anyMatch(lmd -> lmd.channel.getLongID() == channel.getLongID());
}
@Nullable
public static CustomLMD getCustomChat(IChannel channel) {
return lastmsgCustom.stream().filter(lmd -> lmd.channel.getLongID() == channel.getLongID()).findAny().orElse(null);
}
public static boolean removeCustomChat(IChannel channel) {
MCChatUtils.lastmsgfromd.remove(channel.getLongID());
return lastmsgCustom.removeIf(lmd -> {
if (lmd.channel.getLongID() != channel.getLongID())
return false;
if (lmd.mcchannel instanceof ChatRoom)
((ChatRoom) lmd.mcchannel).leaveRoom(lmd.dcp);
return true;
});
}
public static List<CustomLMD> getCustomChats() {
return Collections.unmodifiableList(lastmsgCustom);
}
public static class CustomLMD extends MCChatUtils.LastMsgData {
public final String groupID;
public final Channel mcchannel;
public final DiscordConnectedPlayer dcp;
public int toggles;
public Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles;
private CustomLMD(@NonNull IChannel channel, @NonNull IUser user,
@NonNull String groupid, @NonNull Channel mcchannel, @NonNull DiscordConnectedPlayer dcp, int toggles, Set<TBMCSystemChatEvent.BroadcastTarget> brtoggles) {
super(channel, user);
groupID = groupid;
this.mcchannel = mcchannel;
this.dcp = dcp;
this.toggles = toggles;
this.brtoggles = brtoggles;
}
}
}

View file

@ -1,410 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.ComponentManager;
import buttondevteam.core.component.channel.Channel;
import buttondevteam.core.component.channel.ChatRoom;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.DiscordSender;
import buttondevteam.discordplugin.DiscordSenderBase;
import buttondevteam.discordplugin.listeners.CommandListener;
import buttondevteam.discordplugin.playerfaker.VanillaCommandListener;
import buttondevteam.lib.*;
import buttondevteam.lib.chat.ChatMessage;
import buttondevteam.lib.chat.TBMCChatAPI;
import buttondevteam.lib.player.TBMCPlayer;
import com.vdurmont.emoji.EmojiParser;
import lombok.val;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.scheduler.BukkitTask;
import sx.blah.discord.api.internal.json.objects.EmbedObject;
import sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IUser;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.EmbedBuilder;
import sx.blah.discord.util.MissingPermissionsException;
import java.awt.*;
import java.time.Instant;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class MCChatListener implements Listener {
private BukkitTask sendtask;
private LinkedBlockingQueue<AbstractMap.SimpleEntry<TBMCChatEvent, Instant>> sendevents = new LinkedBlockingQueue<>();
private Runnable sendrunnable;
private static Thread sendthread;
private final MinecraftChatModule module;
public MCChatListener(MinecraftChatModule minecraftChatModule) {
module = minecraftChatModule;
}
@EventHandler // Minecraft
public void onMCChat(TBMCChatEvent ev) {
if (!ComponentManager.isEnabled(MinecraftChatModule.class) || ev.isCancelled()) //SafeMode: Needed so it doesn't restart after server shutdown
return;
sendevents.add(new AbstractMap.SimpleEntry<>(ev, Instant.now()));
if (sendtask != null)
return;
sendrunnable = () -> {
sendthread = Thread.currentThread();
processMCToDiscord();
if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down
sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable);
};
sendtask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable);
}
private void processMCToDiscord() {
try {
TBMCChatEvent e;
Instant time;
val se = sendevents.take(); // Wait until an element is available
e = se.getKey();
time = se.getValue();
final String authorPlayer = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel().DisplayName().get()) + "] " //
+ ("Minecraft".equals(e.getOrigin()) ? "" : "[" + e.getOrigin().substring(0, 1) + "]") //
+ (DPUtils.sanitizeStringNoEscape(e.getSender() instanceof Player //
? ((Player) e.getSender()).getDisplayName() //
: e.getSender().getName()));
val color = e.getChannel().Color().get();
final EmbedBuilder embed = new EmbedBuilder().withAuthorName(authorPlayer)
.withDescription(e.getMessage()).withColor(new Color(color.getRed(),
color.getGreen(), color.getBlue()));
// embed.appendField("Channel", ((e.getSender() instanceof DiscordSenderBase ? "d|" : "")
// + DiscordPlugin.sanitizeString(e.getChannel().DisplayName)), false);
if (e.getSender() instanceof Player)
DPUtils.embedWithHead(
embed.withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=minecraft&id="
+ ((Player) e.getSender()).getUniqueId()),
e.getSender().getName());
else if (e.getSender() instanceof DiscordSenderBase)
embed.withAuthorIcon(((DiscordSenderBase) e.getSender()).getUser().getAvatarURL())
.withAuthorUrl("https://tbmcplugins.github.io/profile.html?type=discord&id="
+ ((DiscordSenderBase) e.getSender()).getUser().getStringID()); // TODO: Constant/method to get URLs like this
// embed.withFooterText(e.getChannel().DisplayName);
embed.withTimestamp(time);
final long nanoTime = System.nanoTime();
InterruptibleConsumer<MCChatUtils.LastMsgData> doit = lastmsgdata -> {
final EmbedObject embedObject = embed.build();
if (lastmsgdata.message == null || lastmsgdata.message.isDeleted()
|| !authorPlayer.equals(lastmsgdata.message.getEmbeds().get(0).getAuthor().getName())
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
|| !lastmsgdata.mcchannel.ID.equals(e.getChannel().ID)) {
lastmsgdata.message = DiscordPlugin.sendMessageToChannelWait(lastmsgdata.channel, "",
embedObject); // TODO Use ChromaBot API
lastmsgdata.time = nanoTime;
lastmsgdata.mcchannel = e.getChannel();
lastmsgdata.content = embedObject.description;
} else
try {
lastmsgdata.content = embedObject.description = lastmsgdata.content + "\n"
+ embedObject.description;// The message object doesn't get updated
final MCChatUtils.LastMsgData _lastmsgdata = lastmsgdata;
DPUtils.perform(() -> _lastmsgdata.message.edit("", embedObject));
} catch (MissingPermissionsException | DiscordException e1) {
TBMCCoreAPI.SendException("An error occurred while editing chat message!", e1);
}
};
// Checks if the given channel is different than where the message was sent from
// Or if it was from MC
Predicate<IChannel> isdifferentchannel = ch -> !(e.getSender() instanceof DiscordSenderBase)
|| ((DiscordSenderBase) e.getSender()).getChannel().getLongID() != ch.getLongID();
if (e.getChannel().isGlobal()
&& (e.isFromCommand() || isdifferentchannel.test(module.chatChannel().get())))
doit.accept(MCChatUtils.lastmsgdata == null
? MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannel().get(), null)
: MCChatUtils.lastmsgdata);
for (MCChatUtils.LastMsgData data : MCChatPrivate.lastmsgPerUser) {
if ((e.isFromCommand() || isdifferentchannel.test(data.channel))
&& e.shouldSendTo(MCChatUtils.getSender(data.channel, data.user)))
doit.accept(data);
}
val iterator = MCChatCustom.lastmsgCustom.iterator();
while (iterator.hasNext()) {
val lmd = iterator.next();
if ((e.isFromCommand() || isdifferentchannel.test(lmd.channel)) //Test if msg is from Discord
&& e.getChannel().ID.equals(lmd.mcchannel.ID) //If it's from a command, the command msg has been deleted, so we need to send it
&& e.getGroupID().equals(lmd.groupID)) { //Check if this is the group we want to test - #58
if (e.shouldSendTo(lmd.dcp)) //Check original user's permissions
doit.accept(lmd);
else {
iterator.remove(); //If the user no longer has permission, remove the connection
DiscordPlugin.sendMessageToChannel(lmd.channel, "The user no longer has permission to view the channel, connection removed.");
}
}
}
} catch (InterruptedException ex) { //Stop if interrupted anywhere
sendtask.cancel();
sendtask = null;
} catch (Exception ex) {
TBMCCoreAPI.SendException("Error while sending message to Discord!", ex);
}
}
@EventHandler
public void onChatPreprocess(TBMCChatPreprocessEvent event) {
int start = -1;
while ((start = event.getMessage().indexOf('@', start + 1)) != -1) {
int mid = event.getMessage().indexOf('#', start + 1);
if (mid == -1)
return;
int end_ = event.getMessage().indexOf(' ', mid + 1);
if (end_ == -1)
end_ = event.getMessage().length();
final int end = end_;
final int startF = start;
DiscordPlugin.dc.getUsersByName(event.getMessage().substring(start + 1, mid)).stream()
.filter(u -> u.getDiscriminator().equals(event.getMessage().substring(mid + 1, end))).findAny()
.ifPresent(user -> event.setMessage(event.getMessage().substring(0, startF) + "@" + user.getName()
+ (event.getMessage().length() > end ? event.getMessage().substring(end) : ""))); // TODO: Add formatting
start = end; // Skip any @s inside the mention
}
}
// ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender
// Offline public chat......x............................................
// Online public chat.......x...........................................x
// Offline private chat.....x.......................x....................
// Online private chat......x.......................x...................x
// If online and enabling private chat, don't login
// If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority
// If private chat is enabled and joining the server, logout the fake player on highest priority
// If online and disabling private chat, don't logout
// The maps may not contain the senders for UnconnectedSenders
/**
* Stop the listener. Any calls to onMCChat will restart it as long as we're not in safe mode.
*
* @param wait Wait 5 seconds for the threads to stop
*/
public static void stop(boolean wait) {
if (sendthread != null) sendthread.interrupt();
if (recthread != null) recthread.interrupt();
try {
if (sendthread != null) {
sendthread.interrupt();
if (wait)
sendthread.join(5000);
}
if (recthread != null) {
recthread.interrupt();
if (wait)
recthread.join(5000);
}
MCChatUtils.lastmsgdata = null;
MCChatPrivate.lastmsgPerUser.clear();
MCChatCustom.lastmsgCustom.clear();
MCChatUtils.lastmsgfromd.clear();
MCChatUtils.ConnectedSenders.clear();
MCChatUtils.UnconnectedSenders.clear();
recthread = sendthread = null;
} catch (InterruptedException e) {
e.printStackTrace(); //This thread shouldn't be interrupted
}
}
private BukkitTask rectask;
private LinkedBlockingQueue<MessageReceivedEvent> recevents = new LinkedBlockingQueue<>();
private Runnable recrun;
private static Thread recthread;
// Discord
public boolean handleDiscord(MessageReceivedEvent ev) {
if (!ComponentManager.isEnabled(MinecraftChatModule.class))
return false;
val author = ev.getMessage().getAuthor();
final boolean hasCustomChat = MCChatCustom.hasCustomChat(ev.getChannel());
if (ev.getMessage().getChannel().getLongID() != module.chatChannel().get().getLongID()
&& !(ev.getMessage().getChannel().isPrivate() && MCChatPrivate.isMinecraftChatEnabled(author.getStringID()))
&& !hasCustomChat)
return false; //Chat isn't enabled on this channel
if (ev.getMessage().getChannel().isPrivate() //Only in private chat
&& ev.getMessage().getContent().length() < "/mcchat<>".length()
&& ev.getMessage().getContent().replace("/", "")
.equalsIgnoreCase("mcchat")) //Either mcchat or /mcchat
return false; //Allow disabling the chat if needed
if (CommandListener.runCommand(ev.getMessage(), true))
return true; //Allow running commands in chat channels
MCChatUtils.resetLastMessage(ev.getChannel());
recevents.add(ev);
if (rectask != null)
return true;
recrun = () -> { //Don't return in a while loop next time
recthread = Thread.currentThread();
processDiscordToMC();
if (DiscordPlugin.plugin.isEnabled()) //Don't run again if shutting down
rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Continue message processing
};
rectask = Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, recrun); //Start message processing
return true;
}
private void processDiscordToMC() {
@val
sx.blah.discord.handle.impl.events.guild.channel.message.MessageReceivedEvent event;
try {
event = recevents.take();
} catch (InterruptedException e1) {
rectask.cancel();
return;
}
val sender = event.getMessage().getAuthor();
String dmessage = event.getMessage().getContent();
try {
final DiscordSenderBase dsender = MCChatUtils.getSender(event.getMessage().getChannel(), sender);
val user = dsender.getChromaUser();
for (IUser u : event.getMessage().getMentions()) {
dmessage = dmessage.replace(u.mention(false), "@" + u.getName()); // TODO: IG Formatting
final String nick = u.getNicknameForGuild(DiscordPlugin.mainServer);
dmessage = dmessage.replace(u.mention(true), "@" + (nick != null ? nick : u.getName()));
}
for (IChannel ch : event.getMessage().getChannelMentions()) {
dmessage = dmessage.replace(ch.mention(), "#" + ch.getName()); // TODO: IG Formatting
}
dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE); //Converts emoji to text- TODO: Add option to disable (resource pack?)
dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:"); //Convert to Discord's format so it still shows up
Function<String, String> getChatMessage = msg -> //
msg + (event.getMessage().getAttachments().size() > 0 ? "\n" + event.getMessage()
.getAttachments().stream().map(IMessage.Attachment::getUrl).collect(Collectors.joining("\n"))
: "");
MCChatCustom.CustomLMD clmd = MCChatCustom.getCustomChat(event.getChannel());
boolean react = false;
if (dmessage.startsWith("/")) { // Ingame command
DPUtils.perform(() -> {
if (!event.getMessage().isDeleted() && !event.getChannel().isPrivate())
event.getMessage().delete();
});
final String cmd = dmessage.substring(1);
final String cmdlowercased = cmd.toLowerCase();
if (dsender instanceof DiscordSender && module.whitelistedCommands().get().stream()
.noneMatch(s -> cmdlowercased.equals(s) || cmdlowercased.startsWith(s + " "))) {
// Command not whitelisted
dsender.sendMessage("Sorry, you can only access these commands:\n"
+ module.whitelistedCommands().get().stream().map(uc -> "/" + uc)
.collect(Collectors.joining(", "))
+ (user.getConnectedID(TBMCPlayer.class) == null
? "\nTo access your commands, first please connect your accounts, using /connect in "
+ DPUtils.botmention()
+ "\nThen y"
: "\nY")
+ "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!");
return;
}
val ev = new TBMCCommandPreprocessEvent(dsender, dmessage);
Bukkit.getPluginManager().callEvent(ev);
if (ev.isCancelled())
return;
int spi = cmdlowercased.indexOf(' ');
final String topcmd = spi == -1 ? cmdlowercased : cmdlowercased.substring(0, spi);
Optional<Channel> ch = Channel.getChannels()
.filter(c -> c.ID.equalsIgnoreCase(topcmd)
|| (c.IDs().get().length > 0
&& Arrays.stream(c.IDs().get()).anyMatch(id -> id.equalsIgnoreCase(topcmd)))).findAny();
if (!ch.isPresent()) //TODO: What if talking in the public chat while we have it on a different one
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, //Commands need to be run sync
() -> { //TODO: Better handling...
val channel = user.channel();
val chtmp = channel.get();
if (clmd != null) {
channel.set(clmd.mcchannel); //Hack to send command in the channel
} //TODO: Permcheck isn't implemented for commands
VanillaCommandListener.runBukkitOrVanillaCommand(dsender, cmd);
Bukkit.getLogger().info(dsender.getName() + " issued command from Discord: /" + cmdlowercased);
if (clmd != null)
channel.set(chtmp);
});
else {
Channel chc = ch.get();
if (!chc.isGlobal() && !event.getMessage().getChannel().isPrivate())
dsender.sendMessage(
"You can only talk in a public chat here. DM `mcchat` to enable private chat to talk in the other channels.");
else {
if (spi == -1) // Switch channels
{
val channel = dsender.getChromaUser().channel();
val oldch = channel.get();
if (oldch instanceof ChatRoom)
((ChatRoom) oldch).leaveRoom(dsender);
if (!oldch.ID.equals(chc.ID)) {
channel.set(chc);
if (chc instanceof ChatRoom)
((ChatRoom) chc).joinRoom(dsender);
} else
channel.set(Channel.GlobalChat);
dsender.sendMessage("You're now talking in: "
+ DPUtils.sanitizeString(channel.get().DisplayName().get()));
} else { // Send single message
final String msg = cmd.substring(spi + 1);
val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(msg)).fromCommand(true);
if (clmd == null)
TBMCChatAPI.SendChatMessage(cmb.build(), chc);
else
TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), chc);
react = true;
}
}
}
} else {// Not a command
if (dmessage.length() == 0 && event.getMessage().getAttachments().size() == 0
&& !event.getChannel().isPrivate() && event.getMessage().isSystemMessage()) {
val rtr = clmd != null ? clmd.mcchannel.getRTR(clmd.dcp)
: dsender.getChromaUser().channel().get().getRTR(dsender);
TBMCChatAPI.SendSystemMessage(clmd != null ? clmd.mcchannel : dsender.getChromaUser().channel().get(), rtr,
(dsender instanceof Player ? ((Player) dsender).getDisplayName()
: dsender.getName()) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL);
}
else {
val cmb = ChatMessage.builder(dsender, user, getChatMessage.apply(dmessage)).fromCommand(false);
if (clmd != null)
TBMCChatAPI.SendChatMessage(cmb.permCheck(clmd.dcp).build(), clmd.mcchannel);
else
TBMCChatAPI.SendChatMessage(cmb.build());
react = true;
}
}
if (react) {
try {
val lmfd = MCChatUtils.lastmsgfromd.get(event.getChannel().getLongID());
if (lmfd != null) {
DPUtils.perform(() -> lmfd.removeReaction(DiscordPlugin.dc.getOurUser(),
DiscordPlugin.DELIVERED_REACTION)); // Remove it no matter what, we know it's there 99.99% of the time
}
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e);
}
MCChatUtils.lastmsgfromd.put(event.getChannel().getLongID(), event.getMessage());
DPUtils.perform(() -> event.getMessage().addReaction(DiscordPlugin.DELIVERED_REACTION));
}
} catch (Exception e) {
TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e);
}
}
@FunctionalInterface
private interface InterruptibleConsumer<T> {
void accept(T value) throws TimeoutException, InterruptedException;
}
}

View file

@ -1,68 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.discordplugin.DiscordConnectedPlayer;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.player.TBMCPlayer;
import lombok.val;
import org.bukkit.Bukkit;
import org.bukkit.event.Event;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IPrivateChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.ArrayList;
public class MCChatPrivate {
/**
* Used for messages in PMs (mcchat).
*/
static ArrayList<MCChatUtils.LastMsgData> lastmsgPerUser = new ArrayList<>();
public static boolean privateMCChat(IChannel channel, boolean start, IUser user, DiscordPlayer dp) {
TBMCPlayer mcp = dp.getAs(TBMCPlayer.class);
if (mcp != null) { // If the accounts aren't connected, can't make a connected sender
val p = Bukkit.getPlayer(mcp.getUUID());
val op = Bukkit.getOfflinePlayer(mcp.getUUID());
if (start) {
val sender = new DiscordConnectedPlayer(user, channel, mcp.getUUID(), op.getName());
MCChatUtils.addSender(MCChatUtils.ConnectedSenders, user, sender);
if (p == null)// Player is offline - If the player is online, that takes precedence
callEventSync(new PlayerJoinEvent(sender, ""));
} else {
val sender = MCChatUtils.removeSender(MCChatUtils.ConnectedSenders, channel, user);
if (p == null)// Player is offline - If the player is online, that takes precedence
callEventSync(new PlayerQuitEvent(sender, ""));
}
} // ---- PermissionsEx warning is normal on logout ----
if (!start)
MCChatUtils.lastmsgfromd.remove(channel.getLongID());
return start //
? lastmsgPerUser.add(new MCChatUtils.LastMsgData(channel, user)) // Doesn't support group DMs
: lastmsgPerUser.removeIf(lmd -> lmd.channel.getLongID() == channel.getLongID());
}
public static boolean isMinecraftChatEnabled(DiscordPlayer dp) {
return isMinecraftChatEnabled(dp.getDiscordID());
}
public static boolean isMinecraftChatEnabled(String did) { // Don't load the player data just for this
return lastmsgPerUser.stream()
.anyMatch(lmd -> ((IPrivateChannel) lmd.channel).getRecipient().getStringID().equals(did));
}
public static void logoutAll() {
for (val entry : MCChatUtils.ConnectedSenders.entrySet())
for (val valueEntry : entry.getValue().entrySet())
if (MCChatUtils.getSender(MCChatUtils.OnlineSenders, valueEntry.getKey(), valueEntry.getValue().getUser()) == null) //If the player is online then the fake player was already logged out
MCChatUtils.callEventExcludingSome(new PlayerQuitEvent(valueEntry.getValue(), "")); //This is sync
MCChatUtils.ConnectedSenders.clear();
}
private static void callEventSync(Event event) {
Bukkit.getScheduler().runTask(DiscordPlugin.plugin, () -> MCChatUtils.callEventExcludingSome(event));
}
}

View file

@ -1,296 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.ComponentManager;
import buttondevteam.core.component.channel.Channel;
import buttondevteam.discordplugin.*;
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule;
import buttondevteam.lib.TBMCSystemChatEvent;
import io.netty.util.collection.LongObjectHashMap;
import lombok.RequiredArgsConstructor;
import lombok.experimental.var;
import lombok.val;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.AuthorNagException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.RegisteredListener;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IMessage;
import sx.blah.discord.handle.obj.IUser;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class MCChatUtils {
/**
* May contain P&lt;DiscordID&gt; as key for public chat
*/
public static final HashMap<String, HashMap<IChannel, DiscordSender>> UnconnectedSenders = new HashMap<>();
public static final HashMap<String, HashMap<IChannel, DiscordConnectedPlayer>> ConnectedSenders = new HashMap<>();
/**
* May contain P&lt;DiscordID&gt; as key for public chat
*/
public static final HashMap<String, HashMap<IChannel, DiscordPlayerSender>> OnlineSenders = new HashMap<>();
static @Nullable LastMsgData lastmsgdata;
static LongObjectHashMap<IMessage> lastmsgfromd = new LongObjectHashMap<>(); // Last message sent by a Discord user, used for clearing checkmarks
private static MinecraftChatModule module;
public static void updatePlayerList() {
if (notEnabled()) return;
DPUtils.performNoWait(() -> {
if (lastmsgdata != null)
updatePL(lastmsgdata);
MCChatCustom.lastmsgCustom.forEach(MCChatUtils::updatePL);
});
}
private static boolean notEnabled() {
return getModule() == null;
}
private static MinecraftChatModule getModule() {
if (module == null) module = ComponentManager.getIfEnabled(MinecraftChatModule.class);
else if (!module.isEnabled()) module = null; //Reset if disabled
return module;
}
private static void updatePL(LastMsgData lmd) {
String topic = lmd.channel.getTopic();
if (topic == null || topic.length() == 0)
topic = ".\n----\nMinecraft chat\n----\n.";
String[] s = topic.split("\\n----\\n");
if (s.length < 3)
return;
s[0] = Bukkit.getOnlinePlayers().size() + " player" + (Bukkit.getOnlinePlayers().size() != 1 ? "s" : "")
+ " online";
s[s.length - 1] = "Players: " + Bukkit.getOnlinePlayers().stream()
.map(p -> DPUtils.sanitizeString(p.getDisplayName())).collect(Collectors.joining(", "));
lmd.channel.changeTopic(String.join("\n----\n", s));
}
public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<IChannel, T>> senders,
IUser user, T sender) {
return addSender(senders, user.getStringID(), sender);
}
public static <T extends DiscordSenderBase> T addSender(HashMap<String, HashMap<IChannel, T>> senders,
String did, T sender) {
var map = senders.get(did);
if (map == null)
map = new HashMap<>();
map.put(sender.getChannel(), sender);
senders.put(did, map);
return sender;
}
public static <T extends DiscordSenderBase> T getSender(HashMap<String, HashMap<IChannel, T>> senders,
IChannel channel, IUser user) {
var map = senders.get(user.getStringID());
if (map != null)
return map.get(channel);
return null;
}
public static <T extends DiscordSenderBase> T removeSender(HashMap<String, HashMap<IChannel, T>> senders,
IChannel channel, IUser user) {
var map = senders.get(user.getStringID());
if (map != null)
return map.remove(channel);
return null;
}
public static void forAllMCChat(Consumer<IChannel> action) {
if (notEnabled()) return;
action.accept(module.chatChannel().get());
for (LastMsgData data : MCChatPrivate.lastmsgPerUser)
action.accept(data.channel);
// lastmsgCustom.forEach(cc -> action.accept(cc.channel)); - Only send relevant messages to custom chat
}
/**
* For custom and all MC chat
*
* @param action The action to act
* @param toggle The toggle to check
* @param hookmsg Whether the message is also sent from the hook
*/
public static void forCustomAndAllMCChat(Consumer<IChannel> action, @Nullable ChannelconBroadcast toggle, boolean hookmsg) {
if (notEnabled()) return;
if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg)
forAllMCChat(action);
final Consumer<MCChatCustom.CustomLMD> customLMDConsumer = cc -> action.accept(cc.channel);
if (toggle == null)
MCChatCustom.lastmsgCustom.forEach(customLMDConsumer);
else
MCChatCustom.lastmsgCustom.stream().filter(cc -> (cc.toggles & toggle.flag) != 0).forEach(customLMDConsumer);
}
/**
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
*
* @param action The action to do
* @param sender The sender to check perms of or null to send to all that has it toggled
* @param toggle The toggle to check or null to send to all allowed
*/
public static void forAllowedCustomMCChat(Consumer<IChannel> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle) {
if (notEnabled()) return;
MCChatCustom.lastmsgCustom.stream().filter(clmd -> {
//new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple
if (toggle != null && (clmd.toggles & toggle.flag) == 0)
return false; //If null then allow
if (sender == null)
return true;
return clmd.groupID.equals(clmd.mcchannel.getGroupID(sender));
}).forEach(cc -> action.accept(cc.channel)); //TODO: Send error messages on channel connect
}
/**
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
*
* @param action The action to do
* @param sender The sender to check perms of or null to send to all that has it toggled
* @param toggle The toggle to check or null to send to all allowed
* @param hookmsg Whether the message is also sent from the hook
*/
public static void forAllowedCustomAndAllMCChat(Consumer<IChannel> action, @Nullable CommandSender sender, @Nullable ChannelconBroadcast toggle, boolean hookmsg) {
if (notEnabled()) return;
if (!GeneralEventBroadcasterModule.isHooked() || !hookmsg)
forAllMCChat(action);
forAllowedCustomMCChat(action, sender, toggle);
}
public static Consumer<IChannel> send(String message) {
return ch -> DiscordPlugin.sendMessageToChannel(ch, DPUtils.sanitizeString(message));
}
public static void forAllowedMCChat(Consumer<IChannel> action, TBMCSystemChatEvent event) {
if (notEnabled()) return;
if (event.getChannel().isGlobal())
action.accept(module.chatChannel().get());
for (LastMsgData data : MCChatPrivate.lastmsgPerUser)
if (event.shouldSendTo(getSender(data.channel, data.user)))
action.accept(data.channel);
MCChatCustom.lastmsgCustom.stream().filter(clmd -> {
if (!clmd.brtoggles.contains(event.getTarget()))
return false;
return event.shouldSendTo(clmd.dcp);
}).map(clmd -> clmd.channel).forEach(action);
}
/**
* This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc.
*/
static DiscordSenderBase getSender(IChannel channel, final IUser author) {
//noinspection OptionalGetWithoutIsPresent
return Stream.<Supplier<Optional<DiscordSenderBase>>>of( // https://stackoverflow.com/a/28833677/2703239
() -> Optional.ofNullable(getSender(OnlineSenders, channel, author)), // Find first non-null
() -> Optional.ofNullable(getSender(ConnectedSenders, channel, author)), // This doesn't support the public chat, but it'll always return null for it
() -> Optional.ofNullable(getSender(UnconnectedSenders, channel, author)), //
() -> Optional.of(addSender(UnconnectedSenders, author,
new DiscordSender(author, channel)))).map(Supplier::get).filter(Optional::isPresent).map(Optional::get).findFirst().get();
}
/**
* Resets the last message, so it will start a new one instead of appending to it.
* This is used when someone (even the bot) sends a message to the channel.
*
* @param channel The channel to reset in - the process is slightly different for the public, private and custom chats
*/
public static void resetLastMessage(IChannel channel) {
if (notEnabled()) return;
if (channel.getLongID() == module.chatChannel().get().getLongID()) {
(lastmsgdata == null ? lastmsgdata = new LastMsgData(module.chatChannel().get(), null)
: lastmsgdata).message = null;
return;
} // Don't set the whole object to null, the player and channel information should be preserved
for (LastMsgData data : channel.isPrivate() ? MCChatPrivate.lastmsgPerUser : MCChatCustom.lastmsgCustom) {
if (data.channel.getLongID() == channel.getLongID()) {
data.message = null;
return;
}
}
//If it gets here, it's sending a message to a non-chat channel
}
public static void callEventExcludingSome(Event event) {
if (notEnabled()) return;
callEventExcluding(event, false, module.excludedPlugins().get());
}
/**
* Calls an event with the given details.
* <p>
* This method only synchronizes when the event is not asynchronous.
*
* @param event Event details
* @param only Flips the operation and <b>includes</b> the listed plugins
* @param plugins The plugins to exclude. Not case sensitive.
*/
@SuppressWarnings("WeakerAccess")
public static void callEventExcluding(Event event, boolean only, String... plugins) { // Copied from Spigot-API and modified a bit
if (event.isAsynchronous()) {
if (Thread.holdsLock(Bukkit.getPluginManager())) {
throw new IllegalStateException(
event.getEventName() + " cannot be triggered asynchronously from inside synchronized code.");
}
if (Bukkit.getServer().isPrimaryThread()) {
throw new IllegalStateException(
event.getEventName() + " cannot be triggered asynchronously from primary server thread.");
}
fireEventExcluding(event, only, plugins);
} else {
synchronized (Bukkit.getPluginManager()) {
fireEventExcluding(event, only, plugins);
}
}
}
private static void fireEventExcluding(Event event, boolean only, String... plugins) {
HandlerList handlers = event.getHandlers(); // Code taken from SimplePluginManager in Spigot-API
RegisteredListener[] listeners = handlers.getRegisteredListeners();
val server = Bukkit.getServer();
for (RegisteredListener registration : listeners) {
if (!registration.getPlugin().isEnabled()
|| Arrays.stream(plugins).anyMatch(p -> only ^ p.equalsIgnoreCase(registration.getPlugin().getName())))
continue; // Modified to exclude plugins
try {
registration.callEvent(event);
} catch (AuthorNagException ex) {
Plugin plugin = registration.getPlugin();
if (plugin.isNaggable()) {
plugin.setNaggable(false);
server.getLogger().log(Level.SEVERE,
String.format("Nag author(s): '%s' of '%s' about the following: %s",
plugin.getDescription().getAuthors(), plugin.getDescription().getFullName(),
ex.getMessage()));
}
} catch (Throwable ex) {
server.getLogger().log(Level.SEVERE, "Could not pass event " + event.getEventName() + " to "
+ registration.getPlugin().getDescription().getFullName(), ex);
}
}
}
@RequiredArgsConstructor
public static class LastMsgData {
public IMessage message;
public long time;
public String content;
public final IChannel channel;
public Channel mcchannel;
public final IUser user;
}
}

View file

@ -1,159 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.discordplugin.*;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.architecture.ConfigData;
import buttondevteam.lib.player.*;
import com.earth2me.essentials.CommandSource;
import lombok.RequiredArgsConstructor;
import lombok.val;
import net.ess3.api.events.AfkStatusChangeEvent;
import net.ess3.api.events.MuteStatusChangeEvent;
import net.ess3.api.events.NickChangeEvent;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerKickEvent;
import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerLoginEvent.Result;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.BroadcastMessageEvent;
import sx.blah.discord.handle.obj.IRole;
import sx.blah.discord.handle.obj.IUser;
import sx.blah.discord.util.DiscordException;
import sx.blah.discord.util.MissingPermissionsException;
@RequiredArgsConstructor
class MCListener implements Listener {
private final MinecraftChatModule module;
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerLogin(PlayerLoginEvent e) {
if (e.getResult() != Result.ALLOWED)
return;
MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream()) //Only private mcchat should be in ConnectedSenders
.filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny()
.ifPresent(dcp -> MCChatUtils.callEventExcludingSome(new PlayerQuitEvent(dcp, "")));
}
@EventHandler(priority = EventPriority.LOWEST)
public void onPlayerJoin(TBMCPlayerJoinEvent e) {
if (e.getPlayer() instanceof DiscordConnectedPlayer)
return; // Don't show the joined message for the fake player
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
final Player p = e.getPlayer();
DiscordPlayer dp = e.GetPlayer().getAs(DiscordPlayer.class);
if (dp != null) {
val user = DiscordPlugin.dc.getUserByID(Long.parseLong(dp.getDiscordID()));
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
new DiscordPlayerSender(user, user.getOrCreatePMChannel(), p));
MCChatUtils.addSender(MCChatUtils.OnlineSenders, dp.getDiscordID(),
new DiscordPlayerSender(user, module.chatChannel().get(), p)); //Stored per-channel
}
final String message = e.GetPlayer().PlayerName().get() + " joined the game";
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
ChromaBot.getInstance().updatePlayerList();
});
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerLeave(TBMCPlayerQuitEvent e) {
if (e.getPlayer() instanceof DiscordConnectedPlayer)
return; // Only care about real users
MCChatUtils.OnlineSenders.entrySet()
.removeIf(entry -> entry.getValue().entrySet().stream().anyMatch(p -> p.getValue().getUniqueId().equals(e.getPlayer().getUniqueId())));
Bukkit.getScheduler().runTask(DiscordPlugin.plugin,
() -> MCChatUtils.ConnectedSenders.values().stream().flatMap(v -> v.values().stream())
.filter(s -> s.getUniqueId().equals(e.getPlayer().getUniqueId())).findAny()
.ifPresent(dcp -> MCChatUtils.callEventExcludingSome(new PlayerJoinEvent(dcp, ""))));
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin,
ChromaBot.getInstance()::updatePlayerList, 5);
final String message = e.GetPlayer().PlayerName().get() + " left the game";
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), e.getPlayer(), ChannelconBroadcast.JOINLEAVE, true);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerKick(PlayerKickEvent e) {
/*if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting")
&& !e.getReason().equals("Server closed")) // The leave messages errored with the previous setup, I could make it wait since I moved it here, but instead I have a special
MCChatListener.forAllowedCustomAndAllMCChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook*/
}
@EventHandler(priority = EventPriority.LOW)
public void onPlayerDeath(PlayerDeathEvent e) {
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(e.getDeathMessage()), e.getEntity(), ChannelconBroadcast.DEATH, true);
}
@EventHandler
public void onPlayerAFK(AfkStatusChangeEvent e) {
final Player base = e.getAffected().getBase();
if (e.isCancelled() || !base.isOnline())
return;
final String msg = base.getDisplayName()
+ " is " + (e.getValue() ? "now" : "no longer") + " AFK.";
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), base, ChannelconBroadcast.AFK, false);
}
private ConfigData<IRole> muteRole() {
return DPUtils.roleData(module.getConfig(), "muteRole", "Muted");
}
@EventHandler
public void onPlayerMute(MuteStatusChangeEvent e) {
try {
DPUtils.performNoWait(() -> {
final IRole role = muteRole().get();
if (role == null) return;
final CommandSource source = e.getAffected().getSource();
if (!source.isPlayer())
return;
final DiscordPlayer p = TBMCPlayerBase.getPlayer(source.getPlayer().getUniqueId(), TBMCPlayer.class)
.getAs(DiscordPlayer.class);
if (p == null) return;
final IUser user = DiscordPlugin.dc.getUserByID(
Long.parseLong(p.getDiscordID()));
if (e.getValue())
user.addRole(role);
else
user.removeRole(role);
val modlog = module.modlogChannel().get();
String msg = (e.getValue() ? "M" : "Unm") + "uted user: " + user.getName();
if (modlog != null)
DiscordPlugin.sendMessageToChannel(modlog, msg);
DPUtils.getLogger().info(msg);
});
} catch (DiscordException | MissingPermissionsException ex) {
TBMCCoreAPI.SendException("Failed to give/take Muted role to player " + e.getAffected().getName() + "!",
ex);
}
}
@EventHandler
public void onChatSystemMessage(TBMCSystemChatEvent event) {
MCChatUtils.forAllowedMCChat(MCChatUtils.send(event.getMessage()), event);
}
@EventHandler
public void onBroadcastMessage(BroadcastMessageEvent event) {
MCChatUtils.forCustomAndAllMCChat(MCChatUtils.send(event.getMessage()), ChannelconBroadcast.BROADCAST, false);
}
@EventHandler
public void onYEEHAW(TBMCYEEHAWEvent event) { //TODO: Inherit from the chat event base to have channel support
String name = event.getSender() instanceof Player ? ((Player) event.getSender()).getDisplayName()
: event.getSender().getName();
//Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO
val yeehaw = DiscordPlugin.mainServer.getEmojiByName("YEEHAW");
MCChatUtils.forAllMCChat(MCChatUtils.send(name + (yeehaw != null ? " <:YEEHAW:" + yeehaw.getStringID() + ">s" : " YEEHAWs")));
}
@EventHandler
public void onNickChange(NickChangeEvent event) {
MCChatUtils.updatePlayerList();
}
}

View file

@ -1,112 +0,0 @@
package buttondevteam.discordplugin.mcchat;
import buttondevteam.core.component.channel.Channel;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordConnectedPlayer;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.TBMCSystemChatEvent;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import com.google.common.collect.Lists;
import lombok.Getter;
import lombok.val;
import org.bukkit.Bukkit;
import sx.blah.discord.handle.obj.IChannel;
import java.util.ArrayList;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Provides Minecraft chat connection to Discord. Commands may be used either in a public chat (limited) or in a DM.
*/
public class MinecraftChatModule extends Component<DiscordPlugin> {
private @Getter MCChatListener listener;
public MCChatListener getListener() { //It doesn't want to generate
return listener;
}
/**
* A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command!
*/
public ConfigData<ArrayList<String>> whitelistedCommands() {
return getConfig().getData("whitelistedCommands", () -> Lists.newArrayList("list", "u", "shrug", "tableflip", "unflip", "mwiki",
"yeehaw", "lenny", "rp", "plugins"));
}
/**
* The channel to use as the public Minecraft chat - everything public gets broadcasted here
*/
public ConfigData<IChannel> chatChannel() {
return DPUtils.channelData(getConfig(), "chatChannel", 239519012529111040L);
}
/**
* The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute
*/
public ConfigData<IChannel> modlogChannel() {
return DPUtils.channelData(getConfig(), "modlogChannel", 283840717275791360L);
}
/**
* 0 * The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here
*/
public ConfigData<String[]> excludedPlugins() {
return getConfig().getData("excludedPlugins", new String[]{"ProtocolLib", "LibsDisguises", "JourneyMapServer"});
}
@Override
protected void enable() {
if (DPUtils.disableIfConfigError(this, chatChannel())) return;
listener = new MCChatListener(this);
DiscordPlugin.dc.getDispatcher().registerListener(listener);
TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin());
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin());//These get undone if restarting/resetting - it will ignore events if disabled
getPlugin().getManager().registerCommand(new MCChatCommand());
getPlugin().getManager().registerCommand(new ChannelconCommand());
val chcons = getConfig().getConfig().getConfigurationSection("chcons");
if (chcons == null) //Fallback to old place
getConfig().getConfig().getRoot().getConfigurationSection("chcons");
if (chcons != null) {
val chconkeys = chcons.getKeys(false);
for (val chconkey : chconkeys) {
val chcon = chcons.getConfigurationSection(chconkey);
val mcch = Channel.getChannels().filter(ch -> ch.ID.equals(chcon.getString("mcchid"))).findAny();
val ch = DiscordPlugin.dc.getChannelByID(chcon.getLong("chid"));
val did = chcon.getLong("did");
val user = DiscordPlugin.dc.fetchUser(did);
val groupid = chcon.getString("groupid");
val toggles = chcon.getInt("toggles");
val brtoggles = chcon.getStringList("brtoggles");
if (!mcch.isPresent() || ch == null || user == null || groupid == null)
continue;
Bukkit.getScheduler().runTask(getPlugin(), () -> { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase)
val dcp = new DiscordConnectedPlayer(user, ch, UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"));
MCChatCustom.addCustomChat(ch, groupid, mcch.get(), user, dcp, toggles, brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::get).filter(Objects::nonNull).collect(Collectors.toSet()));
});
}
}
}
@Override
protected void disable() {
val chcons = MCChatCustom.getCustomChats();
val chconsc = getConfig().getConfig().createSection("chcons");
for (val chcon : chcons) {
val chconc = chconsc.createSection(chcon.channel.getStringID());
chconc.set("mcchid", chcon.mcchannel.ID);
chconc.set("chid", chcon.channel.getLongID());
chconc.set("did", chcon.user.getLongID());
chconc.set("mcuid", chcon.dcp.getUniqueId().toString());
chconc.set("mcname", chcon.dcp.getName());
chconc.set("groupid", chcon.groupID);
chconc.set("toggles", chcon.toggles);
chconc.set("brtoggles", chcon.brtoggles.stream().map(TBMCSystemChatEvent.BroadcastTarget::getName).collect(Collectors.toList()));
}
MCChatListener.stop(true);
}
}

View file

@ -1,44 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlayer;
import buttondevteam.discordplugin.commands.ConnectCommand;
import buttondevteam.discordplugin.mcchat.MCChatUtils;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.player.ChromaGamerBase;
import buttondevteam.lib.player.TBMCPlayer;
import buttondevteam.lib.player.TBMCPlayerBase;
import org.bukkit.entity.Player;
@CommandClass(modOnly = false, path = "accept")
public class AcceptMCCommand extends DiscordMCCommandBase {
@Override
public String[] GetHelpText(String alias) {
return new String[] { //
"§6---- Accept Discord connection ----", //
"Accept a pending connection between your Discord and Minecraft account.", //
"To start the connection process, do §b/connect <MCname>§r in the " + DPUtils.botmention() + " channel on Discord", //
"Usage: /" + alias + " accept" //
};
}
@Override
public boolean OnCommand(Player player, String alias, String[] args) {
String did = ConnectCommand.WaitingToConnect.get(player.getName());
if (did == null) {
player.sendMessage("§cYou don't have a pending connection to Discord.");
return true;
}
DiscordPlayer dp = ChromaGamerBase.getUser(did, DiscordPlayer.class);
TBMCPlayer mcp = TBMCPlayerBase.getPlayer(player.getUniqueId(), TBMCPlayer.class);
dp.connectWith(mcp);
dp.save();
mcp.save();
ConnectCommand.WaitingToConnect.remove(player.getName());
MCChatUtils.UnconnectedSenders.remove(did); //Remove all unconnected, will be recreated where needed
player.sendMessage("§bAccounts connected.");
return true;
}
}

View file

@ -1,32 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.commands.ConnectCommand;
import buttondevteam.lib.chat.CommandClass;
import org.bukkit.entity.Player;
@CommandClass(modOnly = false, path = "decline")
public class DeclineMCCommand extends DiscordMCCommandBase {
@Override
public String[] GetHelpText(String alias) {
return new String[] { //
"§6---- Decline Discord connection ----", //
"Decline a pending connection between your Discord and Minecraft account.", //
"To start the connection process, do §b/connect <MCname>§r in the " + DPUtils.botmention() + " channel on Discord", //
"Usage: /" + alias + " decline" //
};
}
@Override
public boolean OnCommand(Player player, String alias, String[] args) {
String did = ConnectCommand.WaitingToConnect.remove(player.getName());
if (did == null) {
player.sendMessage("§cYou don't have a pending connection to Discord.");
return true;
}
player.sendMessage("§bPending connection declined.");
return true;
}
}

View file

@ -1,9 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.PlayerCommandBase;
@CommandClass(modOnly = false, path = "discord")
public abstract class DiscordMCCommandBase extends PlayerCommandBase {
}

View file

@ -1,26 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.TBMCCommandBase;
import org.bukkit.command.CommandSender;
@CommandClass(path = "discord reload")
public class ReloadMCCommand extends TBMCCommandBase {
@Override
public boolean OnCommand(CommandSender sender, String alias, String[] args) {
if (DiscordPlugin.plugin.tryReloadConfig())
sender.sendMessage("§bConfig reloaded."); //TODO: Convert to new command system
else
sender.sendMessage("§cFailed to reload config.");
return true;
}
@Override
public String[] GetHelpText(String alias) {
return new String[]{
"Reload",
"Reloads the config. To apply some changes, you may need to also run /discord reset."
};
}
}

View file

@ -1,35 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.DiscordSenderBase;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.TBMCCommandBase;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
@CommandClass(path = "discord reset", modOnly = true)
public class ResetMCCommand extends TBMCCommandBase { //Not player-only, so not using DiscordMCCommandBase
public static boolean resetting = false;
@Override
public boolean OnCommand(CommandSender sender, String s, String[] strings) {
Bukkit.getScheduler().runTaskAsynchronously(DiscordPlugin.plugin, () -> {
resetting = true; //Turned off after sending enable message (ReadyEvent)
sender.sendMessage("§bDisabling DiscordPlugin...");
Bukkit.getPluginManager().disablePlugin(DiscordPlugin.plugin);
if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors
sender.sendMessage("§bEnabling DiscordPlugin...");
Bukkit.getPluginManager().enablePlugin(DiscordPlugin.plugin);
if (!(sender instanceof DiscordSenderBase)) //Sending to Discord errors
sender.sendMessage("§bReset finished!");
});
return true;
}
@Override
public String[] GetHelpText(String s) {
return new String[]{ //
"§6---- Reset ChromaBot ----", //
"This command disables and then enables the plugin." //
};
}
}

View file

@ -1,20 +0,0 @@
package buttondevteam.discordplugin.mccommands;
import buttondevteam.discordplugin.commands.VersionCommand;
import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.TBMCCommandBase;
import org.bukkit.command.CommandSender;
@CommandClass(path = "discord version")
public class VersionMCCommand extends TBMCCommandBase {
@Override
public boolean OnCommand(CommandSender commandSender, String s, String[] strings) {
commandSender.sendMessage(VersionCommand.getVersion());
return true;
}
@Override
public String[] GetHelpText(String s) {
return VersionCommand.getVersion(); //Heh
}
}

View file

@ -1,300 +0,0 @@
package buttondevteam.discordplugin.playerfaker;
import buttondevteam.discordplugin.DiscordSenderBase;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.*;
import org.bukkit.block.PistonMoveReaction;
import org.bukkit.entity.Entity;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause;
import org.bukkit.metadata.MetadataValue;
import org.bukkit.plugin.Plugin;
import org.bukkit.util.Vector;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.*;
@Getter
@Setter
@SuppressWarnings("deprecated")
public abstract class DiscordEntity extends DiscordSenderBase implements Entity {
protected DiscordEntity(IUser user, IChannel channel, int entityId, UUID uuid) {
super(user, channel);
this.entityId = entityId;
uniqueId = uuid;
}
private HashMap<String, MetadataValue> metadata = new HashMap<String, MetadataValue>();
private Location location = new Location(Bukkit.getWorlds().get(0), 0, 0, 0);
private Vector velocity;
private final int entityId;
private EntityDamageEvent lastDamageCause;
private final Set<String> scoreboardTags = new HashSet<String>();
private final UUID uniqueId;
@Override
public void setMetadata(String metadataKey, MetadataValue newMetadataValue) {
metadata.put(metadataKey, newMetadataValue);
}
@Override
public List<MetadataValue> getMetadata(String metadataKey) {
return Arrays.asList(metadata.get(metadataKey)); // Who needs multiple data anyways
}
@Override
public boolean hasMetadata(String metadataKey) {
return metadata.containsKey(metadataKey);
}
@Override
public void removeMetadata(String metadataKey, Plugin owningPlugin) {
metadata.remove(metadataKey);
}
@Override
public Location getLocation(Location loc) {
if (loc != null) {
loc.setWorld(getWorld());
loc.setX(location.getX());
loc.setY(location.getY());
loc.setZ(location.getZ());
loc.setYaw(location.getYaw());
loc.setPitch(location.getPitch());
}
return loc;
}
@Override
public double getHeight() {
return 0;
}
@Override
public double getWidth() {
return 0;
}
@Override
public boolean isOnGround() {
return false;
}
@Override
public World getWorld() {
return location.getWorld();
}
@Override
public boolean teleport(Location location) {
this.location = location;
return true;
}
@Override
public boolean teleport(Location location, TeleportCause cause) {
this.location = location;
return true;
}
@Override
public boolean teleport(Entity destination) {
this.location = destination.getLocation();
return true;
}
@Override
public boolean teleport(Entity destination, TeleportCause cause) {
this.location = destination.getLocation();
return true;
}
@Override
public List<Entity> getNearbyEntities(double x, double y, double z) {
return Arrays.asList();
}
@Override
public int getFireTicks() {
return 0;
}
@Override
public int getMaxFireTicks() {
return 0;
}
@Override
public void setFireTicks(int ticks) {
}
@Override
public void remove() {
}
@Override
public boolean isDead() { // Impossible to kill
return false;
}
@Override
public boolean isValid() {
return true;
}
@Override
public Server getServer() {
return Bukkit.getServer();
}
@Override
public Entity getPassenger() {
return null;
}
@Override
public boolean setPassenger(Entity passenger) {
return false;
}
@Override
public List<Entity> getPassengers() {
return Arrays.asList();
}
@Override
public boolean addPassenger(Entity passenger) {
return false;
}
@Override
public boolean removePassenger(Entity passenger) { // Don't support passengers
return false;
}
@Override
public boolean isEmpty() {
return true;
}
@Override
public boolean eject() {
return false;
}
@Override
public float getFallDistance() {
return 0;
}
@Override
public void setFallDistance(float distance) {
}
@Override
public int getTicksLived() {
return 1;
}
@Override
public void setTicksLived(int value) {
}
@Override
public void playEffect(EntityEffect type) {
}
@Override
public boolean isInsideVehicle() {
return false;
}
@Override
public boolean leaveVehicle() {
return false;
}
@Override
public Entity getVehicle() { // Don't support vehicles
return null;
}
@Override
public void setCustomNameVisible(boolean flag) {
}
@Override
public boolean isCustomNameVisible() {
return true;
}
@Override
public void setGlowing(boolean flag) {
}
@Override
public boolean isGlowing() {
return false;
}
@Override
public void setInvulnerable(boolean flag) {
}
@Override
public boolean isInvulnerable() {
return true;
}
@Override
public boolean isSilent() {
return true;
}
@Override
public void setSilent(boolean flag) {
}
@Override
public boolean hasGravity() {
return false;
}
@Override
public void setGravity(boolean gravity) {
}
@Override
public int getPortalCooldown() {
return 0;
}
@Override
public void setPortalCooldown(int cooldown) {
}
@Override
public boolean addScoreboardTag(String tag) {
return scoreboardTags.add(tag);
}
@Override
public boolean removeScoreboardTag(String tag) {
return scoreboardTags.remove(tag);
}
@Override
public PistonMoveReaction getPistonMoveReaction() {
return PistonMoveReaction.IGNORE;
}
@Override
public Entity.Spigot spigot() {
return new Entity.Spigot();
}
}

View file

@ -1,717 +0,0 @@
package buttondevteam.discordplugin.playerfaker;
import buttondevteam.discordplugin.DiscordPlugin;
import lombok.Getter;
import lombok.experimental.Delegate;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.conversations.Conversation;
import org.bukkit.conversations.ConversationAbandonedEvent;
import org.bukkit.entity.Entity;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.player.AsyncPlayerChatEvent;
import org.bukkit.map.MapView;
import org.bukkit.permissions.PermissibleBase;
import org.bukkit.plugin.Plugin;
import org.bukkit.scoreboard.Scoreboard;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.net.InetSocketAddress;
import java.util.*;
@SuppressWarnings("deprecation")
public class DiscordFakePlayer extends DiscordHumanEntity implements Player {
protected DiscordFakePlayer(IUser user, IChannel channel, int entityId, UUID uuid, String mcname) {
super(user, channel, entityId, uuid);
perm = new PermissibleBase(Bukkit.getOfflinePlayer(uuid));
name = mcname;
}
@Delegate
private PermissibleBase perm;
private @Getter String name;
@Override
public EntityType getType() {
return EntityType.PLAYER;
}
@Override
public String getCustomName() {
return user.getName();
}
@Override
public void setCustomName(String name) {
}
@Override
public boolean isConversing() {
return false;
}
@Override
public void acceptConversationInput(String input) {
}
@Override
public boolean beginConversation(Conversation conversation) {
return false;
}
@Override
public void abandonConversation(Conversation conversation) {
}
@Override
public void abandonConversation(Conversation conversation, ConversationAbandonedEvent details) {
}
@Override
public boolean isOnline() {
return true;// Let's pretend
}
@Override
public boolean isBanned() {
return false;
}
@Override
public boolean isWhitelisted() {
return true;
}
@Override
public void setWhitelisted(boolean value) {
}
@Override
public Player getPlayer() {
return this;
}
@Override
public long getFirstPlayed() {
return 0;
}
@Override
public long getLastPlayed() {
return 0;
}
@Override
public boolean hasPlayedBefore() {
return false;
}
@Override
public Map<String, Object> serialize() {
return new HashMap<>();
}
@Override
public void sendPluginMessage(Plugin source, String channel, byte[] message) {
}
@Override
public Set<String> getListeningPluginChannels() {
return Collections.emptySet();
}
@Override
public String getDisplayName() {
return user.getDisplayName(DiscordPlugin.mainServer);
}
@Override
public void setDisplayName(String name) {
}
@Override
public String getPlayerListName() {
return getName();
}
@Override
public void setPlayerListName(String name) {
}
@Override
public void setCompassTarget(Location loc) {
}
@Override
public Location getCompassTarget() {
return new Location(Bukkit.getWorlds().get(0), 0, 0, 0);
}
@Override
public InetSocketAddress getAddress() {
return null;
}
@Override
public void sendRawMessage(String message) {
sendMessage(message);
}
@Override
public void kickPlayer(String message) {
}
@Override
public void chat(String msg) {
Bukkit.getPluginManager()
.callEvent(new AsyncPlayerChatEvent(true, this, msg, new HashSet<>(Bukkit.getOnlinePlayers())));
}
@Override
public boolean performCommand(String command) {
return Bukkit.getServer().dispatchCommand(this, command);
}
@Override
public boolean isSneaking() {
return false;
}
@Override
public void setSneaking(boolean sneak) {
}
@Override
public boolean isSprinting() {
return false;
}
@Override
public void setSprinting(boolean sprinting) {
}
@Override
public void saveData() {
}
@Override
public void loadData() {
}
@Override
public void setSleepingIgnored(boolean isSleeping) {
}
@Override
public boolean isSleepingIgnored() {
return false;
}
@Override
public void playNote(Location loc, byte instrument, byte note) {
}
@Override
public void playNote(Location loc, Instrument instrument, Note note) {
}
@Override
public void playSound(Location location, Sound sound, float volume, float pitch) {
}
@Override
public void playSound(Location location, String sound, float volume, float pitch) {
}
@Override
public void playSound(Location location, Sound sound, SoundCategory category, float volume, float pitch) {
}
@Override
public void playSound(Location location, String sound, SoundCategory category, float volume, float pitch) {
}
@Override
public void stopSound(Sound sound) {
}
@Override
public void stopSound(String sound) {
}
@Override
public void stopSound(Sound sound, SoundCategory category) {
}
@Override
public void stopSound(String sound, SoundCategory category) {
}
@Override
public void playEffect(Location loc, Effect effect, int data) {
}
@Override
public <T> void playEffect(Location loc, Effect effect, T data) {
}
@Override
public void sendBlockChange(Location loc, Material material, byte data) {
}
@Override
public boolean sendChunkChange(Location loc, int sx, int sy, int sz, byte[] data) {
return false;
}
@Override
public void sendBlockChange(Location loc, int material, byte data) {
}
@Override
public void sendSignChange(Location loc, String[] lines) throws IllegalArgumentException {
}
@Override
public void sendMap(MapView map) {
}
@Override
public void updateInventory() {
}
@Override
public void awardAchievement(@SuppressWarnings("deprecation") Achievement achievement) {
}
@Override
public void removeAchievement(@SuppressWarnings("deprecation") Achievement achievement) {
}
@Override
public boolean hasAchievement(@SuppressWarnings("deprecation") Achievement achievement) {
return false;
}
@Override
public void incrementStatistic(Statistic statistic) throws IllegalArgumentException {
}
@Override
public void decrementStatistic(Statistic statistic) throws IllegalArgumentException {
}
@Override
public void incrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException {
}
@Override
public void decrementStatistic(Statistic statistic, int amount) throws IllegalArgumentException {
}
@Override
public void setStatistic(Statistic statistic, int newValue) throws IllegalArgumentException {
}
@Override
public int getStatistic(Statistic statistic) throws IllegalArgumentException {
return 0;
}
@Override
public void incrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException {
}
@Override
public void decrementStatistic(Statistic statistic, Material material) throws IllegalArgumentException {
}
@Override
public int getStatistic(Statistic statistic, Material material) throws IllegalArgumentException {
return 0;
}
@Override
public void incrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException {
}
@Override
public void decrementStatistic(Statistic statistic, Material material, int amount) throws IllegalArgumentException {
}
@Override
public void setStatistic(Statistic statistic, Material material, int newValue) throws IllegalArgumentException {
}
@Override
public void incrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException {
}
@Override
public void decrementStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException {
}
@Override
public int getStatistic(Statistic statistic, EntityType entityType) throws IllegalArgumentException {
return 0;
}
@Override
public void incrementStatistic(Statistic statistic, EntityType entityType, int amount)
throws IllegalArgumentException {
}
@Override
public void decrementStatistic(Statistic statistic, EntityType entityType, int amount) {
}
@Override
public void setStatistic(Statistic statistic, EntityType entityType, int newValue) {
}
@Override
public void setPlayerTime(long time, boolean relative) {
}
@Override
public long getPlayerTime() {
return 0;
}
@Override
public long getPlayerTimeOffset() {
return 0;
}
@Override
public boolean isPlayerTimeRelative() {
return false;
}
@Override
public void resetPlayerTime() {
}
@Override
public void setPlayerWeather(WeatherType type) {
}
@Override
public WeatherType getPlayerWeather() {
return null;
}
@Override
public void resetPlayerWeather() {
}
@Override
public void giveExp(int amount) {
}
@Override
public void giveExpLevels(int amount) {
}
@Override
public float getExp() {
return 0;
}
@Override
public void setExp(float exp) {
}
@Override
public int getLevel() {
return 0;
}
@Override
public void setLevel(int level) {
}
@Override
public int getTotalExperience() {
return 0;
}
@Override
public void setTotalExperience(int exp) {
}
@Override
public float getExhaustion() {
return 0;
}
@Override
public void setExhaustion(float value) {
}
@Override
public float getSaturation() {
return 0;
}
@Override
public void setSaturation(float value) {
}
@Override
public int getFoodLevel() {
return 0;
}
@Override
public void setFoodLevel(int value) {
}
@Override
public Location getBedSpawnLocation() {
return null;
}
@Override
public void setBedSpawnLocation(Location location) {
}
@Override
public void setBedSpawnLocation(Location location, boolean force) {
}
@Override
public boolean getAllowFlight() {
return false;
}
@Override
public void setAllowFlight(boolean flight) {
}
@Override
public void hidePlayer(Player player) {
}
@Override
public void showPlayer(Player player) {
}
@Override
public boolean canSee(Player player) { // Nobody can see them
return false;
}
@Override
public boolean isFlying() {
return false;
}
@Override
public void setFlying(boolean value) {
}
@Override
public void setFlySpeed(float value) throws IllegalArgumentException {
}
@Override
public void setWalkSpeed(float value) throws IllegalArgumentException {
}
@Override
public float getFlySpeed() {
return 0;
}
@Override
public float getWalkSpeed() {
return 0;
}
@Override
public void setTexturePack(String url) {
}
@Override
public void setResourcePack(String url) {
}
@Override
public void setResourcePack(String url, byte[] hash) {
}
@Override
public Scoreboard getScoreboard() {
return null;
}
@Override
public void setScoreboard(Scoreboard scoreboard) throws IllegalArgumentException, IllegalStateException {
}
@Override
public boolean isHealthScaled() {
return false;
}
@Override
public void setHealthScaled(boolean scale) {
}
@Override
public void setHealthScale(double scale) throws IllegalArgumentException {
}
@Override
public double getHealthScale() {
return 1;
}
@Override
public Entity getSpectatorTarget() {
return null;
}
@Override
public void setSpectatorTarget(Entity entity) {
}
@Override
public void sendTitle(String title, String subtitle) {
}
@Override
public void sendTitle(String title, String subtitle, int fadeIn, int stay, int fadeOut) {
}
@Override
public void resetTitle() {
}
@Override
public void spawnParticle(Particle particle, Location location, int count) {
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count) {
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, T data) {
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count, T data) {
}
@Override
public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY,
double offsetZ) {
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX,
double offsetY, double offsetZ) {
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY,
double offsetZ, T data) {
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX,
double offsetY, double offsetZ, T data) {
}
@Override
public void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY,
double offsetZ, double extra) {
}
@Override
public void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX,
double offsetY, double offsetZ, double extra) {
}
@Override
public <T> void spawnParticle(Particle particle, Location location, int count, double offsetX, double offsetY,
double offsetZ, double extra, T data) {
}
@Override
public <T> void spawnParticle(Particle particle, double x, double y, double z, int count, double offsetX,
double offsetY, double offsetZ, double extra, T data) {
}
@Override
public AdvancementProgress getAdvancementProgress(Advancement advancement) { // TODO: Test
return null;
}
@Override
public String getLocale() {
return null;
}
@Override
public Player.Spigot spigot() {
return new Player.Spigot();
}
}

View file

@ -1,167 +0,0 @@
package buttondevteam.discordplugin.playerfaker;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Entity;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Villager;
import org.bukkit.inventory.*;
import org.bukkit.inventory.InventoryView.Property;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.UUID;
public abstract class DiscordHumanEntity extends DiscordLivingEntity implements HumanEntity {
protected DiscordHumanEntity(IUser user, IChannel channel, int entityId, UUID uuid) {
super(user, channel, entityId, uuid);
}
private PlayerInventory inv = new DiscordPlayerInventory(this);
@Override
public PlayerInventory getInventory() {
return inv;
}
private Inventory enderchest = new DiscordInventory(this);
@Override
public Inventory getEnderChest() {
return enderchest;
}
@Override
public MainHand getMainHand() {
return MainHand.RIGHT;
}
@Override
public boolean setWindowProperty(Property prop, int value) {
return false;
}
@Override
public InventoryView getOpenInventory() { // TODO: Test
return null;
}
@Override
public InventoryView openInventory(Inventory inventory) {
return null;
}
@Override
public InventoryView openWorkbench(Location location, boolean force) {
return null;
}
@Override
public InventoryView openEnchanting(Location location, boolean force) {
return null;
}
@Override
public void openInventory(InventoryView inventory) {
}
@Override
public InventoryView openMerchant(Villager trader, boolean force) {
return null;
}
@Override
public InventoryView openMerchant(Merchant merchant, boolean force) {
return null;
}
@Override
public void closeInventory() {
}
@Override
public ItemStack getItemInHand() { // TODO: Test all ItemStack methods
return null;
}
@Override
public void setItemInHand(ItemStack item) {
}
@Override
public ItemStack getItemOnCursor() {
return null;
}
@Override
public void setItemOnCursor(ItemStack item) {
}
@Override
public boolean hasCooldown(Material material) {
return false;
}
@Override
public int getCooldown(Material material) {
return 0;
}
@Override
public void setCooldown(Material material, int ticks) {
}
@Override
public boolean isSleeping() {
return false;
}
@Override
public int getSleepTicks() {
return 0;
}
@Override
public GameMode getGameMode() {
return GameMode.SPECTATOR;
}
@Override
public void setGameMode(GameMode mode) {
}
@Override
public boolean isBlocking() {
return false;
}
@Override
public boolean isHandRaised() {
return false;
}
@Override
public int getExpToLevel() {
return 0;
}
@Override
public Entity getShoulderEntityLeft() {
return null;
}
@Override
public void setShoulderEntityLeft(Entity entity) {
}
@Override
public Entity getShoulderEntityRight() {
return null;
}
@Override
public void setShoulderEntityRight(Entity entity) {
}
}

View file

@ -1,212 +0,0 @@
package buttondevteam.discordplugin.playerfaker;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class DiscordInventory implements Inventory {
public DiscordInventory(DiscordHumanEntity holder) {
this.holder = holder;
}
@Override
public int getSize() {
return 0;
}
@Override
public int getMaxStackSize() {
return 0;
}
@Override
public void setMaxStackSize(int size) {
}
@Override
public String getName() {
return "Player inventory";
}
@Override
public ItemStack getItem(int index) {
return null;
}
@Override
public HashMap<Integer, ItemStack> addItem(ItemStack... items) throws IllegalArgumentException { // Can't add anything
return new HashMap<>(
IntStream.range(0, items.length).boxed().collect(Collectors.toMap(i -> i, i -> items[i])));
}
@Override
public HashMap<Integer, ItemStack> removeItem(ItemStack... items) throws IllegalArgumentException {
return new HashMap<>(
IntStream.range(0, items.length).boxed().collect(Collectors.toMap(i -> i, i -> items[i])));
}
@Override
public ItemStack[] getContents() {
return new ItemStack[0];
}
@Override
public void setContents(ItemStack[] items) throws IllegalArgumentException {
if (items.length > 0)
throw new IllegalArgumentException("This inventory does not support items");
}
@Override
public ItemStack[] getStorageContents() {
return new ItemStack[0];
}
@Override
public void setStorageContents(ItemStack[] items) throws IllegalArgumentException {
if (items.length > 0)
throw new IllegalArgumentException("This inventory does not support items");
}
@Override
public boolean contains(int materialId) {
return false;
}
@Override
public boolean contains(Material material) throws IllegalArgumentException {
return false;
}
@Override
public boolean contains(ItemStack item) {
return false;
}
@Override
public boolean contains(int materialId, int amount) {
return false;
}
@Override
public boolean contains(Material material, int amount) throws IllegalArgumentException {
return false;
}
@Override
public boolean contains(ItemStack item, int amount) {
return false;
}
@Override
public boolean containsAtLeast(ItemStack item, int amount) {
return false;
}
@Override
public HashMap<Integer, ? extends ItemStack> all(int materialId) {
return new HashMap<>();
}
@Override
public HashMap<Integer, ? extends ItemStack> all(Material material) throws IllegalArgumentException {
return new HashMap<>();
}
@Override
public HashMap<Integer, ? extends ItemStack> all(ItemStack item) {
return new HashMap<>();
}
@Override
public int first(int materialId) {
return -1;
}
@Override
public int first(Material material) throws IllegalArgumentException {
return -1;
}
@Override
public int first(ItemStack item) {
return -1;
}
@Override
public int firstEmpty() {
return -1;
}
@Override
public void remove(int materialId) {
}
@Override
public void remove(Material material) throws IllegalArgumentException {
}
@Override
public void remove(ItemStack item) {
}
@Override
public void clear(int index) {
}
@Override
public void clear() {
}
@Override
public List<HumanEntity> getViewers() {
return new ArrayList<>(0);
}
@Override
public String getTitle() {
return "Player inventory";
}
@Override
public InventoryType getType() {
return InventoryType.PLAYER;
}
private ListIterator<ItemStack> iterator = new ArrayList<ItemStack>(0).listIterator();
@Override
public ListIterator<ItemStack> iterator() {
return iterator;
}
@Override
public ListIterator<ItemStack> iterator(int index) {
return iterator;
}
@Override
public Location getLocation() {
return holder.getLocation();
}
@Override
public void setItem(int index, ItemStack item) {
}
private HumanEntity holder;
@Override
public HumanEntity getHolder() {
return holder;
}
}

View file

@ -1,297 +0,0 @@
package buttondevteam.discordplugin.playerfaker;
import lombok.Getter;
import lombok.Setter;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.inventory.EntityEquipment;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.util.Vector;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IUser;
import java.util.*;
public abstract class DiscordLivingEntity extends DiscordEntity implements LivingEntity {
protected DiscordLivingEntity(IUser user, IChannel channel, int entityId, UUID uuid) {
super(user, channel, entityId, uuid);
}
private @Getter EntityEquipment equipment = new DiscordEntityEquipment(this);
@Getter
@Setter
private static class DiscordEntityEquipment implements EntityEquipment {
private float leggingsDropChance;
private ItemStack leggings;
private float itemInOffHandDropChance;
private ItemStack itemInOffHand;
private float itemInMainHandDropChance;
private ItemStack itemInMainHand;
private float itemInHandDropChance;
private ItemStack itemInHand;
private float helmetDropChance;
private ItemStack helmet;
private float chestplateDropChance;
private ItemStack chestplate;
private float bootsDropChance;
private ItemStack boots;
private ItemStack[] armorContents = new ItemStack[0]; // TODO
private final Entity holder;
public DiscordEntityEquipment(Entity holder) {
this.holder = holder;
}
@Override
public void clear() {
armorContents = new ItemStack[0];
}
}
@Override
public AttributeInstance getAttribute(Attribute attribute) { // We don't support any attributes
return null;
}
@Override
public void damage(double amount) {
}
@Override
public void damage(double amount, Entity source) {
}
@Override
public double getHealth() {
return getMaxHealth();
}
@Override
public void setHealth(double health) {
}
@Override
public double getMaxHealth() {
return 100;
}
@Override
public void setMaxHealth(double health) {
}
@Override
public void resetMaxHealth() {
}
@Override
public <T extends Projectile> T launchProjectile(Class<? extends T> projectile) {
return null;
}
@Override
public <T extends Projectile> T launchProjectile(Class<? extends T> projectile, Vector velocity) {
return null;
}
@Override
public double getEyeHeight() {
return 0;
}
@Override
public double getEyeHeight(boolean ignoreSneaking) {
return 0;
}
@Override
public Location getEyeLocation() {
return getLocation();
}
@Override
public List<Block> getLineOfSight(Set<Material> transparent, int maxDistance) {
return Arrays.asList();
}
@Override
public Block getTargetBlock(HashSet<Byte> transparent, int maxDistance) {
return null;
}
@Override
public Block getTargetBlock(Set<Material> transparent, int maxDistance) {
return null;
}
@Override
public List<Block> getLastTwoTargetBlocks(HashSet<Byte> transparent, int maxDistance) {
return Arrays.asList();
}
@Override
public List<Block> getLastTwoTargetBlocks(Set<Material> transparent, int maxDistance) {
return Arrays.asList();
}
@Override
public int getRemainingAir() {
return 100;
}
@Override
public void setRemainingAir(int ticks) {
}
@Override
public int getMaximumAir() {
return 100;
}
@Override
public void setMaximumAir(int ticks) {
}
@Override
public int getMaximumNoDamageTicks() {
return 100;
}
@Override
public void setMaximumNoDamageTicks(int ticks) {
}
@Override
public double getLastDamage() {
return 0;
}
@Override
public void setLastDamage(double damage) {
}
@Override
public int getNoDamageTicks() {
return 100;
}
@Override
public void setNoDamageTicks(int ticks) {
}
@Override
public Player getKiller() {
return null;
}
@Override
public boolean addPotionEffect(PotionEffect effect) {
return false;
}
@Override
public boolean addPotionEffect(PotionEffect effect, boolean force) {
return false;
}
@Override
public boolean addPotionEffects(Collection<PotionEffect> effects) {
return false;
}
@Override
public boolean hasPotionEffect(PotionEffectType type) {
return false;
}
@Override
public PotionEffect getPotionEffect(PotionEffectType type) {
return null;
}
@Override
public void removePotionEffect(PotionEffectType type) {
}
@Override
public Collection<PotionEffect> getActivePotionEffects() {
return Arrays.asList();
}
@Override
public boolean hasLineOfSight(Entity other) {
return false;
}
@Override
public boolean getRemoveWhenFarAway() {
return false;
}
@Override
public void setRemoveWhenFarAway(boolean remove) {
}
@Override
public void setCanPickupItems(boolean pickup) {
}
@Override
public boolean getCanPickupItems() {
return false;
}
@Override
public boolean isLeashed() {
return false;
}
@Override
public Entity getLeashHolder() throws IllegalStateException {
throw new IllegalStateException();
}
@Override
public boolean setLeashHolder(Entity holder) {
return false;
}
@Override
public boolean isGliding() {
return false;
}
@Override
public void setGliding(boolean gliding) {
}
@Override
public void setAI(boolean ai) {
}
@Override
public boolean hasAI() {
return false;
}
@Override
public void setCollidable(boolean collidable) {
}
@Override
public boolean isCollidable() {
return false;
}
}

View file

@ -1,105 +0,0 @@
package buttondevteam.discordplugin.playerfaker;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
public class DiscordPlayerInventory extends DiscordInventory implements PlayerInventory {
public DiscordPlayerInventory(DiscordHumanEntity holder) {
super(holder);
}
@Override
public ItemStack[] getArmorContents() {
return new ItemStack[0];
}
@Override
public ItemStack[] getExtraContents() {
return new ItemStack[0];
}
@Override
public ItemStack getHelmet() {
return null;
}
@Override
public ItemStack getChestplate() {
return null;
}
@Override
public ItemStack getLeggings() {
return null;
}
@Override
public ItemStack getBoots() {
return null;
}
@Override
public void setArmorContents(ItemStack[] items) {
}
@Override
public void setExtraContents(ItemStack[] items) {
}
@Override
public void setHelmet(ItemStack helmet) {
}
@Override
public void setChestplate(ItemStack chestplate) {
}
@Override
public void setLeggings(ItemStack leggings) {
}
@Override
public void setBoots(ItemStack boots) {
}
@Override
public ItemStack getItemInMainHand() {
return null;
}
@Override
public void setItemInMainHand(ItemStack item) {
}
@Override
public ItemStack getItemInOffHand() {
return null;
}
@Override
public void setItemInOffHand(ItemStack item) {
}
@Override
public ItemStack getItemInHand() {
return null;
}
@Override
public void setItemInHand(ItemStack stack) {
}
@Override
public int getHeldItemSlot() {
return 0;
}
@Override
public void setHeldItemSlot(int slot) {
}
@Override
public int clear(int id, int data) {
return 0;
}
}

View file

@ -1,103 +0,0 @@
package buttondevteam.discordplugin.playerfaker;
import buttondevteam.discordplugin.DiscordSenderBase;
import buttondevteam.discordplugin.IMCPlayer;
import lombok.Getter;
import lombok.val;
import net.minecraft.server.v1_12_R1.*;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.v1_12_R1.CraftServer;
import org.bukkit.craftbukkit.v1_12_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_12_R1.command.VanillaCommandWrapper;
import org.bukkit.craftbukkit.v1_12_R1.entity.CraftPlayer;
import org.bukkit.entity.Player;
import java.util.Arrays;
public class VanillaCommandListener<T extends DiscordSenderBase & IMCPlayer<T>> implements ICommandListener {
private @Getter T player;
private Player bukkitplayer;
/**
* This constructor will only send raw vanilla messages to the sender in plain text.
*
* @param player
* The Discord sender player (the wrapper)
*/
public VanillaCommandListener(T player) {
this.player = player;
this.bukkitplayer = null;
}
/**
* This constructor will send both raw vanilla messages to the sender in plain text and forward the raw message to the provided player.
*
* @param player
* The Discord sender player (the wrapper)
* @param bukkitplayer
* The Bukkit player to send the raw message to
*/
public VanillaCommandListener(T player, Player bukkitplayer) {
this.player = player;
this.bukkitplayer = bukkitplayer;
if (!(bukkitplayer instanceof CraftPlayer))
throw new ClassCastException("bukkitplayer must be a Bukkit player!");
}
@Override
public MinecraftServer C_() {
return ((CraftServer) Bukkit.getServer()).getServer();
}
@Override
public boolean a(int oplevel, String cmd) {
// return oplevel <= 2; // Value from CommandBlockListenerAbstract, found what it is in EntityPlayer - Wait, that'd always allow OP commands
return oplevel == 0 || player.isOp();
}
@Override
public String getName() {
return player.getName();
}
@Override
public World getWorld() {
return ((CraftWorld) player.getWorld()).getHandle();
}
@Override
public void sendMessage(IChatBaseComponent arg0) {
player.sendMessage(arg0.toPlainText());
if (bukkitplayer != null)
((CraftPlayer) bukkitplayer).getHandle().sendMessage(arg0);
}
public static boolean runBukkitOrVanillaCommand(DiscordSenderBase dsender, String cmdstr) {
val cmd = ((CraftServer) Bukkit.getServer()).getCommandMap().getCommand(cmdstr.split(" ")[0].toLowerCase());
if (!(dsender instanceof Player) || !(cmd instanceof VanillaCommandWrapper))
return Bukkit.dispatchCommand(dsender, cmdstr); // Unconnected users are treated well in vanilla cmds
if (!(dsender instanceof IMCPlayer))
throw new ClassCastException(
"dsender needs to implement IMCPlayer to use vanilla commands as it implements Player.");
IMCPlayer<?> sender = (IMCPlayer<?>) dsender; // Don't use val on recursive interfaces :P
val vcmd = (VanillaCommandWrapper) cmd;
if (!vcmd.testPermission(sender))
return true;
ICommandListener icommandlistener = sender.getVanillaCmdListener();
String[] args = cmdstr.split(" ");
args = Arrays.copyOfRange(args, 1, args.length);
try {
vcmd.dispatchVanillaCommand(sender, icommandlistener, args);
} catch (CommandException commandexception) {
// Taken from CommandHandler
ChatMessage chatmessage = new ChatMessage(commandexception.getMessage(), commandexception.getArgs());
chatmessage.getChatModifier().setColor(EnumChatFormat.RED);
icommandlistener.sendMessage(chatmessage);
}
return true;
}
}

View file

@ -1,83 +0,0 @@
package buttondevteam.discordplugin.role;
import buttondevteam.core.ComponentManager;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.architecture.ConfigData;
import lombok.val;
import org.bukkit.Bukkit;
import sx.blah.discord.handle.impl.events.guild.role.RoleCreateEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleDeleteEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleEvent;
import sx.blah.discord.handle.impl.events.guild.role.RoleUpdateEvent;
import sx.blah.discord.handle.obj.IChannel;
import sx.blah.discord.handle.obj.IRole;
import java.awt.*;
import java.util.List;
import java.util.stream.Collectors;
public class GameRoleModule extends Component<DiscordPlugin> {
public List<String> GameRoles;
@Override
protected void enable() {
getPlugin().getManager().registerCommand(new RoleCommand(this));
GameRoles = DiscordPlugin.mainServer.getRoles().stream().filter(this::isGameRole).map(IRole::getName).collect(Collectors.toList());
}
@Override
protected void disable() {
}
private ConfigData<IChannel> logChannel() {
return DPUtils.channelData(getConfig(), "logChannel", 239519012529111040L);
}
public static void handleRoleEvent(RoleEvent roleEvent) {
val grm = ComponentManager.getIfEnabled(GameRoleModule.class);
if (grm == null) return;
val GameRoles = grm.GameRoles;
val logChannel = grm.logChannel().get();
if (roleEvent instanceof RoleCreateEvent) {
Bukkit.getScheduler().runTaskLaterAsynchronously(DiscordPlugin.plugin, () -> {
if (roleEvent.getRole().isDeleted() || !grm.isGameRole(roleEvent.getRole()))
return; //Deleted or not a game role
GameRoles.add(roleEvent.getRole().getName());
if (logChannel != null)
DiscordPlugin.sendMessageToChannel(logChannel, "Added " + roleEvent.getRole().getName() + " as game role. If you don't want this, change the role's color from the default.");
}, 100);
} else if (roleEvent instanceof RoleDeleteEvent) {
if (GameRoles.remove(roleEvent.getRole().getName()) && logChannel != null)
DiscordPlugin.sendMessageToChannel(logChannel, "Removed " + roleEvent.getRole().getName() + " as a game role.");
} else if (roleEvent instanceof RoleUpdateEvent) {
val event = (RoleUpdateEvent) roleEvent;
if (!grm.isGameRole(event.getNewRole())) {
if (GameRoles.remove(event.getOldRole().getName()) && logChannel != null)
DiscordPlugin.sendMessageToChannel(logChannel, "Removed " + event.getOldRole().getName() + " as a game role because it's color changed.");
} else {
if (GameRoles.contains(event.getOldRole().getName()) && event.getOldRole().getName().equals(event.getNewRole().getName()))
return;
boolean removed = GameRoles.remove(event.getOldRole().getName()); //Regardless of whether it was a game role
GameRoles.add(event.getNewRole().getName()); //Add it because it has no color
if (logChannel != null) {
if (removed)
DiscordPlugin.sendMessageToChannel(logChannel, "Changed game role from " + event.getOldRole().getName() + " to " + event.getNewRole().getName() + ".");
else
DiscordPlugin.sendMessageToChannel(logChannel, "Added " + event.getNewRole().getName() + " as game role because it has the default color.");
}
}
}
}
private boolean isGameRole(IRole r) {
if (r.getGuild().getLongID() != DiscordPlugin.mainServer.getLongID())
return false; //Only allow on the main server
val rc = new Color(149, 165, 166, 0);
return r.getColor().equals(rc)
&& DiscordPlugin.dc.getOurUser().getRolesForGuild(DiscordPlugin.mainServer)
.stream().anyMatch(or -> r.getPosition() < or.getPosition()); //Below one of our roles
}
}

View file

@ -1,84 +0,0 @@
package buttondevteam.discordplugin.role;
import buttondevteam.discordplugin.DPUtils;
import buttondevteam.discordplugin.DiscordPlugin;
import buttondevteam.discordplugin.commands.Command2DCSender;
import buttondevteam.discordplugin.commands.ICommand2DC;
import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass;
import sx.blah.discord.handle.obj.IRole;
import java.util.List;
import java.util.stream.Collectors;
@CommandClass
public class RoleCommand extends ICommand2DC {
private GameRoleModule grm;
RoleCommand(GameRoleModule grm) {
this.grm = grm;
}
@Command2.Subcommand(helpText = {
"Add role",
"This command adds a role to your account."
})
public boolean add(Command2DCSender sender, @Command2.TextArg String rolename) {
final IRole role = checkAndGetRole(sender, rolename);
if (role == null)
return true;
try {
DPUtils.perform(() -> sender.getMessage().getAuthor().addRole(role));
sender.sendMessage("added role.");
} catch (Exception e) {
TBMCCoreAPI.SendException("Error while adding role!", e);
sender.sendMessage("an error occured while adding the role.");
}
return true;
}
@Command2.Subcommand(helpText = {
"Remove role",
"This command removes a role from your account."
})
public boolean remove(Command2DCSender sender, @Command2.TextArg String rolename) {
final IRole role = checkAndGetRole(sender, rolename);
if (role == null)
return true;
try {
DPUtils.perform(() -> sender.getMessage().getAuthor().removeRole(role));
sender.sendMessage("removed role.");
} catch (Exception e) {
TBMCCoreAPI.SendException("Error while removing role!", e);
sender.sendMessage("an error occured while removing the role.");
}
return true;
}
@Command2.Subcommand
public void list(Command2DCSender sender) {
sender.sendMessage("list of roles:\n" + grm.GameRoles.stream().sorted().collect(Collectors.joining("\n")));
}
private IRole checkAndGetRole(Command2DCSender sender, String rolename) {
if (!grm.GameRoles.contains(rolename)) {
sender.sendMessage("that role cannot be found.");
list(sender);
return null;
}
final List<IRole> roles = DiscordPlugin.mainServer.getRolesByName(rolename);
if (roles.size() == 0) {
sender.sendMessage("the specified role cannot be found on Discord! Removing from the list.");
grm.GameRoles.remove(rolename);
return null;
}
if (roles.size() > 1) {
sender.sendMessage("there are multiple roles with this name. Why are there multiple roles with this name?");
return null;
}
return roles.get(0);
}
}

View file

@ -0,0 +1 @@
buttondevteam.discordplugin.playerfaker.DelegatingMockMaker

View file

@ -1,8 +1,18 @@
name: Thorpe-Discord
name: Chroma-Discord
main: buttondevteam.discordplugin.DiscordPlugin
version: 1.0
version: '${noprefix.version}'
author: NorbiPeti
depend: [ThorpeCore]
depend: [ Chroma-Core ]
softdepend:
- Essentials
commands:
discord:
website: 'https://github.com/TBMCPlugins/DiscordPlugin'
description: 'Main command for Chroma-Discord'
website: 'https://github.com/TBMCPlugins/Chroma-Discord'
api-version: '1.13'
#libraries:
# - 'org.slf4j:slf4j-jdk14:1.7.32'
# - 'com.vdurmont:emoji-java:5.1.1'
# - 'org.mockito:mockito-core:4.2.0'
# - 'io.projectreactor:reactor-scala-extensions_2.13:0.8.0'
# - 'org.scala-lang:scala3-library_3:3.1.0'

View file

@ -0,0 +1,6 @@
package buttondevteam.discordplugin
object ChannelconBroadcast extends Enumeration {
type ChannelconBroadcast = Value
val JOINLEAVE, AFK, RESTART, DEATH, BROADCAST = Value
}

View file

@ -0,0 +1,37 @@
package buttondevteam.discordplugin
import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast
import buttondevteam.discordplugin.mcchat.MCChatUtils
import discord4j.core.`object`.entity.Message
import discord4j.core.`object`.entity.channel.MessageChannel
import reactor.core.scala.publisher.SMono
import javax.annotation.Nullable
object ChromaBot {
private var _enabled = false
def enabled = _enabled
private[discordplugin] def enabled_=(en: Boolean): Unit = _enabled = en
/**
* Send a message to the chat channels and private chats.
*
* @param message The message to send, duh (use [[MessageChannel.createMessage]])
*/
def sendMessage(message: SMono[MessageChannel] => SMono[Message]): Unit =
MCChatUtils.forPublicPrivateChat(message).subscribe()
/**
* Send a message to the chat channels, private chats and custom chats.
*
* @param message The message to send, duh
* @param toggle The toggle type for channelcon
*/
def sendMessageCustomAsWell(message: SMono[MessageChannel] => SMono[Message], @Nullable toggle: ChannelconBroadcast): Unit =
MCChatUtils.forCustomAndAllMCChat(message.apply, toggle, hookmsg = false).subscribe()
def updatePlayerList(): Unit =
MCChatUtils.updatePlayerList()
}

View file

@ -0,0 +1,210 @@
package buttondevteam.discordplugin
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.config.IConfigData
import buttondevteam.lib.architecture.{Component, ConfigData, IHaveConfig}
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.channel.{Channel, MessageChannel}
import discord4j.core.`object`.entity.{Guild, Message, Role}
import discord4j.core.spec.EmbedCreateSpec
import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacySpec}
import reactor.core.publisher.{Flux, Mono}
import reactor.core.scala.publisher.{SFlux, SMono}
import java.util
import java.util.Comparator
import java.util.logging.Logger
import java.util.regex.Pattern
import javax.annotation.Nullable
object DPUtils {
private val URL_PATTERN = Pattern.compile("https?://\\S*")
private val FORMAT_PATTERN = Pattern.compile("[*_~]")
def embedWithHead(ecs: EmbedCreateSpec.Builder, displayname: String, playername: String, profileUrl: String): EmbedCreateSpec.Builder =
ecs.author(displayname, profileUrl, s"https://minotar.net/avatar/$playername/32.png")
/**
* Removes §[char] colour codes from strings & escapes them for Discord <br>
* Ensure that this method only gets called once (escaping)
*/
def sanitizeString(string: String): String = escape(sanitizeStringNoEscape(string))
/**
* Removes §[char] colour codes from strings
*/
def sanitizeStringNoEscape(string: String): String = {
val sanitizedString = new StringBuilder
var random = false
var i = 0
while ( {
i < string.length
}) {
if (string.charAt(i) == '§') {
i += 1 // Skips the data value, the 4 in "§4Alisolarflare"
random = string.charAt(i) == 'k'
}
else if (!random) { // Skip random/obfuscated characters
sanitizedString.append(string.charAt(i))
}
i += 1
}
sanitizedString.toString
}
private def escape(message: String) = { //var ts = new TreeSet<>();
val ts = new util.TreeSet[Array[Int]](Comparator.comparingInt((a: Array[Int]) => a(0)): Comparator[Array[Int]]) //Compare the start, then check the end
var matcher = URL_PATTERN.matcher(message)
while (matcher.find) ts.add(Array[Int](matcher.start, matcher.end))
matcher = FORMAT_PATTERN.matcher(message)
val sb = new StringBuffer
while (matcher.find) matcher.appendReplacement(sb, if (Option(ts.floor(Array[Int](matcher.start, 0))).map( //Find a URL start <= our start
(a: Array[Int]) => a(1)).getOrElse(-1) < matcher.start //Check if URL end < our start
) "\\\\" + matcher.group else matcher.group)
matcher.appendTail(sb)
sb.toString
}
def getLogger: Logger = {
if (DiscordPlugin.plugin == null || DiscordPlugin.plugin.getLogger == null) Logger.getLogger("DiscordPlugin")
else DiscordPlugin.plugin.getLogger
}
def channelData(config: IHaveConfig, key: String): IConfigData[SMono[MessageChannel]] =
config.getData(key, id => getMessageChannel(key, Snowflake.of(id.asInstanceOf[Long])),
(_: SMono[MessageChannel]) => 0L, 0L, true) //We can afford to search for the channel in the cache once (instead of using mainServer)
def roleData(config: IHaveConfig, key: String, defName: String): IConfigData[SMono[Role]] =
roleData(config, key, defName, SMono.just(DiscordPlugin.mainServer))
/**
* Needs to be a [[ConfigData]] for checking if it's set
*/
def roleData(config: IHaveConfig, key: String, defName: String, guild: SMono[Guild]): IConfigData[SMono[Role]] = config.getData(key, name => {
if (!name.isInstanceOf[String] || name.asInstanceOf[String].isEmpty) SMono.empty[Role]
else guild.flatMapMany(_.getRoles).filter(_.getName == name).onErrorResume(e => {
getLogger.warning("Failed to get role data for " + key + "=" + name + " - " + e.getMessage)
SMono.empty[Role]
}).next
}, _ => defName, defName, true)
def snowflakeData(config: IHaveConfig, key: String, defID: Long): IConfigData[Snowflake] =
config.getData(key, id => Snowflake.of(id.asInstanceOf[Long]), _.asLong, defID, true)
/**
* Mentions the <b>bot channel</b>. Useful for help texts.
*
* @return The string for mentioning the channel
*/
def botmention: String = {
if (DiscordPlugin.plugin == null) return "#bot"
channelMention(DiscordPlugin.plugin.commandChannel.get)
}
/**
* Disables the component if one of the given configs return null. Useful for channel/role configs.
*
* @param component The component to disable if needed
* @param configs The configs to check for null
* @return Whether the component got disabled and a warning logged
*/
def disableIfConfigError(@Nullable component: Component[DiscordPlugin], configs: IConfigData[_]*): Boolean = {
for (config <- configs) {
val v = config.get
if (disableIfConfigErrorRes(component, config, v)) return true
}
false
}
/**
* Disables the component if one of the given configs return null. Useful for channel/role configs.
*
* @param component The component to disable if needed
* @param config The (snowflake) config to check for null
* @param result The result of getting the value
* @return Whether the component got disabled and a warning logged
*/
def disableIfConfigErrorRes(@Nullable component: Component[DiscordPlugin], config: IConfigData[_], result: Any): Boolean = {
//noinspection ConstantConditions
if (result == null || (result.isInstanceOf[SMono[_]] && !result.asInstanceOf[SMono[_]].hasElement.block())) {
var path: String = null
try {
if (component != null) Component.setComponentEnabled(component, false)
path = config.getPath
} catch {
case e: Exception =>
if (component != null) TBMCCoreAPI.SendException("Failed to disable component after config error!", e, component)
else TBMCCoreAPI.SendException("Failed to disable component after config error!", e, DiscordPlugin.plugin)
}
getLogger.warning("The config value " + path + " isn't set correctly " + (if (component == null) "in global settings!"
else "for component " + component.getClass.getSimpleName + "!"))
getLogger.warning("Set the correct ID in the config" + (if (component == null) ""
else " or disable this component") + " to remove this message.")
return true
}
false
}
/**
* Send a response in the form of "@User, message". Use SMono.empty() if you don't have a channel object.
*
* @param original The original message to reply to
* @param channel The channel to send the message in, defaults to the original
* @param message The message to send
* @return A mono to send the message
*/
@deprecated("Use reply(Message, SMono<MessageChannel>, String) instead", since = "1.1.0")
def reply(original: Message, @Nullable channel: MessageChannel, message: String): SMono[Message] = {
val ch = if (channel == null) original.getChannel.^^()
else SMono.just(channel)
reply(original, ch, message)
}
/**
* @see #reply(Message, MessageChannel, String)
*/
def reply(original: Message, ch: SMono[MessageChannel], message: String): SMono[Message] =
ch.switchIfEmpty(original.getChannel().^^()).flatMap(channel => channel.createMessage((
if (original.getAuthor.isPresent)
original.getAuthor.get.getMention + ", "
else ""
) + message).^^())
def nickMention(userId: Snowflake): String = "<@!" + userId.asString + ">"
def channelMention(channelId: Snowflake): String = "<#" + channelId.asString + ">"
/**
* Gets a message channel for a config. Returns empty for ID 0.
*
* @param key The config key
* @param id The channel ID
* @return A message channel
*/
def getMessageChannel(key: String, id: Snowflake): SMono[MessageChannel] = {
if (id.asLong == 0L) return SMono.empty[MessageChannel]
DiscordPlugin.dc.getChannelById(id).^^().onErrorResume(e => {
getLogger.warning(s"Failed to get channel data for $key=$id - ${e.getMessage}")
SMono.empty[Channel]
}).filter(ch => ch.isInstanceOf[MessageChannel]).cast[MessageChannel]
}
def getMessageChannel(config: IConfigData[Snowflake]): SMono[MessageChannel] =
getMessageChannel(config.getPath, config.get)
def ignoreError[T](mono: SMono[T]): SMono[T] = mono.onErrorResume((_: Throwable) => SMono.empty)
implicit class MonoExtensions[T](mono: Mono[T]) {
def ^^(): SMono[T] = SMono(mono)
}
implicit class FluxExtensions[T](flux: Flux[T]) {
def ^^(): SFlux[T] = SFlux(flux)
}
implicit class SpecExtensions[T <: LegacySpec[_]](spec: T) {
def ^^(): Unit = ()
}
}

View file

@ -0,0 +1,251 @@
package buttondevteam.discordplugin
import buttondevteam.discordplugin.DPUtils.{FluxExtensions, MonoExtensions}
import buttondevteam.discordplugin.DiscordPlugin.dc
import buttondevteam.discordplugin.announcer.AnnouncerModule
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule
import buttondevteam.discordplugin.commands._
import buttondevteam.discordplugin.exceptions.ExceptionListenerModule
import buttondevteam.discordplugin.fun.FunModule
import buttondevteam.discordplugin.listeners.{CommonListeners, MCListener}
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
import buttondevteam.discordplugin.mcchat.sender.{DiscordSenderBase, DiscordUser}
import buttondevteam.discordplugin.mccommands.DiscordMCCommand
import buttondevteam.discordplugin.role.GameRoleModule
import buttondevteam.discordplugin.util.{DPState, Timings}
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture._
import buttondevteam.lib.architecture.config.IConfigData
import buttondevteam.lib.player.ChromaGamerBase
import com.google.common.io.Files
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.{ApplicationInfo, Guild, Role}
import discord4j.core.`object`.presence.{Activity, ClientActivity, ClientPresence, Presence}
import discord4j.core.`object`.reaction.ReactionEmoji
import discord4j.core.event.domain.guild.GuildCreateEvent
import discord4j.core.event.domain.lifecycle.ReadyEvent
import discord4j.core.{DiscordClientBuilder, GatewayDiscordClient}
import discord4j.gateway.ShardInfo
import discord4j.rest.interaction.Interactions
import discord4j.store.jdk.JdkStoreService
import org.bukkit.command.CommandSender
import org.bukkit.configuration.file.YamlConfiguration
import org.bukkit.plugin.PluginDescriptionFile
import org.bukkit.plugin.java.JavaPluginLoader
import org.mockito.internal.util.MockUtil
import reactor.core.Disposable
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.SMono
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.Optional
import scala.jdk.OptionConverters._
@ButtonPlugin.ConfigOpts(disableConfigGen = true) object DiscordPlugin {
private[discordplugin] var dc: GatewayDiscordClient = null
private[discordplugin] var plugin: DiscordPlugin = null
private[discordplugin] var SafeMode = true
def getPrefix: Char = {
if (plugin == null) '/'
else plugin.prefix.get
}
private[discordplugin] var mainServer: Guild = null
private[discordplugin] val DELIVERED_REACTION = ReactionEmoji.unicode("✅")
}
@ButtonPlugin.ConfigOpts(disableConfigGen = true) class DiscordPlugin(loader: JavaPluginLoader, description: PluginDescriptionFile, folder: File, file: File) extends ButtonPlugin(loader, description, folder, file) {
private var _manager: Command2DC = null
def manager: Command2DC = _manager
private var starting = false
/**
* The prefix to use with Discord commands like /role. It only works in the bot channel.
*/
final private val prefix = getIConfig.getData("prefix", '/', (str: Any) => str.asInstanceOf[String].charAt(0), (_: Char).toString)
/**
* The main server where the roles and other information is pulled from. It's automatically set to the first server the bot's invited to.
*/
private def mainServer = getIConfig.getData("mainServer", (id: Any) => { //It attempts to get the default as well
if (id.asInstanceOf[Long] == 0L) Option.empty
else DiscordPlugin.dc.getGuildById(Snowflake.of(id.asInstanceOf[Long])).^^()
.onErrorResume(t => {
getLogger.warning("Failed to get guild: " + t.getMessage)
SMono.empty
}).blockOption()
}, (g: Option[Guild]) => g.map(_.getId.asLong).getOrElse(0L), 0L, true)
/**
* The (bot) channel to use for Discord commands like /role.
*/
def commandChannel: IConfigData[Snowflake] = DPUtils.snowflakeData(getIConfig, "commandChannel", 0L)
/**
* The role that allows using mod-only Discord commands.
* If empty (&#39;&#39;), then it will only allow for the owner.
*/
def modRole: IConfigData[SMono[Role]] = DPUtils.roleData(getIConfig, "modRole", "Moderator")
/**
* The invite link to show by /discord invite. If empty, it defaults to the first invite if the bot has access.
*/
def inviteLink: IConfigData[String] = getIConfig.getData("inviteLink", "")
override def onLoad(): Unit = { //Needed by ServerWatcher
val thread = Thread.currentThread
val cl = thread.getContextClassLoader
thread.setContextClassLoader(getClassLoader)
MockUtil.isMock(null) //Load MockUtil to load Mockito plugins
thread.setContextClassLoader(cl)
getLogger.info("Load complete")
}
override def pluginEnable(): Unit = try {
getLogger.info("Initializing...")
DiscordPlugin.plugin = this
_manager = new Command2DC
registerCommand(new DiscordMCCommand) //Register so that the restart command works
var token: String = null
val tokenFile = new File("TBMC", "Token.txt")
if (tokenFile.exists) { //Legacy support
//noinspection UnstableApiUsage
token = Files.readFirstLine(tokenFile, StandardCharsets.UTF_8)
}
else {
val privateFile = new File(getDataFolder, "private.yml")
val conf = YamlConfiguration.loadConfiguration(privateFile)
token = conf.getString("token")
if (token == null || token.equalsIgnoreCase("Token goes here")) {
conf.set("token", "Token goes here")
conf.save(privateFile)
getLogger.severe("Token not found! Please set it in private.yml then do /discord restart")
getLogger.severe("You need to have a bot account to use with your server.")
getLogger.severe("If you don't have one, go to https://discordapp.com/developers/applications/ and create an application, then create a bot for it and copy the bot token.")
return ()
}
}
starting = true
val cb = DiscordClientBuilder.create(token).build.gateway
cb.setInitialPresence((si: ShardInfo) => ClientPresence.doNotDisturb(ClientActivity.playing("booting")))
cb.login.doOnError((t: Throwable) => {
def foo(t: Throwable): Unit = {
stopStarting()
}
foo(t)
}).subscribe((dc: GatewayDiscordClient) => {
DiscordPlugin.dc = dc //Set to gateway client
dc.on(classOf[ReadyEvent]).^^().map(_.getGuilds.size).flatMap(dc.on(classOf[GuildCreateEvent]).take(_).collectList)
.doOnError(_ => stopStarting()).subscribe(this.handleReady _) // Take all received GuildCreateEvents and make it a List
()
}) /* All guilds have been received, client is fully connected */
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("Failed to enable the Discord plugin!", e, this)
getLogger.severe("You may be able to restart the plugin using /discord restart")
stopStarting()
}
private def stopStarting(): Unit = {
this synchronized {
starting = false
notifyAll()
}
}
private def handleReady(event: java.util.List[GuildCreateEvent]): Unit = { //System.out.println("Got ready event");
try {
if (DiscordPlugin.mainServer != null) { //This is not the first ready event
getLogger.info("Ready event already handled") //TODO: It should probably handle disconnections
DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("Minecraft"))).subscribe //Update from the initial presence
return ()
}
DiscordPlugin.mainServer = mainServer.get.orNull //Shouldn't change afterwards
if (DiscordPlugin.mainServer == null) {
if (event.size == 0) {
getLogger.severe("Main server not found! Invite the bot and do /discord restart")
DiscordPlugin.dc.getApplicationInfo.subscribe((info: ApplicationInfo) => getLogger.severe("Click here: https://discordapp.com/oauth2/authorize?client_id=" + info.getId.asString + "&scope=bot&permissions=268509264"))
saveConfig() //Put default there
return () //We should have all guilds by now, no need to retry
}
DiscordPlugin.mainServer = event.get(0).getGuild
getLogger.warning("Main server set to first one: " + DiscordPlugin.mainServer.getName)
mainServer.set(Option(DiscordPlugin.mainServer)) //Save in config
}
DiscordPlugin.SafeMode = false
DPUtils.disableIfConfigErrorRes(null, commandChannel, DPUtils.getMessageChannel(commandChannel))
//Won't disable, just prints the warning here
if (MinecraftChatModule.state eq DPState.STOPPING_SERVER) {
stopStarting()
return () //Reusing that field to check if stopping while still initializing
}
CommonListeners.register(DiscordPlugin.dc.getEventDispatcher)
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener, this)
TBMCCoreAPI.RegisterUserClass(classOf[DiscordUser], () => new DiscordUser)
ChromaGamerBase.addConverter {
case dsender: DiscordSenderBase => Some(dsender.getChromaUser).toJava
case _ => None.toJava
}
ChromaBot.enabled = true //Initialize ChromaBot
Component.registerComponent(this, new GeneralEventBroadcasterModule)
Component.registerComponent(this, new MinecraftChatModule)
Component.registerComponent(this, new ExceptionListenerModule)
Component.registerComponent(this, new GameRoleModule) //Needs the mainServer to be set
Component.registerComponent(this, new AnnouncerModule)
Component.registerComponent(this, new FunModule)
ChromaBot.updatePlayerList() //The MCChatModule is tested to be enabled
val applicationId = dc.getRestClient.getApplicationId.block()
val guildId = Some(DiscordPlugin.mainServer.getId.asLong())
manager.registerCommand(new VersionCommand, applicationId, guildId)
manager.registerCommand(new UserinfoCommand, applicationId, guildId)
manager.registerCommand(new HelpCommand, applicationId, guildId)
manager.registerCommand(new DebugCommand, applicationId, guildId)
manager.registerCommand(new ConnectCommand, applicationId, guildId)
TBMCCoreAPI.SendUnsentExceptions()
TBMCCoreAPI.SendUnsentDebugMessages()
// TODO: Removed log watcher (responsible for detecting restarts)
if (!TBMCCoreAPI.IsTestServer) DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("Minecraft"))).subscribe()
else DiscordPlugin.dc.updatePresence(ClientPresence.online(ClientActivity.playing("testing"))).subscribe()
getLogger.info("Loaded!")
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("An error occurred while enabling DiscordPlugin!", e, this)
}
stopStarting()
}
override def pluginPreDisable(): Unit = {
if (MinecraftChatModule.state eq DPState.RUNNING) MinecraftChatModule.state = DPState.STOPPING_SERVER
this synchronized {
if (starting) try wait(10000)
catch {
case e: InterruptedException =>
e.printStackTrace()
}
}
if (!ChromaBot.enabled) return () //Failed to load
val timings = new Timings
timings.printElapsed("Disable start")
timings.printElapsed("Updating player list")
ChromaBot.updatePlayerList()
timings.printElapsed("Done")
}
override def pluginDisable(): Unit = {
val timings = new Timings
timings.printElapsed("Actual disable start (logout)")
if (!ChromaBot.enabled) return ()
try {
DiscordPlugin.SafeMode = true // Stop interacting with Discord
ChromaBot.enabled = false
timings.printElapsed("Logging out...")
DiscordPlugin.dc.logout.block
DiscordPlugin.mainServer = null //Allow ReadyEvent again
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("An error occured while disabling DiscordPlugin!", e, this)
}
}
}

View file

@ -0,0 +1,107 @@
package buttondevteam.discordplugin.announcer
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin.mcchat.sender.DiscordUser
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.{Component, ComponentMetadata}
import buttondevteam.lib.player.ChromaGamerBase
import com.google.gson.JsonParser
import discord4j.core.`object`.entity.channel.MessageChannel
import reactor.core.scala.publisher.SMono
import scala.annotation.tailrec
import scala.collection.mutable
/**
* Posts new posts from Reddit to the specified channel(s). It will pin the regular posts (not the mod posts).
*/
@ComponentMetadata(enabledByDefault = false) object AnnouncerModule {
private var stop = false
}
@ComponentMetadata(enabledByDefault = false) class AnnouncerModule extends Component[DiscordPlugin] {
/**
* Channel to post new posts.
*/
final def channel = DPUtils.channelData(getConfig, "channel")
/**
* Channel where distinguished (moderator) posts go.
*/
final private def modChannel = DPUtils.channelData(getConfig, "modChannel")
/**
* Automatically unpins all messages except the last few. Set to 0 or >50 to disable
*/
final private def keepPinned = getConfig.getData("keepPinned", 40.toShort)
final private def lastAnnouncementTime = getConfig.getData("lastAnnouncementTime", 0L)
final private def lastSeenTime = getConfig.getData("lastSeenTime", 0L)
/**
* The subreddit to pull the posts from
*/
final private def subredditURL = getConfig.getData("subredditURL", "https://www.reddit.com/r/ChromaGamers")
override protected def enable(): Unit = {
if (DPUtils.disableIfConfigError(this, channel, modChannel)) return ()
AnnouncerModule.stop = false //If not the first time
val kp = keepPinned.get
if (kp <= 0) return ()
val msgs = channel.get.flatMapMany(_.getPinnedMessages).takeLast(kp)
msgs.subscribe(_.unpin)
new Thread(() => this.AnnouncementGetterThreadMethod()).start()
}
override protected def disable(): Unit = AnnouncerModule.stop = true
@tailrec
private def AnnouncementGetterThreadMethod(): Unit = {
if (AnnouncerModule.stop) return ()
if (isEnabled) try { //If not enabled, just wait
val body = TBMCCoreAPI.DownloadString(subredditURL.get + "/new/.json?limit=10")
val json = new JsonParser().parse(body).getAsJsonObject.get("data").getAsJsonObject.get("children").getAsJsonArray
val msgsb = new mutable.StringBuilder
val modmsgsb = new mutable.StringBuilder
var lastanntime = lastAnnouncementTime.get
for (i <- json.size - 1 to 0 by -1) {
val item = json.get(i).getAsJsonObject
val data = item.get("data").getAsJsonObject
var author = data.get("author").getAsString
val distinguishedjson = data.get("distinguished")
val distinguished = if (distinguishedjson.isJsonNull) null else distinguishedjson.getAsString
val permalink = "https://www.reddit.com" + data.get("permalink").getAsString
val date = data.get("created_utc").getAsLong
if (date > lastSeenTime.get) lastSeenTime.set(date)
else if (date > lastAnnouncementTime.get) { //noinspection ConstantConditions
{
val reddituserclass = ChromaGamerBase.getTypeForFolder("reddit")
if (reddituserclass != null) {
val user = ChromaGamerBase.getUser(author, reddituserclass)
val id = user.getConnectedID(classOf[DiscordUser])
if (id != null) author = "<@" + id + ">"
}
}
if (!author.startsWith("<")) author = "/u/" + author
(if (distinguished != null && distinguished == "moderator") modmsgsb else msgsb)
.append("A new post was submitted to the subreddit by ").append(author).append("\n")
.append(permalink).append("\n")
lastanntime = date
}
}
def sendMsg(ch: SMono[MessageChannel], msg: String) =
ch.flatMap(c => c.createMessage(msg).^^()).flatMap(_.pin.^^()).subscribe()
if (msgsb.nonEmpty) sendMsg(channel.get(), msgsb.toString())
if (modmsgsb.nonEmpty) sendMsg(modChannel.get(), modmsgsb.toString())
if (lastAnnouncementTime.get != lastanntime) lastAnnouncementTime.set(lastanntime) // If sending succeeded
} catch {
case e: Exception =>
e.printStackTrace()
}
try Thread.sleep(10000)
catch {
case ex: InterruptedException =>
Thread.currentThread.interrupt()
}
AnnouncementGetterThreadMethod()
}
}

View file

@ -0,0 +1,34 @@
package buttondevteam.discordplugin.broadcaster
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.{Component, ComponentMetadata}
/**
* Uses a bit of a hacky method of getting all broadcasted messages, including advancements and any other message that's for everyone.
* If this component is enabled then these messages will show up on Discord.
*/
@ComponentMetadata(enabledByDefault = false) object GeneralEventBroadcasterModule {
def isHooked: Boolean = GeneralEventBroadcasterModule.hooked
private var hooked = false
}
@ComponentMetadata(enabledByDefault = false) class GeneralEventBroadcasterModule extends Component[DiscordPlugin] {
override protected def enable(): Unit = try {
// TODO: Removed for now
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("Error while hacking the player list! Disable this module if you're on an incompatible version.", e, this)
case _: NoClassDefFoundError =>
logWarn("Error while hacking the player list! Disable this module if you're on an incompatible version.")
}
override protected def disable(): Unit = try {
if (!GeneralEventBroadcasterModule.hooked) return ()
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("Error while hacking the player list!", e, this)
case _: NoClassDefFoundError =>
}
}

View file

@ -0,0 +1,42 @@
package buttondevteam.discordplugin.commands
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.lib.chat.Command2
import buttondevteam.lib.chat.commands.SubcommandData
import discord4j.common.util.Snowflake
import discord4j.core.`object`.command.ApplicationCommandOption
import discord4j.discordjson.json.{ApplicationCommandOptionData, ApplicationCommandRequest}
import java.lang.reflect.Method
class Command2DC extends Command2[ICommand2DC, Command2DCSender]('/', false) {
override def registerCommand(command: ICommand2DC): Unit = {
registerCommand(command, DiscordPlugin.dc.getApplicationInfo.block().getId.asLong())
}
def registerCommand(command: ICommand2DC, appId: Long, guildId: Option[Long] = None): Unit = {
val mainNode = super.registerCommandSuper(command) //Needs to be configurable for the helps
println("Main node name is " + mainNode.getName)
// TODO: Go through all subcommands and register them
val greetCmdRequest = ApplicationCommandRequest.builder()
.name(mainNode.getName)
.description("Connect your Minecraft account.") //TODO: Description
.addOption(ApplicationCommandOptionData.builder()
.name("name")
.description("Your name")
.`type`(ApplicationCommandOption.Type.STRING.getValue)
.required(true)
.build()
).build()
val service = DiscordPlugin.dc.getRestClient.getApplicationService
guildId match {
case Some(id) => service.createGuildApplicationCommand(appId, id, greetCmdRequest).subscribe()
case None => service.createGlobalApplicationCommand(appId, greetCmdRequest).subscribe()
}
}
override def hasPermission(sender: Command2DCSender, data: SubcommandData[ICommand2DC, Command2DCSender]): Boolean = {
//return !command.isModOnly() || sender.getMessage().getAuthor().hasRole(DiscordPlugin.plugin.modRole().get()); //TODO: modRole may be null; more customisable way?
true
}
}

View file

@ -0,0 +1,24 @@
package buttondevteam.discordplugin.commands
import buttondevteam.discordplugin.DPUtils
import buttondevteam.lib.chat.Command2Sender
import discord4j.core.`object`.entity.channel.MessageChannel
import discord4j.core.`object`.entity.{Member, Message, User}
import discord4j.core.event.domain.interaction.ChatInputInteractionEvent
import java.util.Optional
import scala.jdk.OptionConverters._
class Command2DCSender(val event: ChatInputInteractionEvent) extends Command2Sender {
val authorAsMember: Option[Member] = event.getInteraction.getMember.toScala
val author: User = event.getInteraction.getUser
override def sendMessage(message: String): Unit = {
if (message.isEmpty) return ()
//Some(message) map DPUtils.sanitizeString map { (msg: String) => Character.toLowerCase(msg.charAt(0)) + msg.substring(1) } foreach event.reply - don't even need this
event.reply(message)
}
override def sendMessage(message: Array[String]): Unit = sendMessage(String.join("\n", message: _*))
override def getName: String = authorAsMember.flatMap(_.getNickname.toScala).getOrElse(author.getUsername)
}

View file

@ -0,0 +1,47 @@
package buttondevteam.discordplugin.commands
import buttondevteam.discordplugin.mcchat.sender.DiscordUser
import buttondevteam.lib.chat.{Command2, CommandClass}
import buttondevteam.lib.player.{TBMCPlayer, TBMCPlayerBase}
import com.google.common.collect.HashBiMap
import org.bukkit.Bukkit
import org.bukkit.entity.Player
@CommandClass(helpText = Array("Connect command", //
"This command lets you connect your account with a Minecraft account." +
" This allows using the private Minecraft chat and other things.")) object ConnectCommand {
/**
* Key: Minecraft name<br>
* Value: Discord ID
*/
var WaitingToConnect: HashBiMap[String, String] = HashBiMap.create
}
@CommandClass(helpText = Array("Connect command",
"This command lets you connect your account with a Minecraft account." +
" This allows using the private Minecraft chat and other things.")) class ConnectCommand extends ICommand2DC {
@Command2.Subcommand def `def`(sender: Command2DCSender, Minecraftname: String): Boolean = {
val author = sender.event.getInteraction.getUser
println("Author is " + author.getUsername)
if (ConnectCommand.WaitingToConnect.inverse.containsKey(author.getId.asString)) {
sender.event.reply("Replacing " + ConnectCommand.WaitingToConnect.inverse.get(author.getId.asString) + " with " + Minecraftname).subscribe()
ConnectCommand.WaitingToConnect.inverse.remove(author.getId.asString)
}
//noinspection ScalaDeprecation
val p = Bukkit.getOfflinePlayer(Minecraftname)
if (p == null) {
sender.event.reply("The specified Minecraft player cannot be found").subscribe()
return true
}
val pl = TBMCPlayerBase.getPlayer(p.getUniqueId, classOf[TBMCPlayer])
val dp = pl.getAs(classOf[DiscordUser])
if (dp != null && author.getId.asString == dp.getDiscordID) {
sender.event.reply("You already have this account connected.").subscribe()
return true
}
ConnectCommand.WaitingToConnect.put(p.getName, author.getId.asString)
sender.event.reply("Alright! Now accept the connection in Minecraft from the account " + Minecraftname + " before the next server restart. You can also adjust the Minecraft name you want to connect to by running this command again.").subscribe()
if (p.isOnline) p.asInstanceOf[Player].sendMessage("§bTo connect with the Discord account " + author.getUsername + "#" + author.getDiscriminator + " do /discord accept")
true
}
}

View file

@ -0,0 +1,31 @@
package buttondevteam.discordplugin.commands
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.discordplugin.listeners.CommonListeners
import buttondevteam.lib.chat.{Command2, CommandClass}
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.{Member, User}
import reactor.core.scala.publisher.SMono
@CommandClass(helpText = Array("Switches debug mode."))
class DebugCommand extends ICommand2DC {
@Command2.Subcommand
override def `def`(sender: Command2DCSender): Boolean = {
SMono.justOrEmpty(sender.authorAsMember.orNull)
.switchIfEmpty(Option(sender.author) //Support DMs
.map((u: User) => u.asMember(DiscordPlugin.mainServer.getId).^^()).getOrElse(SMono.empty[Member]))
.flatMap((m: Member) => DiscordPlugin.plugin.modRole.get
.map(mr => m.getRoleIds.stream.anyMatch((r: Snowflake) => r == mr.getId))
.switchIfEmpty(SMono.fromCallable(() => DiscordPlugin.mainServer.getOwnerId.asLong == m.getId.asLong)))
.onErrorResume(_ => SMono.just(false)) //Role not found
.subscribe(success => {
if (success) {
CommonListeners.debug = !CommonListeners.debug
sender.sendMessage("debug " + (if (CommonListeners.debug) "enabled" else "disabled"))
} else
sender.sendMessage("you need to be a moderator to use this command.")
})
true
}
}

View file

@ -0,0 +1,18 @@
package buttondevteam.discordplugin.commands
import buttondevteam.lib.chat.{Command2, CommandClass}
@CommandClass(helpText = Array("Help command", //
"Shows some info about a command or lists the available commands."))
class HelpCommand extends ICommand2DC {
@Command2.Subcommand
def `def`(sender: Command2DCSender, @Command2.TextArg @Command2.OptionalArg command: String): Boolean = {
if (command == null || command.isEmpty) sender.sendMessage(getManager.getCommandsText)
else {
val ht = getManager.getCommandNode(command).getData.getHelpText(sender)
if (ht == null) sender.sendMessage("Command not found: " + command)
else sender.sendMessage(ht)
}
true
}
}

View file

@ -0,0 +1,16 @@
package buttondevteam.discordplugin.commands
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.lib.chat.{CommandClass, ICommand2}
abstract class ICommand2DC() extends ICommand2[Command2DCSender](DiscordPlugin.plugin.manager) {
final private var modOnly = false
{
val ann: CommandClass = getClass.getAnnotation(classOf[CommandClass])
if (ann == null) modOnly = false
else modOnly = ann.modOnly
}
def isModOnly: Boolean = this.modOnly
}

View file

@ -0,0 +1,42 @@
package buttondevteam.discordplugin.commands
import buttondevteam.discordplugin.DPUtils.FluxExtensions
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.discordplugin.mcchat.sender.DiscordUser
import buttondevteam.lib.chat.{Command2, CommandClass}
import buttondevteam.lib.player.ChromaGamerBase
import buttondevteam.lib.player.ChromaGamerBase.InfoTarget
import discord4j.core.`object`.entity.{Member, Message, User}
import scala.jdk.CollectionConverters.ListHasAsScala
@CommandClass(helpText = Array("User information", //
"Shows some information about users, from Discord, from Minecraft or from Reddit if they have these accounts connected.",
"If used without args, shows your info."))
class UserinfoCommand extends ICommand2DC {
@Command2.Subcommand
def `def`(sender: Command2DCSender, @Command2.OptionalArg @Command2.TextArg user: String): Boolean = {
var target: User = null
if (user == null || user.isEmpty) target = sender.author
else { // TODO: Mention option
}
if (target == null) {
sender.sendMessage("An error occurred.")
return true
}
val dp = ChromaGamerBase.getUser(target.getId.asString, classOf[DiscordUser])
val uinfo = new StringBuilder("User info for ").append(target.getUsername).append(":\n")
uinfo.append(dp.getInfo(InfoTarget.Discord))
sender.sendMessage(uinfo.toString)
true
}
private def getUsers(message: Message, args: String) = {
val guild = message.getGuild.block
if (guild == null) { //Private channel
DiscordPlugin.dc.getUsers.^^().filter(u => u.getUsername.equalsIgnoreCase(args)).collectSeq().block()
}
else
guild.getMembers.^^().filter(_.getUsername.equalsIgnoreCase(args)).map(_.asInstanceOf[User]).collectSeq().block()
}
}

View file

@ -0,0 +1,20 @@
package buttondevteam.discordplugin.commands
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.lib.chat.{Command2, CommandClass}
@CommandClass(helpText = Array("Version", "Returns the plugin's version"))
object VersionCommand {
def getVersion: Array[String] = {
val desc = DiscordPlugin.plugin.getDescription
Array[String](desc.getFullName, desc.getWebsite)
}
}
@CommandClass(helpText = Array("Version", "Returns the plugin's version"))
class VersionCommand extends ICommand2DC {
@Command2.Subcommand override def `def`(sender: Command2DCSender): Boolean = {
sender.sendMessage(VersionCommand.getVersion)
true
}
}

View file

@ -0,0 +1,33 @@
package buttondevteam.discordplugin.exceptions
import buttondevteam.core.ComponentManager
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.lib.TBMCDebugMessageEvent
import discord4j.core.`object`.entity.channel.MessageChannel
import org.bukkit.event.{EventHandler, Listener}
object DebugMessageListener {
private def SendMessage(message: String): Unit = {
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(classOf[ExceptionListenerModule])) return ()
try {
val mc = ExceptionListenerModule.getChannel
if (mc == null) return ()
val sb = new StringBuilder
sb.append("```").append("\n")
sb.append(if (message.length > 2000) message.substring(0, 2000) else message).append("\n")
sb.append("```")
mc.flatMap((ch: MessageChannel) => ch.createMessage(sb.toString).^^()).subscribe()
} catch {
case ex: Exception =>
ex.printStackTrace()
}
}
}
class DebugMessageListener extends Listener {
@EventHandler def onDebugMessage(e: TBMCDebugMessageEvent): Unit = {
DebugMessageListener.SendMessage(e.getDebugMessage)
e.setSent()
}
}

View file

@ -0,0 +1,93 @@
package buttondevteam.discordplugin.exceptions
import buttondevteam.core.ComponentManager
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
import buttondevteam.lib.architecture.Component
import buttondevteam.lib.{TBMCCoreAPI, TBMCExceptionEvent}
import discord4j.core.`object`.entity.Guild
import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel}
import org.apache.commons.lang.exception.ExceptionUtils
import org.bukkit.Bukkit
import org.bukkit.event.{EventHandler, Listener}
import reactor.core.scala.publisher.SMono
import java.util
import java.util.stream.Collectors
/**
* Listens for errors from the Chroma plugins and posts them to Discord, ignoring repeating errors so it's not that spammy.
*/
object ExceptionListenerModule {
private def SendException(e: Throwable, sourcemessage: String): Unit = {
if (instance == null) return ()
try getChannel.flatMap(channel => {
val coderRole = channel match {
case ch: GuildChannel => instance.pingRole(ch.getGuild.^^()).get
case _ => SMono.empty
}
coderRole.map(role => if (TBMCCoreAPI.IsTestServer) new StringBuilder else new StringBuilder(role.getMention).append("\n")) // Ping if prod server
.defaultIfEmpty(new StringBuilder).flatMap(sb => {
sb.append(sourcemessage).append("\n")
sb.append("```").append("\n")
var stackTrace = util.Arrays.stream(ExceptionUtils.getStackTrace(e).split("\\n"))
.filter(s => !s.contains("\tat ") || s.contains("buttondevteam."))
.collect(Collectors.joining("\n"))
if (sb.length + stackTrace.length >= 1980) stackTrace = stackTrace.substring(0, 1980 - sb.length)
sb.append(stackTrace).append("\n")
sb.append("```")
channel.createMessage(sb.toString).^^()
})
}).subscribe()
catch {
case ex: Exception =>
ex.printStackTrace()
}
}
private var instance: ExceptionListenerModule = null
def getChannel: SMono[MessageChannel] = {
if (instance != null) return instance.channel.get
SMono.empty
}
}
class ExceptionListenerModule extends Component[DiscordPlugin] with Listener {
final private val lastthrown = new util.ArrayList[Throwable]
final private val lastsourcemsg = new util.ArrayList[String]
@EventHandler def onException(e: TBMCExceptionEvent): Unit = {
if (DiscordPlugin.SafeMode || !ComponentManager.isEnabled(getClass)) return ()
if (lastthrown.stream.anyMatch(ex => e.getException.getStackTrace.sameElements(ex.getStackTrace)
&& (if (e.getException.getMessage == null) ex.getMessage == null else e.getException.getMessage == ex.getMessage))
&& lastsourcemsg.contains(e.getSourceMessage)) {
return ()
}
ExceptionListenerModule.SendException(e.getException, e.getSourceMessage)
if (lastthrown.size >= 10) lastthrown.remove(0)
if (lastsourcemsg.size >= 10) lastsourcemsg.remove(0)
lastthrown.add(e.getException)
lastsourcemsg.add(e.getSourceMessage)
e.setHandled(true)
}
/**
* The channel to post the errors to.
*/
final private def channel = DPUtils.channelData(getConfig, "channel")
/**
* The role to ping if an error occurs. Set to empty ('') to disable.
*/
private def pingRole(guild: SMono[Guild]) = DPUtils.roleData(getConfig, "pingRole", "Coder", guild)
override protected def enable(): Unit = {
if (DPUtils.disableIfConfigError(this, channel)) return ()
ExceptionListenerModule.instance = this
Bukkit.getPluginManager.registerEvents(new ExceptionListenerModule, getPlugin)
TBMCCoreAPI.RegisterEventsForExceptions(new DebugMessageListener, getPlugin)
}
override protected def disable(): Unit = ExceptionListenerModule.instance = null
}

View file

@ -0,0 +1,142 @@
package buttondevteam.discordplugin.fun
import buttondevteam.core.ComponentManager
import buttondevteam.discordplugin.DPUtils.{FluxExtensions, MonoExtensions}
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.config.IConfigData
import buttondevteam.lib.architecture.{Component, ConfigData}
import com.google.common.collect.Lists
import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel}
import discord4j.core.`object`.entity.{Guild, Message}
import discord4j.core.`object`.presence.Status
import discord4j.core.event.domain.PresenceUpdateEvent
import discord4j.core.spec.{EmbedCreateSpec, MessageCreateSpec}
import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacyMessageCreateSpec}
import org.bukkit.Bukkit
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.{EventHandler, Listener}
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.SMono
import java.util
import java.util.Calendar
import java.util.concurrent.TimeUnit
import java.util.stream.IntStream
import scala.util.Random
/**
* All kinds of random things.
* The YEEHAW event uses an emoji named :YEEHAW: if available
*/
object FunModule {
private val serverReadyStrings = Array[String]("in one week from now", // Ali
"between now and the heat-death of the universe.", // Ghostise
"soon™", "ask again this time next month", "in about 3 seconds", // Nicolai
"after we finish 8 plugins", "tomorrow.", "after one tiiiny feature",
"next commit", "after we finish strangling Towny", "when we kill every *fucking* bug",
"once the server stops screaming.", "after HL3 comes out", "next time you ask",
"when will *you* be open?") // Ali
private val serverReadyRandom = new Random
private val usableServerReadyStrings = new java.util.ArrayList[Short](0)
private var lastlist = 0
private var lastlistp = 0
private var ListC = 0
def executeMemes(message: Message): Boolean = {
val fm = ComponentManager.getIfEnabled(classOf[FunModule])
if (fm == null) return false
val msglowercased = message.getContent.toLowerCase
lastlist += 1
if (lastlist > 5) {
ListC = 0
lastlist = 0
}
if (msglowercased == "/list" && Bukkit.getOnlinePlayers.size == lastlistp && {
ListC += 1
ListC - 1
} > 2) { // Lowered already
DPUtils.reply(message, SMono.empty, "stop it. You know the answer.").subscribe()
lastlist = 0
lastlistp = Bukkit.getOnlinePlayers.size.toShort
return true //Handled
}
lastlistp = Bukkit.getOnlinePlayers.size.toShort //Didn't handle
if (!TBMCCoreAPI.IsTestServer && fm.serverReady.get.exists(msglowercased.contains)) {
var next = 0
if (usableServerReadyStrings.size == 0) fm.createUsableServerReadyStrings()
next = usableServerReadyStrings.remove(serverReadyRandom.nextInt(usableServerReadyStrings.size))
DPUtils.reply(message, SMono.empty, fm.serverReadyAnswers.get.get(next)).subscribe()
return false //Still process it as a command/mcchat if needed
}
false
}
private var lasttime: Long = 0
def handleFullHouse(event: PresenceUpdateEvent): Unit = {
val fm = ComponentManager.getIfEnabled(classOf[FunModule])
if (fm == null) return ()
if (Calendar.getInstance.get(Calendar.DAY_OF_MONTH) % 5 != 0) return ()
if (!Option(event.getOld.orElse(null)).exists(_.getStatus == Status.OFFLINE)
|| event.getCurrent.getStatus == Status.OFFLINE)
return () //If it's not an offline -> online change
fm.fullHouseChannel.get.filter((ch: MessageChannel) => ch.isInstanceOf[GuildChannel])
.flatMap(channel => fm.fullHouseDevRole(channel.asInstanceOf[GuildChannel].getGuild.^^()).get
.filterWhen(devrole => event.getMember.^^()
.flatMap(m => m.getRoles.^^().any(_.getId.asLong == devrole.getId.asLong)))
.filterWhen(devrole => event.getGuild.^^()
.flatMapMany(g => g.getMembers.filter(_.getRoleIds.stream.anyMatch(_ == devrole.getId)))
.flatMap(_.getPresence).all(_.getStatus != Status.OFFLINE))
.filter(_ => lasttime + 10 < TimeUnit.NANOSECONDS.toHours(System.nanoTime)) //This should stay so it checks this last
.flatMap(_ => {
lasttime = TimeUnit.NANOSECONDS.toHours(System.nanoTime)
channel.createMessage(MessageCreateSpec.builder().content("Full house!")
.addEmbed(EmbedCreateSpec.builder()
.image("https://cdn.discordapp.com/attachments/249295547263877121/249687682618359808/poker-hand-full-house-aces-kings-playing-cards-15553791.png")
.build())
.build()).^^()
})).subscribe()
}
}
class FunModule extends Component[DiscordPlugin] with Listener {
/**
* Questions that the bot will choose a random answer to give to.
*/
final private def serverReady: IConfigData[Array[String]] =
getConfig.getData("serverReady", Array[String](
"when will the server be open", "when will the server be ready",
"when will the server be done", "when will the server be complete",
"when will the server be finished", "when's the server ready",
"when's the server open", "vhen vill ze server be open?"))
/**
* Answers for a recognized question. Selected randomly.
*/
final private def serverReadyAnswers: IConfigData[util.ArrayList[String]] =
getConfig.getData("serverReadyAnswers", Lists.newArrayList(FunModule.serverReadyStrings: _*))
private def createUsableServerReadyStrings(): Unit =
IntStream.range(0, serverReadyAnswers.get.size).forEach((i: Int) => FunModule.usableServerReadyStrings.add(i.toShort))
override protected def enable(): Unit = registerListener(this)
override protected def disable(): Unit = {
FunModule.lastlist = 0
FunModule.lastlistp = 0
FunModule.ListC = 0
}
@EventHandler def onPlayerJoin(event: PlayerJoinEvent): Unit = FunModule.ListC = 0
/**
* If all of the people who have this role are online, the bot will post a full house.
*/
private def fullHouseDevRole(guild: SMono[Guild]) = DPUtils.roleData(getConfig, "fullHouseDevRole", "Developer", guild)
/**
* The channel to post the full house to.
*/
final private def fullHouseChannel = DPUtils.channelData(getConfig, "fullHouseChannel")
}

View file

@ -0,0 +1,56 @@
package buttondevteam.discordplugin.listeners
import buttondevteam.discordplugin.DPUtils.FluxExtensions
import buttondevteam.discordplugin.commands.{Command2DCSender, ConnectCommand}
import buttondevteam.discordplugin.fun.FunModule
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
import buttondevteam.discordplugin.role.GameRoleModule
import buttondevteam.discordplugin.util.Timings
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.architecture.Component
import discord4j.core.`object`.entity.Message
import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel}
import discord4j.core.event.EventDispatcher
import discord4j.core.event.domain.PresenceUpdateEvent
import discord4j.core.event.domain.interaction.{ChatInputInteractionEvent, MessageInteractionEvent}
import discord4j.core.event.domain.message.MessageCreateEvent
import discord4j.core.event.domain.role.{RoleCreateEvent, RoleDeleteEvent, RoleUpdateEvent}
import reactor.core.Disposable
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.SMono
object CommonListeners {
val timings = new Timings
def register(dispatcher: EventDispatcher): Unit = {
dispatcher.on(classOf[MessageCreateEvent]).flatMap((event: MessageCreateEvent) => {
SMono.just(event.getMessage).filter(_ => !DiscordPlugin.SafeMode)
.filter(message => message.getAuthor.filter(!_.isBot).isPresent)
.filter(message => !FunModule.executeMemes(message))
.filterWhen(message => {
Option(Component.getComponents.get(classOf[MinecraftChatModule])).filter(_.isEnabled)
.map(_.asInstanceOf[MinecraftChatModule].getListener.handleDiscord(event))
.getOrElse(SMono.just(true)) //Wasn't handled, continue
})
}).onErrorContinue((err, _) => TBMCCoreAPI.SendException("An error occured while handling a message!", err, DiscordPlugin.plugin)).subscribe()
dispatcher.on(classOf[PresenceUpdateEvent]).subscribe((event: PresenceUpdateEvent) => {
if (!DiscordPlugin.SafeMode)
FunModule.handleFullHouse(event)
})
dispatcher.on(classOf[RoleCreateEvent]).^^().subscribe(GameRoleModule.handleRoleEvent _)
dispatcher.on(classOf[RoleDeleteEvent]).^^().subscribe(GameRoleModule.handleRoleEvent _)
dispatcher.on(classOf[RoleUpdateEvent]).^^().subscribe(GameRoleModule.handleRoleEvent _)
dispatcher.on(classOf[ChatInputInteractionEvent]).^^().subscribe((event: ChatInputInteractionEvent) => {
if (event.getCommandName equals "connect") {
new ConnectCommand().`def`(new Command2DCSender(event), event.getOption("name").get.getValue.get.asString)
}
})
}
var debug = false
def debug(debug: String): Unit = if (CommonListeners.debug) { //Debug
DPUtils.getLogger.info(debug)
}
}

View file

@ -0,0 +1,55 @@
package buttondevteam.discordplugin.listeners
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin.commands.ConnectCommand
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
import buttondevteam.discordplugin.util.DPState
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.discordplugin.mcchat.sender.DiscordUser
import buttondevteam.lib.ScheduledServerRestartEvent
import buttondevteam.lib.player.TBMCPlayerGetInfoEvent
import discord4j.common.util.Snowflake
import org.bukkit.event.player.PlayerJoinEvent
import org.bukkit.event.{EventHandler, Listener}
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.SMono
import scala.jdk.OptionConverters._
class MCListener extends Listener {
@EventHandler def onPlayerJoin(e: PlayerJoinEvent): Unit =
if (ConnectCommand.WaitingToConnect.containsKey(e.getPlayer.getName)) {
@SuppressWarnings(Array("ConstantConditions")) val user = DiscordPlugin.dc.getUserById(Snowflake.of(ConnectCommand.WaitingToConnect.get(e.getPlayer.getName))).block
if (user == null) return ()
e.getPlayer.sendMessage("§bTo connect with the Discord account @" + user.getUsername + "#" + user.getDiscriminator + " do /discord accept")
e.getPlayer.sendMessage("§bIf it wasn't you, do /discord decline")
}
@EventHandler def onGetInfo(e: TBMCPlayerGetInfoEvent): Unit = {
Option(DiscordPlugin.SafeMode).filterNot(identity).flatMap(_ => Option(e.getPlayer.getAs(classOf[DiscordUser])))
.flatMap(dp => Option(dp.getDiscordID)).filter(_.nonEmpty)
.map(Snowflake.of).flatMap(id => DiscordPlugin.dc.getUserById(id).^^().onErrorResume(_ => SMono.empty).blockOption())
.map(user => {
e.addInfo("Discord tag: " + user.getUsername + "#" + user.getDiscriminator)
user
})
.flatMap(user => user.asMember(DiscordPlugin.mainServer.getId).^^().onErrorResume(t => SMono.empty).blockOption())
.flatMap(member => member.getPresence.blockOptional().toScala)
.map(pr => {
e.addInfo(pr.getStatus.toString)
pr
})
.flatMap(_.getActivity.toScala).foreach(activity => e.addInfo(s"${activity.getType}: ${activity.getName}"))
}
/*@EventHandler
public void onCommandPreprocess(TBMCCommandPreprocessEvent e) {
if (e.getMessage().equalsIgnoreCase("/stop"))
MinecraftChatModule.state = DPState.STOPPING_SERVER;
else if (e.getMessage().equalsIgnoreCase("/restart"))
MinecraftChatModule.state = DPState.RESTARTING_SERVER;
}*/ @EventHandler //We don't really need this with the logger stuff but hey
def onScheduledRestart(e: ScheduledServerRestartEvent): Unit =
MinecraftChatModule.state = DPState.RESTARTING_SERVER
}

View file

@ -0,0 +1,184 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.core.component.channel.{Channel, ChatRoom}
import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin._
import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC}
import buttondevteam.discordplugin.mcchat.sender.{DiscordConnectedPlayer, DiscordUser}
import buttondevteam.lib.TBMCSystemChatEvent
import buttondevteam.lib.chat.{Command2, CommandClass}
import buttondevteam.lib.player.{ChromaGamerBase, TBMCPlayer}
import discord4j.core.`object`.entity.Message
import discord4j.core.`object`.entity.channel.{GuildChannel, MessageChannel}
import discord4j.rest.util.{Permission, PermissionSet}
import org.bukkit.Bukkit
import reactor.core.scala.publisher.SMono
import java.lang.reflect.Method
import java.util.function.Supplier
import java.util.{Objects, Optional}
import javax.annotation.Nullable
import scala.jdk.Accumulator
import scala.jdk.CollectionConverters.ListHasAsScala
import scala.jdk.OptionConverters.RichOptional
import scala.jdk.StreamConverters.StreamHasToScala
@SuppressWarnings(Array("SimplifyOptionalCallChains")) //Java 11
@CommandClass(helpText = Array("Channel connect", //
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).",
"You need to have access to the MC channel and have manage permissions on the Discord channel.",
"You also need to have your Minecraft account connected. In #bot use /connect <mcname>.",
"Call this command from the channel you want to use.", "Usage: @Bot channelcon <mcchannel>",
"Use the ID (command) of the channel, for example `g` for the global chat.",
"To remove a connection use @ChromaBot channelcon remove in the channel.",
"Mentioning the bot is needed in this case because the / prefix only works in #bot.",
"Invite link: <Unknown>" //
))
class ChannelconCommand(private val module: MinecraftChatModule) extends ICommand2DC {
@Command2.Subcommand def remove(sender: Command2DCSender): Boolean = {
val message: Message = null // TODO
if (checkPerms(message, null)) return true
else if (MCChatCustom.removeCustomChat(message.getChannelId))
DPUtils.reply(message, SMono.empty, "channel connection removed.").subscribe()
else
DPUtils.reply(message, SMono.empty, "this channel isn't connected.").subscribe()
true
}
@Command2.Subcommand def toggle(sender: Command2DCSender, @Command2.OptionalArg toggle: String): Boolean = {
val message: Message = null // TODO
if (checkPerms(message, null)) {
return true
}
val cc: MCChatCustom.CustomLMD = MCChatCustom.getCustomChat(message.getChannelId)
if (cc == null) {
return respond(sender, "this channel isn't connected.")
}
val togglesString: Supplier[String] = () => ChannelconBroadcast.values
.map(t => t.toString.toLowerCase + ": " + (if ((cc.toggles & (1 << t.id)) == 0) "disabled" else "enabled"))
.mkString("\n") + "\n\n" +
TBMCSystemChatEvent.BroadcastTarget.stream.toScala(Accumulator)
.map(target => s"${target.getName}: ${if (cc.brtoggles.contains(target)) "enabled" else "disabled"}")
.mkString("\n")
if (toggle == null) {
DPUtils.reply(message, SMono.empty, "toggles:\n" + togglesString.get).subscribe()
return true
}
val arg: String = toggle.toUpperCase
val b = ChannelconBroadcast.values.find((t: ChannelconBroadcast) => t.toString == arg)
if (b.isEmpty) {
val bt: TBMCSystemChatEvent.BroadcastTarget = TBMCSystemChatEvent.BroadcastTarget.get(arg)
if (bt == null) {
DPUtils.reply(message, SMono.empty, "cannot find toggle. Toggles:\n" + togglesString.get).subscribe()
return true
}
val add: Boolean = !cc.brtoggles.contains(bt)
if (add) {
cc.brtoggles += bt
}
else {
cc.brtoggles -= bt
}
return respond(sender, "'" + bt.getName + "' " + (if (add) "en" else "dis") + "abled")
}
//A B | F
//------- A: original - B: mask - F: new
//0 0 | 0
//0 1 | 1
//1 0 | 1
//1 1 | 0
// XOR
cc.toggles ^= (1 << b.get.id)
DPUtils.reply(message, SMono.empty, "'" + b.get.toString.toLowerCase + "' "
+ (if ((cc.toggles & (1 << b.get.id)) == 0) "disabled" else "enabled")).subscribe()
true
}
@Command2.Subcommand def `def`(sender: Command2DCSender, channelID: String): Boolean = {
val message: Message = null // TODO
if (!(module.allowCustomChat.get)) {
sender.sendMessage("channel connection is not allowed on this Minecraft server.")
return true
}
val channel = message.getChannel.block
if (checkPerms(message, channel)) {
return true
}
if (MCChatCustom.hasCustomChat(message.getChannelId)) {
return respond(sender, "this channel is already connected to a Minecraft channel. Use `@ChromaBot channelcon remove` to remove it.")
}
val chan: Optional[Channel] = Channel.getChannels.filter(ch => ch.getIdentifier.equalsIgnoreCase(channelID)
|| ch.extraIdentifiers.get().asScala.exists(id => id.equalsIgnoreCase(channelID))).findAny
if (!chan.isPresent) { //TODO: Red embed that disappears over time (kinda like the highlight messages in OW)
DPUtils.reply(message, channel, "MC channel with ID '" + channelID + "' not found! The ID is the command for it without the /.").subscribe()
return true
}
if (!(message.getAuthor.isPresent)) {
return true
}
val author = message.getAuthor.get
val dp: DiscordUser = ChromaGamerBase.getUser(author.getId.asString, classOf[DiscordUser])
val chp: TBMCPlayer = dp.getAs(classOf[TBMCPlayer])
if (chp == null) {
DPUtils.reply(message, channel, "you need to connect your Minecraft account. On the main server in " + DPUtils.botmention + " do " + DiscordPlugin.getPrefix + "connect <MCname>").subscribe()
return true
}
val dcp: DiscordConnectedPlayer = DiscordConnectedPlayer.create(message.getAuthor.get, channel, chp.getUniqueId, Bukkit.getOfflinePlayer(chp.getUniqueId).getName, module)
//Using a fake player with no login/logout, should be fine for this event
val groupid: String = chan.get.getGroupID(chp)
if (groupid == null && !((chan.get.isInstanceOf[ChatRoom]))) { //ChatRooms don't allow it unless the user joins, which happens later
DPUtils.reply(message, channel, "sorry, you cannot use that Minecraft channel.").subscribe()
return true
}
if (chan.get.isInstanceOf[ChatRoom]) { //ChatRooms don't work well
DPUtils.reply(message, channel, "chat rooms are not supported yet.").subscribe()
return true
}
/*if (MCChatListener.getCustomChats().stream().anyMatch(cc -> cc.groupID.equals(groupid) && cc.mcchannel.ID.equals(chan.get().ID))) {
DPUtils.reply(message, null, "sorry, this MC chat is already connected to a different channel, multiple channels are not supported atm.");
return true;
}*/
//TODO: "Channel admins" that can connect channels?
MCChatCustom.addCustomChat(channel, groupid, chan.get, author, dcp, 0, Set())
if (chan.get.isInstanceOf[ChatRoom]) {
DPUtils.reply(message, channel, "alright, connection made to the room!").subscribe()
}
else {
DPUtils.reply(message, channel, "alright, connection made to group `" + groupid + "`!").subscribe()
}
true
}
@SuppressWarnings(Array("ConstantConditions"))
private def checkPerms(message: Message, @Nullable channel: MessageChannel): Boolean = {
if (channel == null) {
return checkPerms(message, message.getChannel.block)
}
if (!((channel.isInstanceOf[GuildChannel]))) {
DPUtils.reply(message, channel, "you can only use this command in a server!").subscribe()
return true
}
//noinspection OptionalGetWithoutIsPresent
val perms: PermissionSet = (channel.asInstanceOf[GuildChannel]).getEffectivePermissions(message.getAuthor.toScala.map(_.getId).get).block
if (!(perms.contains(Permission.ADMINISTRATOR)) && !(perms.contains(Permission.MANAGE_CHANNELS))) {
DPUtils.reply(message, channel, "you need to have manage permissions for this channel!").subscribe()
return true
}
false
}
override def getHelpText(method: Method, ann: Command2.Subcommand): Array[String] =
Array[String](
"Channel connect",
"This command allows you to connect a Minecraft channel to a Discord channel (just like how the global chat is connected to #minecraft-chat).",
"You need to have access to the MC channel and have manage permissions on the Discord channel.",
"You also need to have your Minecraft account connected. In " + DPUtils.botmention + " use " + DiscordPlugin.getPrefix + "connect <mcname>.",
"Call this command from the channel you want to use.", "Usage: " + Objects.requireNonNull(DiscordPlugin.dc.getSelf.block).getMention + " channelcon <mcchannel>",
"Use the ID (command) of the channel, for example `g` for the global chat.",
"To remove a connection use @ChromaBot channelcon remove in the channel.",
"Mentioning the bot is needed in this case because the " + DiscordPlugin.getPrefix + " prefix only works in " + DPUtils.botmention + ".",
"Invite link: <https://discordapp.com/oauth2/authorize?client_id="
+ DiscordPlugin.dc.getApplicationInfo.^^().map(info => info.getId.asString).blockOption().getOrElse("Unknown")
+ "&scope=bot&permissions=268509264>")
}

View file

@ -0,0 +1,37 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.discordplugin.commands.{Command2DCSender, ICommand2DC}
import buttondevteam.discordplugin.mcchat.sender.DiscordUser
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
import buttondevteam.lib.chat.{Command2, CommandClass}
import buttondevteam.lib.player.ChromaGamerBase
import discord4j.core.`object`.entity.channel.PrivateChannel
@CommandClass(helpText = Array(
"MC Chat",
"This command enables or disables the Minecraft chat in private messages.", //
"It can be useful if you don't want your messages to be visible, for example when talking in a private channel.",
"You can also run all of the ingame commands you have access to using this command, if you have your accounts connected." //
))
class MCChatCommand(private val module: MinecraftChatModule) extends ICommand2DC {
@Command2.Subcommand override def `def`(sender: Command2DCSender): Boolean = {
// TODO: If the user is logged in, don't let the object be removed from the cache (test first)
if (!module.allowPrivateChat.get) {
sender.sendMessage("Using the private chat is not allowed on this Minecraft server.")
return true
}
val channel = sender.event.getInteraction.getChannel.block()
if (!channel.isInstanceOf[PrivateChannel]) {
sender.sendMessage("This command can only be issued in a direct message with the bot.")
return true
}
val user: DiscordUser = ChromaGamerBase.getUser(sender.author.getId.asString, classOf[DiscordUser])
val mcchat: Boolean = !user.isMinecraftChatEnabled
MCChatPrivate.privateMCChat(channel, mcchat, sender.author, user)
sender.sendMessage("Minecraft chat " +
(if (mcchat) "enabled. Use '" + DiscordPlugin.getPrefix + "mcchat' again to turn it off."
else "disabled."))
true
// TODO: Pin channel switching to indicate the current channel
}
}

View file

@ -0,0 +1,67 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.core.component.channel.{Channel, ChatRoom}
import buttondevteam.discordplugin.mcchat.sender.DiscordConnectedPlayer
import buttondevteam.lib.TBMCSystemChatEvent
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.User
import discord4j.core.`object`.entity.channel.MessageChannel
import javax.annotation.Nullable
import scala.collection.mutable.ListBuffer
object MCChatCustom {
/**
* Used for town or nation chats or anything else
*/
private[mcchat] val lastmsgCustom = new ListBuffer[MCChatCustom.CustomLMD]
def addCustomChat(channel: MessageChannel, groupid: String, mcchannel: Channel, user: User, dcp: DiscordConnectedPlayer, toggles: Int, brtoggles: Set[TBMCSystemChatEvent.BroadcastTarget]): Boolean = {
lastmsgCustom synchronized {
var gid: String = null
mcchannel match {
case room: ChatRoom =>
room.joinRoom(dcp.getChromaUser)
gid = if (groupid == null) mcchannel.getGroupID(dcp.getChromaUser) else groupid
case _ =>
gid = groupid
}
val lmd = new MCChatCustom.CustomLMD(channel, user, gid, mcchannel, dcp, toggles, brtoggles)
lastmsgCustom += lmd
}
true
}
def hasCustomChat(channel: Snowflake): Boolean =
lastmsgCustom.exists(_.channel.getId.asLong == channel.asLong)
@Nullable def getCustomChat(channel: Snowflake): CustomLMD =
lastmsgCustom.find(_.channel.getId.asLong == channel.asLong).orNull
def removeCustomChat(channel: Snowflake): Boolean = {
lastmsgCustom synchronized {
MCChatUtils.lastmsgfromd.remove(channel.asLong)
val count = lastmsgCustom.size
lastmsgCustom.filterInPlace(lmd => {
if (lmd.channel.getId.asLong != channel.asLong) true
else {
lmd.mcchannel match {
case room: ChatRoom => room.leaveRoom(lmd.dcp.getChromaUser)
case _ =>
}
false
}
})
lastmsgCustom.size < count
}
}
def getCustomChats: List[CustomLMD] = lastmsgCustom.toList
// TODO: Store Chroma user only
class CustomLMD private[mcchat](channel: MessageChannel, val dcUser: User, val groupID: String,
mcchannel: Channel, val dcp: DiscordConnectedPlayer, var toggles: Int,
var brtoggles: Set[TBMCSystemChatEvent.BroadcastTarget]) extends MCChatUtils.LastMsgData(channel, mcchannel, dcp.getChromaUser) {
}
}

View file

@ -0,0 +1,479 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.core.ComponentManager
import buttondevteam.discordplugin._
import buttondevteam.discordplugin.DPUtils.{MonoExtensions, SpecExtensions}
import buttondevteam.discordplugin.mcchat.sender.{DiscordSender, DiscordSenderBase, DiscordUser}
import buttondevteam.lib._
import buttondevteam.lib.chat.{ChatMessage, TBMCChatAPI}
import buttondevteam.lib.player.TBMCPlayer
import com.vdurmont.emoji.EmojiParser
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel}
import discord4j.core.`object`.entity.{Member, Message, User}
import discord4j.core.event.domain.message.MessageCreateEvent
import discord4j.core.spec.EmbedCreateSpec
import discord4j.core.spec.legacy.{LegacyEmbedCreateSpec, LegacyMessageEditSpec}
import discord4j.rest.http.client.ClientException
import discord4j.rest.util.Color
import io.netty.handler.codec.http.HttpResponseStatus
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import org.bukkit.event.{EventHandler, Listener}
import org.bukkit.scheduler.BukkitTask
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.SMono
import java.time.Instant
import java.util
import java.util.concurrent.{LinkedBlockingQueue, TimeoutException}
import java.util.function.{Consumer, Predicate, Supplier}
import java.util.stream.Collectors
import scala.jdk.CollectionConverters.{IterableHasAsScala, ListHasAsScala, SetHasAsScala}
import scala.jdk.OptionConverters.RichOptional
object MCChatListener {
// ......................DiscordSender....DiscordConnectedPlayer.DiscordPlayerSender
// Offline public chat......x............................................
// Online public chat.......x...........................................x
// Offline private chat.....x.......................x....................
// Online private chat......x.......................x...................x
// If online and enabling private chat, don't login
// If leaving the server and private chat is enabled (has ConnectedPlayer), call login in a task on lowest priority
// If private chat is enabled and joining the server, logout the fake player on highest priority
// If online and disabling private chat, don't logout
// The maps may not contain the senders for UnconnectedSenders
@FunctionalInterface private trait InterruptibleConsumer[T] {
@throws[TimeoutException]
@throws[InterruptedException]
def accept(value: T): Unit
}
}
class MCChatListener(val module: MinecraftChatModule) extends Listener {
private var sendtask: BukkitTask = null
final private val sendevents = new LinkedBlockingQueue[util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant]]
private var sendrunnable: Runnable = null
private var sendthread: Thread = null
private var stop = false //A new instance will be created on enable
@EventHandler // Minecraft
def onMCChat(ev: TBMCChatEvent): Unit = {
if (!(ComponentManager.isEnabled(classOf[MinecraftChatModule])) || ev.isCancelled) { //SafeMode: Needed so it doesn't restart after server shutdown
return ()
}
sendevents.add(new util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant](ev, Instant.now))
if (sendtask != null) {
return ()
}
sendrunnable = () => {
def foo(): Unit = {
sendthread = Thread.currentThread
processMCToDiscord()
if (DiscordPlugin.plugin.isEnabled && !(stop)) { //Don't run again if shutting down
sendtask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable)
}
}
foo()
}
sendtask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, sendrunnable)
}
private def processMCToDiscord(): Unit = {
try {
var e: TBMCChatEvent = null
var time: Instant = null
val se: util.AbstractMap.SimpleEntry[TBMCChatEvent, Instant] = sendevents.take // Wait until an element is available
e = se.getKey
time = se.getValue
val authorPlayer: String = "[" + DPUtils.sanitizeStringNoEscape(e.getChannel.displayName.get) + "] " + //
(if ("Minecraft" == e.getOrigin) "" else "[" + e.getOrigin.charAt(0) + "]") +
DPUtils.sanitizeStringNoEscape(e.getUser.getName)
val color: chat.Color = e.getChannel.color.get
val embed = () => {
val ecs = EmbedCreateSpec.builder()
ecs.description(e.getMessage).color(Color.of(color.getRed, color.getGreen, color.getBlue))
val url: String = module.profileURL.get
e.getUser match {
case player: TBMCPlayer =>
DPUtils.embedWithHead(ecs, authorPlayer, e.getUser.getName,
if (url.nonEmpty) url + "?type=minecraft&id=" + player.getUniqueId else null)
case dsender: DiscordUser =>
ecs.author(authorPlayer,
if (url.nonEmpty) url + "?type=discord&id=" + dsender.getDiscordID else null,
dsender.getDiscordUser.getAvatarUrl)
case _ =>
DPUtils.embedWithHead(ecs, authorPlayer, e.getUser.getName, null)
}
ecs.timestamp(time)
}
val nanoTime: Long = System.nanoTime
val doit = (lastmsgdata: MCChatUtils.LastMsgData) => {
if (lastmsgdata.message == null
|| authorPlayer != lastmsgdata.message.getEmbeds.get(0).getAuthor.toScala.flatMap(_.getName.toScala).orNull
|| lastmsgdata.time / 1000000000f < nanoTime / 1000000000f - 120
|| !(lastmsgdata.mcchannel.getIdentifier == e.getChannel.getIdentifier)
|| lastmsgdata.content.length + e.getMessage.length + 1 > 2048) {
lastmsgdata.message = lastmsgdata.channel.createMessage(embed().build()).block
lastmsgdata.time = nanoTime
lastmsgdata.mcchannel = e.getChannel
lastmsgdata.content = e.getMessage
}
else {
//TODO: The editing shouldn't be blocking, this whole method should be reactive
lastmsgdata.content = lastmsgdata.content + "\n" + e.getMessage // The message object doesn't get updated
try {
lastmsgdata.message.edit().withEmbeds(embed().description(lastmsgdata.content).build()).block
} catch {
case e: ClientException => {
// The message was deleted
if (e.getStatus ne HttpResponseStatus.NOT_FOUND) {
throw e
}
}
}
}
}
// Checks if the given channel is different than where the message was sent from
// Or if it was from MC
val isdifferentchannel: Predicate[Snowflake] = {
id => !e.getUser.isInstanceOf[DiscordUser] || e.getUser.asInstanceOf[DiscordSender].getChannel.getId.asLong != id.asLong
}
if (e.getChannel.isGlobal && (e.isFromCommand || isdifferentchannel.test(module.chatChannel.get))) {
if (MCChatUtils.lastmsgdata == null)
MCChatUtils.lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono.block(), null)
doit(MCChatUtils.lastmsgdata)
}
for (data <- MCChatPrivate.lastmsgPerUser) {
if ((e.isFromCommand || isdifferentchannel.test(data.channel.getId)) && e.shouldSendTo(data.user)) {
doit(data)
}
}
MCChatCustom.lastmsgCustom synchronized {
MCChatCustom.lastmsgCustom.filterInPlace(lmd => {
if ((e.isFromCommand || isdifferentchannel.test(lmd.channel.getId)) //Test if msg is from Discord
&& e.getChannel.getIdentifier == lmd.mcchannel.getIdentifier //If it's from a command, the command msg has been deleted, so we need to send it
&& e.getGroupID() == lmd.groupID) { //Check if this is the group we want to test - #58
if (e.shouldSendTo(lmd.dcp.getChromaUser)) { //Check original user's permissions
doit(lmd)
true
}
else {
lmd.channel.createMessage("The user no longer has permission to view the channel, connection removed.").subscribe()
false //If the user no longer has permission, remove the connection
}
}
else true
})
}
} catch {
case ex: InterruptedException =>
//Stop if interrupted anywhere
sendtask.cancel()
sendtask = null
case ex: Exception =>
TBMCCoreAPI.SendException("Error while sending message to Discord!", ex, module)
}
}
@EventHandler def onChatPreprocess(event: TBMCChatPreprocessEvent): Unit = {
var start: Int = -(1)
while ( {
(start = event.getMessage.indexOf('@', start + 1), start) != ((), -1)
}) {
val mid: Int = event.getMessage.indexOf('#', start + 1)
if (mid == -1) {
return ()
}
var end_ = event.getMessage.indexOf(' ', mid + 1)
if (end_ == -1) {
end_ = event.getMessage.length
}
val end: Int = end_
val startF: Int = start
val user = DiscordPlugin.dc.getUsers.filter((u) => u.getUsername.equals(event.getMessage.substring(startF + 1, mid))).filter((u) => u.getDiscriminator.equals(event.getMessage.substring(mid + 1, end))).blockFirst
if (user != null) { //TODO: Nicknames
event.setMessage(event.getMessage.substring(0, startF) + "@" + user.getUsername + (if (event.getMessage.length > end) {
event.getMessage.substring(end)
}
else {
""
})) // TODO: Add formatting
}
start = end // Skip any @s inside the mention
}
}
/**
* Stop the listener permanently. Enabling the module will create a new instance.
*
* @param wait Wait 5 seconds for the threads to stop
*/
def stop(wait: Boolean): Unit = {
stop = true
MCChatPrivate.logoutAll()
MCChatUtils.LoggedInPlayers.clear()
if (sendthread != null) {
sendthread.interrupt()
}
if (recthread != null) {
recthread.interrupt()
}
try {
if (sendthread != null) {
sendthread.interrupt()
if (wait) {
sendthread.join(5000)
}
}
if (recthread != null) {
recthread.interrupt()
if (wait) {
recthread.join(5000)
}
}
MCChatUtils.lastmsgdata = null
MCChatPrivate.lastmsgPerUser.clear()
MCChatCustom.lastmsgCustom.clear()
MCChatUtils.lastmsgfromd.clear()
MCChatUtils.UnconnectedSenders.clear()
recthread = null
sendthread = null
} catch {
case e: InterruptedException =>
e.printStackTrace() //This thread shouldn't be interrupted
}
}
private var rectask: BukkitTask = null
final private val recevents: LinkedBlockingQueue[MessageCreateEvent] = new LinkedBlockingQueue[MessageCreateEvent]
private var recrun: Runnable = null
private var recthread: Thread = null
// Discord
def handleDiscord(ev: MessageCreateEvent): SMono[Boolean] = {
val author = Option(ev.getMessage.getAuthor.orElse(null))
val hasCustomChat = MCChatCustom.hasCustomChat(ev.getMessage.getChannelId)
val prefix = DiscordPlugin.getPrefix
ev.getMessage.getChannel.^^()
.filter(channel => isChatEnabled(channel, author, hasCustomChat))
.filter(channel => !isRunningMCChatCommand(channel, ev.getMessage.getContent, prefix))
.filter(channel => {
MCChatUtils.resetLastMessage(channel)
recevents.add(ev)
if (rectask == null) {
recrun = () => {
recthread = Thread.currentThread
processDiscordToMC()
if (DiscordPlugin.plugin.isEnabled && !(stop)) {
rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Continue message processing
}
}
rectask = Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, recrun) //Start message processing
}
true
}).map(_ => false).defaultIfEmpty(true)
}
private def isChatEnabled(channel: MessageChannel, author: Option[User], hasCustomChat: Boolean) = {
def hasPrivateChat = channel.isInstanceOf[PrivateChannel] &&
author.exists((u: User) => MCChatPrivate.isMinecraftChatEnabled(u.getId.asString))
def hasPublicChat = channel.getId.asLong == module.chatChannel.get.asLong
hasPublicChat || hasPrivateChat || hasCustomChat
}
private def isRunningMCChatCommand(channel: MessageChannel, content: String, prefix: Char) = {
(channel.isInstanceOf[PrivateChannel] //Only in private chat
&& content.length < "/mcchat<>".length
&& content.replace(prefix + "", "").equalsIgnoreCase("mcchat")) //Either mcchat or /mcchat
//Allow disabling the chat if needed
}
private def processDiscordToMC(): Unit = {
var event: MessageCreateEvent = null
try event = recevents.take
catch {
case _: InterruptedException =>
rectask.cancel()
return ()
}
val sender: User = event.getMessage.getAuthor.orElse(null)
var dmessage: String = event.getMessage.getContent
try {
val dsender: DiscordSenderBase = MCChatUtils.getSender(event.getMessage.getChannelId, sender)
val user: DiscordUser = dsender.getChromaUser
def replaceUserMentions(): Unit = {
for (u <- event.getMessage.getUserMentions.asScala) { //TODO: Role mentions
dmessage = dmessage.replace(u.getMention, "@" + u.getUsername) // TODO: IG Formatting
val m = u.asMember(DiscordPlugin.mainServer.getId).^^().onErrorResume(_ => SMono.empty).blockOption()
if (m.nonEmpty) {
val mm: Member = m.get
val nick: String = mm.getDisplayName
dmessage = dmessage.replace(mm.getNicknameMention, "@" + nick)
}
}
}
replaceUserMentions()
def replaceChannelMentions(): Unit = {
for (ch <- event.getGuild.flux.flatMap(_.getChannels).toIterable().asScala) {
dmessage = dmessage.replace(ch.getMention, "#" + ch.getName)
}
}
replaceChannelMentions()
def replaceEmojis(): Unit = {
dmessage = EmojiParser.parseToAliases(dmessage, EmojiParser.FitzpatrickAction.PARSE) //Converts emoji to text- TODO: Add option to disable (resource pack?)
dmessage = dmessage.replaceAll(":(\\S+)\\|type_(?:(\\d)|(1)_2):", ":$1::skin-tone-$2:") //Convert to Discord's format so it still shows up
dmessage = dmessage.replaceAll("<a?:(\\S+):(\\d+)>", ":$1:") //We don't need info about the custom emojis, just display their text
}
replaceEmojis()
val clmd = MCChatCustom.getCustomChat(event.getMessage.getChannelId)
val sendChannel = event.getMessage.getChannel.block
val isPrivate = sendChannel.isInstanceOf[PrivateChannel]
def addCheckmark() = {
try {
val lmfd = MCChatUtils.lastmsgfromd.get(event.getMessage.getChannelId.asLong)
if (lmfd != null)
lmfd.removeSelfReaction(DiscordPlugin.DELIVERED_REACTION).subscribe // Remove it no matter what, we know it's there 99.99% of the time
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("An error occured while removing reactions from chat!", e, module)
}
MCChatUtils.lastmsgfromd.put(event.getMessage.getChannelId.asLong, event.getMessage)
event.getMessage.addReaction(DiscordPlugin.DELIVERED_REACTION).subscribe()
}
if (dmessage.startsWith("/")) // Ingame command
handleIngameCommand(event, dmessage, dsender, user, clmd, isPrivate)
else if (handleIngameMessage(event, dmessage, dsender, user, clmd, isPrivate)) // Not a command
addCheckmark()
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("An error occured while handling message \"" + dmessage + "\"!", e, module)
}
}
/**
* Handles a message coming from Discord to Minecraft.
*
* @param event The Discord event
* @param dmessage The message itself
* @param dsender The sender who sent it
* @param user The Chroma user of the sender
* @param clmd Custom chat last message data (if in a custom chat)
* @param isPrivate Whether the chat is private
* @return Whether the bot should react with a checkmark
*/
private def handleIngameMessage(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordUser,
clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Boolean = {
def getAttachmentText = {
val att = event.getMessage.getAttachments.asScala
if (att.nonEmpty) att map (_.getUrl) mkString "\n"
else ""
}
if (event.getMessage.getType eq Message.Type.CHANNEL_PINNED_MESSAGE) {
val mcchannel = if (clmd != null) clmd.mcchannel else dsender.getChromaUser.getChannel.get
val rtr = mcchannel getRTR (if (clmd != null) clmd.dcp.getChromaUser else user)
TBMCChatAPI.SendSystemMessage(mcchannel, rtr, (dsender match {
case player: Player => player.getDisplayName
case _ => dsender.getName
}) + " pinned a message on Discord.", TBMCSystemChatEvent.BroadcastTarget.ALL)
false
}
else {
val cmb = ChatMessage.builder(user, dmessage + getAttachmentText).fromCommand(false)
if (clmd != null)
TBMCChatAPI.sendChatMessage(cmb.permCheck(clmd.dcp.getChromaUser).build, clmd.mcchannel)
else
TBMCChatAPI.sendChatMessage(cmb.build)
true
}
}
/**
* Handle a Minecraft command coming from Discord.
*
* @param event The Discord event
* @param dmessage The Discord mewsage, starting with a slash
* @param dsender The sender who sent it
* @param user The Chroma user of the sender
* @param clmd The custom last message data (if in a custom chat)
* @param isPrivate Whether the chat is private
* @return
*/
private def handleIngameCommand(event: MessageCreateEvent, dmessage: String, dsender: DiscordSenderBase, user: DiscordUser,
clmd: MCChatCustom.CustomLMD, isPrivate: Boolean): Unit = {
def notWhitelisted(cmd: String) = module.whitelistedCommands.get.stream
.noneMatch(s => cmd == s || cmd.startsWith(s + " "))
def whitelistedCommands = module.whitelistedCommands.get.asScala.map("/" + _).mkString(", ")
if (!isPrivate)
event.getMessage.delete.subscribe()
val cmd = dmessage.substring(1)
val cmdlowercased = cmd.toLowerCase
if (dsender.isInstanceOf[DiscordSender] && notWhitelisted(cmdlowercased)) { // Command not whitelisted
dsender.sendMessage("Sorry, you can only access these commands from here:\n" + whitelistedCommands +
(if (user.getConnectedID(classOf[TBMCPlayer]) == null)
"\nTo access your commands, first please connect your accounts, using /connect in " + DPUtils.botmention
+ "\nThen y" else "\nY") + "ou can access all of your regular commands (even offline) in private chat: DM me `mcchat`!")
return ()
}
module.log(dsender.getName + " ran from DC: /" + cmd)
if (dsender.isInstanceOf[DiscordSender] && runCustomCommand(dsender, cmdlowercased)) {
return ()
}
val channel = if (clmd == null) user.getChannel.get else clmd.mcchannel
val ev = new TBMCCommandPreprocessEvent(user, channel, dmessage, if (clmd == null) user else clmd.dcp.getChromaUser)
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => { //Commands need to be run sync
Bukkit.getPluginManager.callEvent(ev)
if (!ev.isCancelled)
runMCCommand(dsender, cmd)
})
}
private def runMCCommand(dsender: DiscordSenderBase, cmd: String): Unit = {
try {
val mcpackage = Bukkit.getServer.getClass.getPackage.getName
if (!module.enableVanillaCommands.get)
Bukkit.dispatchCommand(dsender, cmd)
else // TODO: Vanilla command handling
Bukkit.dispatchCommand(dsender, cmd)
} catch {
case e: NoClassDefFoundError =>
TBMCCoreAPI.SendException("A class is not found when trying to run command " + cmd + "!", e, module)
case e: Exception =>
TBMCCoreAPI.SendException("An error occurred when trying to run command " + cmd + "! Vanilla commands are only supported in some MC versions.", e, module)
}
}
/**
* Handles custom public commands. Used to hide sensitive information in public chats.
*
* @param dsender The Discord sender
* @param cmdlowercased The command, lowercased
* @return Whether the command was a custom command
*/
private def runCustomCommand(dsender: DiscordSenderBase, cmdlowercased: String): Boolean = {
if (cmdlowercased.startsWith("list")) {
val players = Bukkit.getOnlinePlayers.asScala
dsender.sendMessage("There are " + players.count(MCChatUtils.checkEssentials) + " out of " + Bukkit.getMaxPlayers + " players online.")
dsender.sendMessage("Players: " + players.filter(MCChatUtils.checkEssentials).map(_.getDisplayName).mkString(", "))
true
}
else false
}
}

View file

@ -0,0 +1,79 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.core.ComponentManager
import buttondevteam.discordplugin.mcchat.MCChatUtils.LastMsgData
import buttondevteam.discordplugin.mcchat.sender.{DiscordConnectedPlayer, DiscordUser, DiscordSenderBase}
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.lib.player.TBMCPlayer
import discord4j.core.`object`.entity.User
import discord4j.core.`object`.entity.channel.{MessageChannel, PrivateChannel}
import org.bukkit.Bukkit
import scala.collection.mutable.ListBuffer
import scala.jdk.javaapi.CollectionConverters.asScala
object MCChatPrivate {
/**
* Used for messages in PMs (mcchat).
*/
private[mcchat] var lastmsgPerUser: ListBuffer[LastMsgData] = ListBuffer()
def privateMCChat(channel: MessageChannel, start: Boolean, user: User, dp: DiscordUser): Unit = {
MCChatUtils.ConnectedSenders synchronized {
val mcp = dp.getAs(classOf[TBMCPlayer])
if (mcp != null) { // If the accounts aren't connected, can't make a connected sender
val p = Bukkit.getPlayer(mcp.getUniqueId)
val op = Bukkit.getOfflinePlayer(mcp.getUniqueId)
val mcm = ComponentManager.getIfEnabled(classOf[MinecraftChatModule])
if (start) {
val sender = DiscordConnectedPlayer.create(user, channel, mcp.getUniqueId, op.getName, mcm)
MCChatUtils.addSenderTo(MCChatUtils.ConnectedSenders, user, sender)
MCChatUtils.LoggedInPlayers.put(mcp.getUniqueId, sender)
if (p == null) { // Player is offline - If the player is online, that takes precedence
MCChatUtils.callLoginEvents(sender)
}
}
else {
val sender = MCChatUtils.removeSenderFrom(MCChatUtils.ConnectedSenders, channel.getId, user)
assert(sender != null)
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => {
def foo(): Unit = {
if ((p == null || p.isInstanceOf[DiscordSenderBase]) // Player is offline - If the player is online, that takes precedence
&& sender.isLoggedIn) { //Don't call the quit event if login failed
MCChatUtils.callLogoutEvent(sender, needsSync = false) //The next line has to run *after* this one, so can't use the needsSync parameter
}
MCChatUtils.LoggedInPlayers.remove(sender.getUniqueId)
sender.setLoggedIn(false)
}
foo()
}
)
}
// ---- PermissionsEx warning is normal on logout ----
}
if (!start) MCChatUtils.lastmsgfromd.remove(channel.getId.asLong)
if (start) lastmsgPerUser += new MCChatUtils.LastMsgData(channel, dp) // Doesn't support group DMs
else lastmsgPerUser.filterInPlace(_.channel.getId.asLong != channel.getId.asLong) //Remove
}
}
def isMinecraftChatEnabled(dp: DiscordUser): Boolean = isMinecraftChatEnabled(dp.getDiscordID)
def isMinecraftChatEnabled(did: String): Boolean = { // Don't load the player data just for this
lastmsgPerUser.exists(_.channel.asInstanceOf[PrivateChannel].getRecipientIds.stream.anyMatch(u => u.asString == did))
}
def logoutAll(): Unit = {
MCChatUtils.ConnectedSenders synchronized {
for ((_, userMap) <- MCChatUtils.ConnectedSenders) {
for (valueEntry <- asScala(userMap.entrySet)) {
if (MCChatUtils.getSenderFrom(MCChatUtils.OnlineSenders, valueEntry.getKey, valueEntry.getValue.getUser) == null) { //If the player is online then the fake player was already logged out
MCChatUtils.callLogoutEvent(valueEntry.getValue, !Bukkit.isPrimaryThread)
}
}
}
}
}
}

View file

@ -0,0 +1,360 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.core.{ComponentManager, MainPlugin, component}
import buttondevteam.discordplugin.ChannelconBroadcast.ChannelconBroadcast
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin._
import buttondevteam.discordplugin.broadcaster.GeneralEventBroadcasterModule
import buttondevteam.discordplugin.mcchat.MCChatCustom.CustomLMD
import buttondevteam.discordplugin.mcchat.sender.{DiscordConnectedPlayer, DiscordPlayerSender, DiscordSender, DiscordSenderBase}
import buttondevteam.lib.player.ChromaGamerBase
import buttondevteam.lib.{TBMCCoreAPI, TBMCSystemChatEvent}
import com.google.common.collect.Sets
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.channel.{Channel, MessageChannel, PrivateChannel, TextChannel}
import discord4j.core.`object`.entity.{Message, User}
import io.netty.util.collection.LongObjectHashMap
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import org.bukkit.event.Event
import org.bukkit.event.player.{AsyncPlayerPreLoginEvent, PlayerJoinEvent, PlayerLoginEvent, PlayerQuitEvent}
import org.bukkit.plugin.AuthorNagException
import reactor.core.scala.publisher.SMono
import java.net.InetAddress
import java.util
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicInteger
import java.util.logging.Level
import java.util.stream.Collectors
import javax.annotation.Nullable
import scala.collection.convert.ImplicitConversions.`map AsJavaMap`
import scala.collection.mutable.ListBuffer
import scala.collection.{concurrent, mutable}
import scala.jdk.CollectionConverters.CollectionHasAsScala
import scala.jdk.javaapi.CollectionConverters.asScala
object MCChatUtils {
/**
* May contain P&lt;DiscordID&gt; as key for public chat
*/
val UnconnectedSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordSender]])
val ConnectedSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordConnectedPlayer]])
val OnlineSenders = asScala(new ConcurrentHashMap[String, ConcurrentHashMap[Snowflake, DiscordPlayerSender]])
val LoggedInPlayers = asScala(new ConcurrentHashMap[UUID, DiscordConnectedPlayer])
@Nullable private[mcchat] var lastmsgdata: MCChatUtils.LastMsgData = null
private[mcchat] val lastmsgfromd = new LongObjectHashMap[Message] // Last message sent by a Discord user, used for clearing checkmarks
private var module: MinecraftChatModule = null
private val staticExcludedPlugins: concurrent.Map[Class[_ <: Event], util.HashSet[String]] = concurrent.TrieMap()
val broadcastedMessages: mutable.Map[String, Long] = mutable.Map()
def updatePlayerList(): Unit = {
val mod = getModule
if (mod == null || !mod.showPlayerListOnDC.get) return ()
if (lastmsgdata != null) updatePL(lastmsgdata)
MCChatCustom.lastmsgCustom.foreach(MCChatUtils.updatePL)
}
private def notEnabled = (module == null || !module.disabling) && getModule == null //Allow using things while disabling the module
private def getModule = {
if (module == null || !module.isEnabled) module = ComponentManager.getIfEnabled(classOf[MinecraftChatModule])
//If disabled, it will try to get it again because another instance may be enabled - useful for /discord restart
module
}
private def updatePL(lmd: MCChatUtils.LastMsgData): Unit = {
if (!lmd.channel.isInstanceOf[TextChannel]) {
TBMCCoreAPI.SendException("Failed to update player list for channel " + lmd.channel.getId, new Exception("The channel isn't a (guild) text channel."), getModule)
return ()
}
var topic = lmd.channel.asInstanceOf[TextChannel].getTopic.orElse("")
if (topic.isEmpty) topic = ".\n----\nMinecraft chat\n----\n."
val s = topic.split("\\n----\\n")
if (s.length < 3) return ()
var gid: String = null
lmd match {
case clmd: CustomLMD => gid = clmd.groupID
case _ => //If we're not using a custom chat then it's either can ("everyone") or can't (null) see at most
gid = buttondevteam.core.component.channel.Channel.GROUP_EVERYONE // (Though it's a public chat then rn)
}
val C = new AtomicInteger
s(s.length - 1) = "Players: " + Bukkit.getOnlinePlayers.asScala.filter(p => if (lmd.mcchannel == null) {
gid == buttondevteam.core.component.channel.Channel.GROUP_EVERYONE //If null, allow if public (custom chats will have their channel stored anyway)
}
else {
gid == lmd.mcchannel.getGroupID(ChromaGamerBase.getFromSender(p))
}
).filter(MCChatUtils.checkEssentials) //If they can see it
.filter(_ => C.incrementAndGet > 0) //Always true
.map((p) => DPUtils.sanitizeString(p.getDisplayName)).mkString(", ")
s(0) = s"$C player${if (C.get != 1) "s" else ""} online"
lmd.channel.asInstanceOf[TextChannel].edit().withTopic(String.join("\n----\n", s: _*)).withReason("Player list update").subscribe() //Don't wait
}
private[mcchat] def checkEssentials(p: Player): Boolean = {
val ess = MainPlugin.ess
if (ess == null) return true
!ess.getUser(p).isHidden
}
def addSenderTo[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], user: User, sender: T): T =
addSenderTo(senders, user.getId.asString, sender)
def addSenderTo[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], did: String, sender: T): T = {
val mapOpt = senders.get(did)
val map = if (mapOpt.isEmpty) new ConcurrentHashMap[Snowflake, T] else mapOpt.get
map.put(sender.getChannel.getId, sender)
senders.put(did, map)
sender
}
def getSenderFrom[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = {
val mapOpt = senders.get(user.getId.asString)
if (mapOpt.nonEmpty) mapOpt.get.get(channel)
else null.asInstanceOf
}
def removeSenderFrom[T <: DiscordSenderBase](senders: concurrent.Map[String, ConcurrentHashMap[Snowflake, T]], channel: Snowflake, user: User): T = {
val mapOpt = senders.get(user.getId.asString)
if (mapOpt.nonEmpty) mapOpt.get.remove(channel)
else null.asInstanceOf
}
def forPublicPrivateChat(action: SMono[MessageChannel] => SMono[_]): SMono[_] = {
if (notEnabled) return SMono.empty
val list = MCChatPrivate.lastmsgPerUser.map(data => action(SMono.just(data.channel)))
.prepend(action(module.chatChannelMono))
SMono.whenDelayError(list)
}
/**
* For custom and all MC chat
*
* @param action The action to act (cannot complete empty)
* @param toggle The toggle to check
* @param hookmsg Whether the message is also sent from the hook
*/
def forCustomAndAllMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): SMono[_] = {
if (notEnabled) return SMono.empty
val list = List(if (!GeneralEventBroadcasterModule.isHooked || !hookmsg)
forPublicPrivateChat(action) else SMono.empty) ++
(if (toggle == null) MCChatCustom.lastmsgCustom
else MCChatCustom.lastmsgCustom.filter(cc => (cc.toggles & (1 << toggle.id)) != 0))
.map(_.channel).map(SMono.just).map(action)
SMono.whenDelayError(list)
}
/**
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
*
* @param action The action to do
* @param permUser The user to check perms of or null to send to all that has it toggled
* @param toggle The toggle to check or null to send to all allowed
*/
def forAllowedCustomMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable permUser: ChromaGamerBase, @Nullable toggle: ChannelconBroadcast): SMono[_] = {
if (notEnabled) return SMono.empty
val st = MCChatCustom.lastmsgCustom.filter(clmd => { //new TBMCChannelConnectFakeEvent(sender, clmd.mcchannel).shouldSendTo(clmd.dcp) - Thought it was this simple hehe - Wait, it *should* be this simple
if (toggle != null && ((clmd.toggles & (1 << toggle.id)) == 0)) false //If null then allow
else if (permUser == null) true
else clmd.groupID.equals(clmd.mcchannel.getGroupID(permUser))
}).map(cc => action.apply(SMono.just(cc.channel))) //TODO: Send error messages on channel connect
//Mono.whenDelayError((() => st.iterator).asInstanceOf[java.lang.Iterable[SMono[_]]]) //Can't convert as an iterator or inside the stream, but I can convert it as a stream
SMono.whenDelayError(st)
}
/**
* Do the {@code action} for each custom chat the {@code sender} have access to and has that broadcast type enabled.
*
* @param action The action to do
* @param permUser The user to check perms of or null to send to all that has it toggled
* @param toggle The toggle to check or null to send to all allowed
* @param hookmsg Whether the message is also sent from the hook
*/
def forAllowedCustomAndAllMCChat(action: SMono[MessageChannel] => SMono[_], @Nullable permUser: ChromaGamerBase, @Nullable toggle: ChannelconBroadcast, hookmsg: Boolean): SMono[_] = {
if (notEnabled) return SMono.empty
val cc = forAllowedCustomMCChat(action, permUser, toggle)
if (!GeneralEventBroadcasterModule.isHooked || !hookmsg) return SMono.whenDelayError(List(forPublicPrivateChat(action), cc))
SMono.whenDelayError(List(cc))
}
def send(message: String): SMono[MessageChannel] => SMono[_] = _.flatMap((mc: MessageChannel) => {
resetLastMessage(mc)
mc.createMessage(DPUtils.sanitizeString(message)).^^()
})
def forAllowedMCChat(action: SMono[MessageChannel] => SMono[_], event: TBMCSystemChatEvent): SMono[_] = {
if (notEnabled) return SMono.empty
val list = new ListBuffer[SMono[_]]
if (event.getChannel.isGlobal) list.append(action(module.chatChannelMono))
for (data <- MCChatPrivate.lastmsgPerUser)
if (event.shouldSendTo(data.user))
list.append(action(SMono.just(data.channel))) //TODO: Only store ID?
MCChatCustom.lastmsgCustom.filter(clmd =>
clmd.brtoggles.contains(event.getTarget) && event.shouldSendTo(clmd.dcp.getChromaUser))
.map(clmd => action(SMono.just(clmd.channel))).foreach(elem => {
list.append(elem)
()
})
SMono.whenDelayError(list)
}
/**
* This method will find the best sender to use: if the player is online, use that, if not but connected then use that etc.
*/
private[mcchat] def getSender(channel: Snowflake, author: User): DiscordSenderBase = {
Option(getSenderFrom(OnlineSenders, channel, author)) // Find first non-null
.orElse(Option(getSenderFrom(ConnectedSenders, channel, author))) // This doesn't support the public chat, but it'll always return null for it
.orElse(Option(getSenderFrom(UnconnectedSenders, channel, author))) //
.orElse(Option(addSenderTo(UnconnectedSenders, author,
new DiscordSender(author, DiscordPlugin.dc.getChannelById(channel).block().asInstanceOf[MessageChannel]))))
.get
}
/**
* Resets the last message, so it will start a new one instead of appending to it.
* This is used when someone (even the bot) sends a message to the channel.
*
* @param channel The channel to reset in - the process is slightly different for the public, private and custom chats
*/
def resetLastMessage(channel: Channel): Unit = {
if (notEnabled) return ()
if (channel.getId.asLong == module.chatChannel.get.asLong) {
if (lastmsgdata == null) lastmsgdata = new MCChatUtils.LastMsgData(module.chatChannelMono.block(), null)
else lastmsgdata.message = null
return ()
} // Don't set the whole object to null, the player and channel information should be preserved
val channelData = if (channel.isInstanceOf[PrivateChannel]) MCChatPrivate.lastmsgPerUser else MCChatCustom.lastmsgCustom
channelData.collectFirst({ case data if data.channel.getId.asLong == channel.getId.asLong => data })
.foreach(data => data.message = null)
//If the above didn't find anything, it's sending a message to a non-chat channel
}
def addStaticExcludedPlugin(event: Class[_ <: Event], plugin: String): util.HashSet[String] =
staticExcludedPlugins.compute(event, (_, hs: util.HashSet[String]) =>
if (hs == null) Sets.newHashSet(plugin) else if (hs.add(plugin)) hs else hs)
def callEventExcludingSome(event: Event): Unit = {
if (notEnabled) return ()
val second = staticExcludedPlugins.get(event.getClass)
val first = module.excludedPlugins.get
val both = if (second.isEmpty) first
else util.Arrays.copyOf(first, first.length + second.size)
var i = first.length
if (second.nonEmpty) {
for (plugin <- second.get.asScala) {
both(i) = plugin
i += 1
}
}
callEventExcluding(event, false, both: _*)
}
/**
* Calls an event with the given details.
* <p>
* This method only synchronizes when the event is not asynchronous.
*
* @param event Event details
* @param only Flips the operation and <b>includes</b> the listed plugins
* @param plugins The plugins to exclude. Not case sensitive.
*/
def callEventExcluding(event: Event, only: Boolean, plugins: String*): Unit = { // Copied from Spigot-API and modified a bit
if (event.isAsynchronous) {
if (Thread.holdsLock(Bukkit.getPluginManager)) throw new IllegalStateException(event.getEventName + " cannot be triggered asynchronously from inside synchronized code.")
if (Bukkit.getServer.isPrimaryThread) throw new IllegalStateException(event.getEventName + " cannot be triggered asynchronously from primary server thread.")
fireEventExcluding(event, only, plugins: _*)
}
else Bukkit.getPluginManager synchronized fireEventExcluding(event, only, plugins: _*)
}
private def fireEventExcluding(event: Event, only: Boolean, plugins: String*): Unit = {
val handlers = event.getHandlers // Code taken from SimplePluginManager in Spigot-API
val listeners = handlers.getRegisteredListeners
val server = Bukkit.getServer
for (registration <- listeners) { // Modified to exclude plugins
if (registration.getPlugin.isEnabled
&& !plugins.exists(only ^ _.equalsIgnoreCase(registration.getPlugin.getName))) try registration.callEvent(event)
catch {
case ex: AuthorNagException =>
val plugin = registration.getPlugin
if (plugin.isNaggable) {
plugin.setNaggable(false)
server.getLogger.log(Level.SEVERE, String.format("Nag author(s): '%s' of '%s' about the following: %s", plugin.getDescription.getAuthors, plugin.getDescription.getFullName, ex.getMessage))
}
case ex: Throwable =>
server.getLogger.log(Level.SEVERE, "Could not pass event " + event.getEventName + " to " + registration.getPlugin.getDescription.getFullName, ex)
}
}
}
/**
* Call it from an async thread.
*/
def callLoginEvents(dcp: DiscordConnectedPlayer): Unit = {
val loginFail = (kickMsg: String) => {
dcp.sendMessage("Minecraft chat disabled, as the login failed: " + kickMsg)
MCChatPrivate.privateMCChat(dcp.getChannel, start = false, dcp.getUser, dcp.getChromaUser)
} //Probably also happens if the user is banned or so
val event = new AsyncPlayerPreLoginEvent(dcp.getName, InetAddress.getLoopbackAddress, dcp.getUniqueId)
callEventExcludingSome(event)
if (event.getLoginResult ne AsyncPlayerPreLoginEvent.Result.ALLOWED) {
loginFail(event.getKickMessage)
return ()
}
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => {
def foo(): Unit = {
val ev = new PlayerLoginEvent(dcp, "localhost", InetAddress.getLoopbackAddress)
callEventExcludingSome(ev)
if (ev.getResult ne PlayerLoginEvent.Result.ALLOWED) {
loginFail(ev.getKickMessage)
return ()
}
callEventExcludingSome(new PlayerJoinEvent(dcp, ""))
dcp.setLoggedIn(true)
if (module != null) {
// TODO: ServerWatcher
module.log(dcp.getName + " (" + dcp.getUniqueId + ") logged in from Discord")
}
}
foo()
})
}
/**
* Only calls the events if the player is actually logged in
*
* @param dcp The player
* @param needsSync Whether we're in an async thread
*/
def callLogoutEvent(dcp: DiscordConnectedPlayer, needsSync: Boolean): Unit = {
if (!dcp.isLoggedIn) return ()
val event = new PlayerQuitEvent(dcp, "")
if (needsSync) callEventSync(event)
else callEventExcludingSome(event)
dcp.setLoggedIn(false)
if (module != null) {
module.log(dcp.getName + " (" + dcp.getUniqueId + ") logged out from Discord")
// TODO: ServerWatcher
}
}
private[mcchat] def callEventSync(event: Event) = Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => callEventExcludingSome(event))
class LastMsgData(val channel: MessageChannel, @Nullable val user: ChromaGamerBase) {
var message: Message = null
var time = 0L
var content: String = null
var mcchannel: component.channel.Channel = null
protected def this(channel: MessageChannel, mcchannel: component.channel.Channel, user: ChromaGamerBase) = {
this(channel, user)
this.mcchannel = mcchannel
}
}
}

View file

@ -0,0 +1,143 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.discordplugin.DPUtils.{FluxExtensions, MonoExtensions}
import buttondevteam.discordplugin._
import buttondevteam.discordplugin.mcchat.sender.{DiscordConnectedPlayer, DiscordPlayerSender, DiscordUser}
import buttondevteam.lib.TBMCSystemChatEvent
import buttondevteam.lib.player.{ChromaGamerBase, TBMCPlayer, TBMCPlayerBase, TBMCYEEHAWEvent}
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.Role
import discord4j.core.`object`.entity.channel.MessageChannel
import net.ess3.api.events.{AfkStatusChangeEvent, MuteStatusChangeEvent, NickChangeEvent, VanishStatusChangeEvent}
import org.bukkit.Bukkit
import org.bukkit.entity.Player
import org.bukkit.event.entity.PlayerDeathEvent
import org.bukkit.event.player.PlayerLoginEvent.Result
import org.bukkit.event.player._
import org.bukkit.event.server.{BroadcastMessageEvent, TabCompleteEvent}
import org.bukkit.event.{EventHandler, EventPriority, Listener}
import reactor.core.publisher.Flux
import reactor.core.scala.publisher.SMono
class MCListener(val module: MinecraftChatModule) extends Listener {
final private val muteRole = DPUtils.roleData(module.getConfig, "muteRole", "Muted")
@EventHandler(priority = EventPriority.HIGHEST) def onPlayerLogin(e: PlayerLoginEvent): Unit = {
if (e.getResult ne Result.ALLOWED) return ()
if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return ()
val dcp = MCChatUtils.LoggedInPlayers.get(e.getPlayer.getUniqueId)
if (dcp.nonEmpty) MCChatUtils.callLogoutEvent(dcp.get, needsSync = false)
}
@EventHandler(priority = EventPriority.MONITOR) def onPlayerJoin(e: PlayerJoinEvent): Unit = {
if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return () // Don't show the joined message for the fake player
Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, () => {
def foo(): Unit = {
val p = e.getPlayer
val dp = TBMCPlayerBase.getPlayer(p.getUniqueId, classOf[TBMCPlayer]).getAs(classOf[DiscordUser])
if (dp != null)
DiscordPlugin.dc.getUserById(Snowflake.of(dp.getDiscordID)).^^().flatMap(user =>
user.getPrivateChannel.^^().flatMap(chan => module.chatChannelMono.flatMap(cc => {
MCChatUtils.addSenderTo(MCChatUtils.OnlineSenders, dp.getDiscordID, DiscordPlayerSender.create(user, chan, p, module))
MCChatUtils.addSenderTo(MCChatUtils.OnlineSenders, dp.getDiscordID, DiscordPlayerSender.create(user, cc, p, module)) //Stored per-channel
SMono.empty
}))).subscribe()
val message = e.getJoinMessage
sendJoinLeaveMessage(message, e.getPlayer)
ChromaBot.updatePlayerList()
}
foo()
})
}
private def sendJoinLeaveMessage(message: String, player: Player): Unit =
if (message != null && message.trim.nonEmpty)
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(message), ChromaGamerBase.getFromSender(player), ChannelconBroadcast.JOINLEAVE, hookmsg = true).subscribe()
@EventHandler(priority = EventPriority.MONITOR) def onPlayerLeave(e: PlayerQuitEvent): Unit = {
if (e.getPlayer.isInstanceOf[DiscordConnectedPlayer]) return () // Only care about real users
MCChatUtils.OnlineSenders.filterInPlace((_, userMap) => userMap.entrySet.stream.noneMatch(_.getValue.getUniqueId.equals(e.getPlayer.getUniqueId)))
Bukkit.getScheduler.runTaskAsynchronously(DiscordPlugin.plugin, () => MCChatUtils.LoggedInPlayers.get(e.getPlayer.getUniqueId).foreach(MCChatUtils.callLoginEvents))
Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, () => ChromaBot.updatePlayerList(), 5)
val message = e.getQuitMessage
sendJoinLeaveMessage(message, e.getPlayer)
}
@EventHandler(priority = EventPriority.HIGHEST) def onPlayerKick(e: PlayerKickEvent): Unit = {
/*if (!DiscordPlugin.hooked && !e.getReason().equals("The server is restarting")
&& !e.getReason().equals("Server closed")) // The leave messages errored with the previous setup, I could make it wait since I moved it here, but instead I have a special
MCChatListener.forAllowedCustomAndAllMCChat(e.getPlayer().getName() + " left the game"); // message for this - Oh wait this doesn't even send normally because of the hook*/
}
@EventHandler(priority = EventPriority.LOW) def onPlayerDeath(e: PlayerDeathEvent): Unit =
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(e.getDeathMessage), ChromaGamerBase.getFromSender(e.getEntity), ChannelconBroadcast.DEATH, hookmsg = true).subscribe()
@EventHandler def onPlayerAFK(e: AfkStatusChangeEvent): Unit = {
val base = e.getAffected.getBase
if (e.isCancelled || !base.isOnline) return ()
val msg = base.getDisplayName + " is " + (if (e.getValue) "now"
else "no longer") + " AFK."
MCChatUtils.forAllowedCustomAndAllMCChat(MCChatUtils.send(msg), ChromaGamerBase.getFromSender(base), ChannelconBroadcast.AFK, hookmsg = false).subscribe()
}
@EventHandler def onPlayerMute(e: MuteStatusChangeEvent): Unit = {
val role = muteRole.get
if (role == null) return ()
val source = e.getAffected.getSource
if (!source.isPlayer) return ()
val p = TBMCPlayerBase.getPlayer(source.getPlayer.getUniqueId, classOf[TBMCPlayer]).getAs(classOf[DiscordUser])
if (p == null) return ()
DPUtils.ignoreError(DiscordPlugin.dc.getUserById(Snowflake.of(p.getDiscordID)).^^()
.flatMap(user => user.asMember(DiscordPlugin.mainServer.getId).^^())
.flatMap(user => role.flatMap((r: Role) => {
def foo(r: Role): SMono[_] = {
if (e.getValue) user.addRole(r.getId)
else user.removeRole(r.getId)
val modlog = module.modlogChannel.get
val msg = s"${(if (e.getValue) "M" else "Unm")}uted user: ${user.getUsername}#${user.getDiscriminator}"
module.log(msg)
if (modlog != null) return modlog.flatMap((ch: MessageChannel) => ch.createMessage(msg).^^())
SMono.empty
}
foo(r)
}))).subscribe()
}
@EventHandler def onChatSystemMessage(event: TBMCSystemChatEvent): Unit =
MCChatUtils.forAllowedMCChat(MCChatUtils.send(event.getMessage), event).subscribe()
@EventHandler def onBroadcastMessage(event: BroadcastMessageEvent): Unit = {
MCChatUtils.broadcastedMessages += ((event.getMessage, System.nanoTime()))
MCChatUtils.forCustomAndAllMCChat(MCChatUtils.send(event.getMessage), ChannelconBroadcast.BROADCAST, hookmsg = false).subscribe()
}
@EventHandler def onYEEHAW(event: TBMCYEEHAWEvent): Unit = { //TODO: Implement as a colored system message to have channel support
val name = event.getSender match {
case player: Player => player.getDisplayName
case _ => event.getSender.getName
}
//Channel channel = ChromaGamerBase.getFromSender(event.getSender()).channel().get(); - TODO
DiscordPlugin.mainServer.getEmojis.^^().filter(e => "YEEHAW" == e.getName).take(1).singleOrEmpty
.map(Option.apply).defaultIfEmpty(Option.empty)
.flatMap(yeehaw => MCChatUtils.forPublicPrivateChat(MCChatUtils.send(name +
yeehaw.map(guildEmoji => " <:YEEHAW:" + guildEmoji.getId.asString + ">s").getOrElse(" YEEHAWs")))).subscribe()
}
@EventHandler def onNickChange(event: NickChangeEvent): Unit = MCChatUtils.updatePlayerList()
@EventHandler def onTabComplete(event: TabCompleteEvent): Unit = {
val i = event.getBuffer.lastIndexOf(' ')
val t = event.getBuffer.substring(i + 1) //0 if not found
if (!t.startsWith("@")) return ()
val token = t.substring(1)
val x = DiscordPlugin.mainServer.getMembers.^^().flatMap(m => Flux.just(m.getUsername, m.getNickname.orElse("")))
.filter(_.startsWith(token)).map("@" + _).doOnNext(event.getCompletions.add(_)).blockLast()
}
@EventHandler def onVanish(event: VanishStatusChangeEvent): Unit = {
if (event.isCancelled) return ()
Bukkit.getScheduler.runTask(DiscordPlugin.plugin, () => MCChatUtils.updatePlayerList())
}
}

View file

@ -0,0 +1,222 @@
package buttondevteam.discordplugin.mcchat
import buttondevteam.core.component.channel.Channel
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin.mcchat.sender.DiscordConnectedPlayer
import buttondevteam.discordplugin.util.DPState
import buttondevteam.discordplugin.{ChannelconBroadcast, DPUtils, DiscordPlugin}
import buttondevteam.lib.architecture.Component
import buttondevteam.lib.architecture.config.IConfigData
import buttondevteam.lib.{TBMCCoreAPI, TBMCSystemChatEvent}
import com.google.common.collect.Lists
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.channel.MessageChannel
import discord4j.core.spec.EmbedCreateSpec
import discord4j.rest.util.Color
import org.bukkit.Bukkit
import reactor.core.scala.publisher.SMono
import java.util
import java.util.{Objects, UUID}
import scala.jdk.CollectionConverters.IterableHasAsScala
/**
* Provides Minecraft chat connection to Discord. Commands may be used either in a public chat (limited) or in a DM.
*/
object MinecraftChatModule {
var state = DPState.RUNNING
}
class MinecraftChatModule extends Component[DiscordPlugin] {
def getListener: MCChatListener = this.listener
private var listener: MCChatListener = null
private[mcchat] var serverWatcher = null
private var lpInjector = null
private[mcchat] var disabling = false
/**
* A list of commands that can be used in public chats - Warning: Some plugins will treat players as OPs, always test before allowing a command!
*/
def whitelistedCommands: IConfigData[util.ArrayList[String]] = getConfig.getData("whitelistedCommands",
Lists.newArrayList("list", "u", "shrug", "tableflip", "unflip", "mwiki", "yeehaw", "lenny", "rp", "plugins"))
/**
* The channel to use as the public Minecraft chat - everything public gets broadcasted here
*/
def chatChannel: IConfigData[Snowflake] = DPUtils.snowflakeData(getConfig, "chatChannel", 0L)
def chatChannelMono: SMono[MessageChannel] = DPUtils.getMessageChannel(chatChannel.getPath, chatChannel.get)
/**
* The channel where the plugin can log when it mutes a player on Discord because of a Minecraft mute
*/
def modlogChannel: IConfigData[SMono[MessageChannel]] = DPUtils.channelData(getConfig, "modlogChannel")
/**
* The plugins to exclude from fake player events used for the 'mcchat' command - some plugins may crash, add them here
*/
def excludedPlugins: IConfigData[Array[String]] = getConfig.getData("excludedPlugins", Array[String]("ProtocolLib", "LibsDisguises", "JourneyMapServer"))
/**
* If this setting is on then players logged in through the 'mcchat' command will be able to teleport using plugin commands.
* They can then use commands like /tpahere to teleport others to that place.<br />
* If this is off, then teleporting will have no effect.
*/
def allowFakePlayerTeleports: IConfigData[Boolean] = getConfig.getData("allowFakePlayerTeleports", false)
/**
* If this is on, each chat channel will have a player list in their description.
* It only gets added if there's no description yet or there are (at least) two lines of "----" following each other.
* Note that it will replace <b>everything</b> above the first and below the last "----" but it will only detect exactly four dashes.
* So if you want to use dashes for something else in the description, make sure it's either less or more dashes in one line.
*/
def showPlayerListOnDC: IConfigData[Boolean] = getConfig.getData("showPlayerListOnDC", true)
/**
* This setting controls whether custom chat connections can be <i>created</i> (existing connections will always work).
* Custom chat connections can be created using the channelcon command and they allow players to display town chat in a Discord channel for example.
* See the channelcon command for more details.
*/
def allowCustomChat: IConfigData[Boolean] = getConfig.getData("allowCustomChat", true)
/**
* This setting allows you to control if players can DM the bot to log on the server from Discord.
* This allows them to both chat and perform any command they can in-game.
*/
def allowPrivateChat: IConfigData[Boolean] = getConfig.getData("allowPrivateChat", true)
/**
* If set, message authors appearing on Discord will link to this URL. A 'type' and 'id' parameter will be added with the user's platform (Discord, Minecraft, ...) and ID.
*/
def profileURL: IConfigData[String] = getConfig.getData("profileURL", "")
/**
* Enables support for running vanilla commands through Discord, if you ever need it.
*/
def enableVanillaCommands: IConfigData[Boolean] = getConfig.getData("enableVanillaCommands", true)
/**
* Whether players logged on from Discord (mcchat command) should be recognised by other plugins. Some plugins might break if it's turned off.
* But it's really hacky.
*/
final private val addFakePlayersToBukkit = getConfig.getData("addFakePlayersToBukkit", false)
/**
* Set by the component to report crashes.
*/
final private val serverUp = getConfig.getData("serverUp", false)
final private val mcChatCommand = new MCChatCommand(this)
final private val channelconCommand = new ChannelconCommand(this)
override protected def enable(): Unit = {
if (DPUtils.disableIfConfigErrorRes(this, chatChannel, chatChannelMono)) return ()
listener = new MCChatListener(this)
TBMCCoreAPI.RegisterEventsForExceptions(listener, getPlugin)
TBMCCoreAPI.RegisterEventsForExceptions(new MCListener(this), getPlugin) //These get undone if restarting/resetting - it will ignore events if disabled
getPlugin.manager.registerCommand(mcChatCommand)
getPlugin.manager.registerCommand(channelconCommand)
val chcons = getConfig.getConfig.getConfigurationSection("chcons")
if (chcons == null) { //Fallback to old place
getConfig.getConfig.getRoot.getConfigurationSection("chcons")
}
if (chcons != null) {
val chconkeys = chcons.getKeys(false)
for (chconkey <- chconkeys.asScala) {
val chcon = chcons.getConfigurationSection(chconkey)
val mcch = Channel.getChannels.filter((ch: Channel) => ch.getIdentifier == chcon.getString("mcchid")).findAny
val ch = DiscordPlugin.dc.getChannelById(Snowflake.of(chcon.getLong("chid"))).block
val did = chcon.getLong("did")
val user = DiscordPlugin.dc.getUserById(Snowflake.of(did)).block
val groupid = chcon.getString("groupid")
val toggles = chcon.getInt("toggles")
val brtoggles = chcon.getStringList("brtoggles")
if (mcch.isPresent && ch != null && user != null && groupid != null) {
Bukkit.getScheduler.runTask(getPlugin, () => { //<-- Needed because of occasional ConcurrentModificationExceptions when creating the player (PermissibleBase)
val dcp = DiscordConnectedPlayer.create(user, ch.asInstanceOf[MessageChannel],
UUID.fromString(chcon.getString("mcuid")), chcon.getString("mcname"), this)
MCChatCustom.addCustomChat(ch.asInstanceOf[MessageChannel], groupid, mcch.get, user, dcp, toggles,
brtoggles.asScala.map(TBMCSystemChatEvent.BroadcastTarget.get).filter(Objects.nonNull).toSet)
()
})
}
}
}
// TODO: LPInjector
if (addFakePlayersToBukkit.get) try {
// TODO: Fake players
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("Failed to hack the server (object)! Disable addFakePlayersToBukkit in the config.", e, this)
}
if (MinecraftChatModule.state eq DPState.RESTARTING_PLUGIN) { //These will only execute if the chat is enabled
sendStateMessage(Color.CYAN, "Discord plugin restarted - chat connected.") //Really important to note the chat, hmm
MinecraftChatModule.state = DPState.RUNNING
}
else if (MinecraftChatModule.state eq DPState.DISABLED_MCCHAT) {
sendStateMessage(Color.CYAN, "Minecraft chat enabled - chat connected.")
MinecraftChatModule.state = DPState.RUNNING
}
else if (serverUp.get) {
sendStateMessage(Color.YELLOW, "Server started after a crash - chat connected.")
val thr = new Throwable("The server shut down unexpectedly. See the log of the previous run for more details.")
thr.setStackTrace(new Array[StackTraceElement](0))
TBMCCoreAPI.SendException("The server crashed!", thr, this)
}
else sendStateMessage(Color.GREEN, "Server started - chat connected.")
serverUp.set(true)
}
override protected def disable(): Unit = {
disabling = true
if (MinecraftChatModule.state eq DPState.RESTARTING_PLUGIN) sendStateMessage(Color.ORANGE, "Discord plugin restarting")
else if (MinecraftChatModule.state eq DPState.RUNNING) {
sendStateMessage(Color.ORANGE, "Minecraft chat disabled")
MinecraftChatModule.state = DPState.DISABLED_MCCHAT
}
else {
val kickmsg = if (Bukkit.getOnlinePlayers.size > 0)
DPUtils.sanitizeString(Bukkit.getOnlinePlayers.asScala.map(_.getDisplayName).mkString(", ")) +
(if (Bukkit.getOnlinePlayers.size == 1) " was " else " were ") + "thrown out" //TODO: Make configurable
else ""
if (MinecraftChatModule.state eq DPState.RESTARTING_SERVER) sendStateMessage(Color.ORANGE, "Server restarting", kickmsg)
else if (MinecraftChatModule.state eq DPState.STOPPING_SERVER) sendStateMessage(Color.RED, "Server stopping", kickmsg)
else sendStateMessage(Color.GRAY, "Unknown state, please report.")
//If 'restart' is disabled then this isn't shown even if joinleave is enabled}
}
serverUp.set(false) //Disable even if just the component is disabled because that way it won't falsely report crashes
try //If it's not enabled it won't do anything
if (serverWatcher != null) {
// TODO: ServerWatcher
}
catch {
case e: Exception =>
TBMCCoreAPI.SendException("Failed to restore the server object!", e, this)
}
val chcons = MCChatCustom.getCustomChats
val chconsc = getConfig.getConfig.createSection("chcons")
for (chcon <- chcons) {
val chconc = chconsc.createSection(chcon.channel.getId.asString)
chconc.set("mcchid", chcon.mcchannel.getIdentifier)
chconc.set("chid", chcon.channel.getId.asLong)
chconc.set("did", chcon.dcUser.getId.asLong)
chconc.set("mcuid", chcon.dcp.getUniqueId.toString)
chconc.set("mcname", chcon.dcp.getName)
chconc.set("groupid", chcon.groupID)
chconc.set("toggles", chcon.toggles)
chconc.set("brtoggles", chcon.brtoggles.map(_.getName).toList)
}
if (listener != null) { //Can be null if disabled because of a config error
listener.stop(true)
}
getPlugin.manager.unregisterCommand(mcChatCommand)
getPlugin.manager.unregisterCommand(channelconCommand)
disabling = false
}
/**
* It will block to make sure all messages are sent
*/
private def sendStateMessage(color: Color, message: String) =
MCChatUtils.forCustomAndAllMCChat(_.flatMap(ch => ch.createMessage(
EmbedCreateSpec.builder().color(color).title(message).build()).^^()
.onErrorResume(_ => SMono.empty)
), ChannelconBroadcast.RESTART, hookmsg = false).block()
private def sendStateMessage(color: Color, message: String, extra: String) =
MCChatUtils.forCustomAndAllMCChat(_.flatMap(ch => ch.createMessage(
EmbedCreateSpec.builder().color(color).title(message).description(extra).build()).^^()
.onErrorResume(_ => SMono.empty)
), ChannelconBroadcast.RESTART, hookmsg = false).block()
}

View file

@ -0,0 +1,235 @@
package buttondevteam.discordplugin.mcchat.sender
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
import discord4j.core.`object`.entity.User
import discord4j.core.`object`.entity.channel.MessageChannel
import net.kyori.adventure.text.Component
import org.bukkit._
import org.bukkit.attribute.{Attribute, AttributeInstance, AttributeModifier}
import org.bukkit.entity.{Entity, Player}
import org.bukkit.event.inventory.InventoryType
import org.bukkit.event.player.{AsyncPlayerChatEvent, PlayerTeleportEvent}
import org.bukkit.inventory.{Inventory, PlayerInventory}
import org.bukkit.permissions.{PermissibleBase, Permission, PermissionAttachment, PermissionAttachmentInfo}
import org.bukkit.plugin.Plugin
import org.mockito.Answers.RETURNS_DEFAULTS
import org.mockito.invocation.InvocationOnMock
import org.mockito.{MockSettings, Mockito}
import java.lang.reflect.Modifier
import java.util
import java.util._
object DiscordConnectedPlayer {
def create(user: User, channel: MessageChannel, uuid: UUID, mcname: String, module: MinecraftChatModule): DiscordConnectedPlayer =
Mockito.mock(classOf[DiscordConnectedPlayer], getSettings.useConstructor(user, channel, uuid, mcname, module))
def createTest: DiscordConnectedPlayer =
Mockito.mock(classOf[DiscordConnectedPlayer], getSettings.useConstructor(null, null))
private def getSettings: MockSettings = Mockito.withSettings.defaultAnswer((invocation: InvocationOnMock) => {
def foo(invocation: InvocationOnMock): AnyRef =
try {
if (!Modifier.isAbstract(invocation.getMethod.getModifiers))
invocation.callRealMethod
else
RETURNS_DEFAULTS.answer(invocation)
} catch {
case e: Exception =>
System.err.println("Error in mocked player!")
e.printStackTrace()
RETURNS_DEFAULTS.answer(invocation)
}
foo(invocation)
}).stubOnly
}
/**
* @constructor The parameters must match with [[DiscordConnectedPlayer.create]]
* @param user May be null.
* @param channel May not be null.
* @param uniqueId The UUID of the player.
* @param playerName The Minecraft name of the player.
* @param module The MinecraftChatModule or null if testing.
*/
abstract class DiscordConnectedPlayer(user: User, channel: MessageChannel, val uniqueId: UUID, val playerName: String, val module: MinecraftChatModule) extends DiscordSenderBase(user, channel) with IMCPlayer[DiscordConnectedPlayer] with Player {
private var loggedIn = false
private var dispName: String = playerName
private var location: Location = if (module == null) null else Bukkit.getWorlds.get(0).getSpawnLocation
private val basePlayer: OfflinePlayer = if (module == null) null else Bukkit.getOfflinePlayer(uniqueId)
private var perm: PermissibleBase = if (module == null) null else new PermissibleBase(basePlayer)
private val origPerm: PermissibleBase = perm
private val vanillaCmdListener = null // TODO
private val inventory: PlayerInventory = if (module == null) null else Bukkit.createInventory(this, InventoryType.PLAYER).asInstanceOf
override def isPermissionSet(name: String): Boolean = this.origPerm.isPermissionSet(name)
override def isPermissionSet(perm: Permission): Boolean = this.origPerm.isPermissionSet(perm)
override def hasPermission(inName: String): Boolean = this.origPerm.hasPermission(inName)
override def hasPermission(perm: Permission): Boolean = this.origPerm.hasPermission(perm)
override def addAttachment(plugin: Plugin, name: String, value: Boolean): PermissionAttachment = this.origPerm.addAttachment(plugin, name, value)
override def addAttachment(plugin: Plugin): PermissionAttachment = this.origPerm.addAttachment(plugin)
override def removeAttachment(attachment: PermissionAttachment): Unit = this.origPerm.removeAttachment(attachment)
override def recalculatePermissions(): Unit = this.origPerm.recalculatePermissions()
def clearPermissions(): Unit = this.origPerm.clearPermissions()
override def addAttachment(plugin: Plugin, name: String, value: Boolean, ticks: Int): PermissionAttachment =
this.origPerm.addAttachment(plugin, name, value, ticks)
override def addAttachment(plugin: Plugin, ticks: Int): PermissionAttachment = this.origPerm.addAttachment(plugin, ticks)
override def getEffectivePermissions: util.Set[PermissionAttachmentInfo] = this.origPerm.getEffectivePermissions
def setLoggedIn(loggedIn: Boolean): Unit = this.loggedIn = loggedIn
def setPerm(perm: PermissibleBase): Unit = this.perm = perm
override def setDisplayName(displayName: String): Unit = this.dispName = displayName
override def getVanillaCmdListener = this.vanillaCmdListener
def isLoggedIn: Boolean = this.loggedIn
override def getName: String = this.playerName
def getBasePlayer: OfflinePlayer = this.basePlayer
def getPerm: PermissibleBase = this.perm
override def getUniqueId: UUID = this.uniqueId
override def getDisplayName: String = this.dispName
/**
* For testing
*/
def this(user: User, channel: MessageChannel) =
this(user, channel, UUID.randomUUID(), "Test", null)
override def setOp(value: Boolean): Unit = { //CraftPlayer-compatible implementation
this.origPerm.setOp(value)
this.perm.recalculatePermissions()
}
override def isOp: Boolean = this.origPerm.isOp
override def teleport(location: Location): Boolean = {
if (module.allowFakePlayerTeleports.get) this.location = location
true
}
def teleport(location: Location, cause: PlayerTeleportEvent.TeleportCause): Boolean = {
if (module.allowFakePlayerTeleports.get) this.location = location
true
}
override def teleport(destination: Entity): Boolean = {
if (module.allowFakePlayerTeleports.get) this.location = destination.getLocation
true
}
def teleport(destination: Entity, cause: PlayerTeleportEvent.TeleportCause): Boolean = {
if (module.allowFakePlayerTeleports.get) this.location = destination.getLocation
true
}
override def getLocation(loc: Location): Location = {
if (loc != null) {
loc.setWorld(getWorld)
loc.setX(location.getX)
loc.setY(location.getY)
loc.setZ(location.getZ)
loc.setYaw(location.getYaw)
loc.setPitch(location.getPitch)
}
loc
}
override def getServer: Server = Bukkit.getServer
override def sendRawMessage(message: String): Unit = sendMessage(message)
override def chat(msg: String): Unit = Bukkit.getPluginManager.callEvent(new AsyncPlayerChatEvent(true, this, msg, new util.HashSet[Player](Bukkit.getOnlinePlayers)))
override def getWorld: World = Bukkit.getWorlds.get(0)
override def isOnline = true
override def getLocation = new Location(getWorld, location.getX, location.getY, location.getZ, location.getYaw, location.getPitch)
override def getEyeLocation: Location = getLocation
@deprecated override def getMaxHealth = 20d
override def getPlayer: DiscordConnectedPlayer = this
override def getAttribute(attribute: Attribute): AttributeInstance = new AttributeInstance() {
override def getAttribute: Attribute = attribute
override def getBaseValue: Double = getDefaultValue
override def setBaseValue(value: Double): Unit = {
}
override def getModifiers: util.Collection[AttributeModifier] = Collections.emptyList
override def addModifier(modifier: AttributeModifier): Unit = {
}
override def removeModifier(modifier: AttributeModifier): Unit = {
}
override def getValue: Double = getDefaultValue
override def getDefaultValue: Double = 20 //Works for max health, should be okay for the rest
}
override def getGameMode = GameMode.SPECTATOR
override def getInventory: PlayerInventory = inventory
override def name(): Component = Component.text(playerName)
//noinspection ScalaDeprecation
/*@SuppressWarnings(Array("deprecation")) override def spigot: super.Spigot = new super.Spigot() {
override def getRawAddress: InetSocketAddress = null
override def playEffect(location: Location, effect: Effect, id: Int, data: Int, offsetX: Float, offsetY: Float, offsetZ: Float, speed: Float, particleCount: Int, radius: Int): Unit = {
}
override def getCollidesWithEntities = false
override def setCollidesWithEntities(collides: Boolean): Unit = {
}
override def respawn(): Unit = {
}
override def getLocale = "en_us"
override def getHiddenPlayers: util.Set[Player] = Collections.emptySet
override def sendMessage(component: BaseComponent): Unit =
DiscordConnectedPlayer.super.sendMessage(component.toPlainText)
override def sendMessage(components: BaseComponent*): Unit =
for (component <- components)
sendMessage(component)
override def sendMessage(position: ChatMessageType, component: BaseComponent): Unit =
sendMessage(component) //Ignore position
override def sendMessage(position: ChatMessageType, components: BaseComponent*): Unit =
sendMessage(components: _*)
override def isInvulnerable = true
}*/
}

View file

@ -0,0 +1,39 @@
package buttondevteam.discordplugin.mcchat.sender
import buttondevteam.discordplugin.mcchat.MinecraftChatModule
import discord4j.core.`object`.entity.User
import discord4j.core.`object`.entity.channel.MessageChannel
import org.bukkit.entity.Player
import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import java.lang.reflect.Modifier
object DiscordPlayerSender {
def create(user: User, channel: MessageChannel, player: Player, module: MinecraftChatModule): DiscordPlayerSender =
Mockito.mock(classOf[DiscordPlayerSender], Mockito.withSettings.stubOnly.defaultAnswer((invocation: InvocationOnMock) => {
def foo(invocation: InvocationOnMock): AnyRef = {
if (!Modifier.isAbstract(invocation.getMethod.getModifiers))
invocation.callRealMethod
else
invocation.getMethod.invoke(invocation.getMock.asInstanceOf[DiscordPlayerSender].player, invocation.getArguments)
}
foo(invocation)
}).useConstructor(user, channel, player, module))
}
abstract class DiscordPlayerSender(user: User, channel: MessageChannel, var player: Player, val module: Nothing) extends DiscordSenderBase(user, channel) with IMCPlayer[DiscordPlayerSender] with Player {
override def getVanillaCmdListener = null
override def sendMessage(message: String): Unit = {
player.sendMessage(message)
super.sendMessage(message)
}
override def sendMessage(messages: String*): Unit = {
player.sendMessage(messages: _*)
super.sendMessage(messages: _*)
}
}

View file

@ -0,0 +1,68 @@
package buttondevteam.discordplugin.mcchat.sender
import buttondevteam.discordplugin.DPUtils.MonoExtensions
import buttondevteam.discordplugin.DiscordPlugin
import discord4j.core.`object`.entity.User
import discord4j.core.`object`.entity.channel.MessageChannel
import net.kyori.adventure.text.{Component, ComponentBuilder, TextComponent}
import org.bukkit.command.CommandSender
import org.bukkit.permissions.{PermissibleBase, Permission, PermissionAttachment, PermissionAttachmentInfo}
import org.bukkit.plugin.Plugin
import org.bukkit.{Bukkit, Server}
import reactor.core.publisher.Mono
import reactor.core.scala.publisher.SMono
import scala.jdk.OptionConverters._
import java.util
class DiscordSender(user: User, channel: MessageChannel, pname: String) extends DiscordSenderBase(user, channel) with CommandSender {
private val perm = new PermissibleBase(this)
private val senderName: String = Option(pname)
.orElse(Option(user).flatMap(u => u.asMember(DiscordPlugin.mainServer.getId).^^()
.onErrorResume(_ => SMono.empty).blockOption()
.map(u => u.getDisplayName)))
.getOrElse("Discord user")
def this(user: User, channel: MessageChannel) = {
this(user, channel, null)
}
override def isPermissionSet(name: String): Boolean = perm.isPermissionSet(name)
override def isPermissionSet(perm: Permission): Boolean = this.perm.isPermissionSet(perm)
override def hasPermission(name: String): Boolean = {
if (name.contains("essentials") && !(name == "essentials.list")) false
else perm.hasPermission(name)
}
override def hasPermission(perm: Permission): Boolean = this.perm.hasPermission(perm)
override def addAttachment(plugin: Plugin, name: String, value: Boolean): PermissionAttachment = perm.addAttachment(plugin, name, value)
override def addAttachment(plugin: Plugin): PermissionAttachment = perm.addAttachment(plugin)
override def addAttachment(plugin: Plugin, name: String, value: Boolean, ticks: Int): PermissionAttachment = perm.addAttachment(plugin, name, value, ticks)
override def addAttachment(plugin: Plugin, ticks: Int): PermissionAttachment = perm.addAttachment(plugin, ticks)
override def removeAttachment(attachment: PermissionAttachment): Unit = perm.removeAttachment(attachment)
override def recalculatePermissions(): Unit = perm.recalculatePermissions()
override def getEffectivePermissions: util.Set[PermissionAttachmentInfo] = perm.getEffectivePermissions
override def isOp = false
override def setOp(value: Boolean): Unit = {
}
override def getServer: Server = Bukkit.getServer
override def getName: String = senderName
override def name(): Component = Component.text(senderName)
//override def spigot(): CommandSender.Spigot = new CommandSender.Spigot
override def spigot(): CommandSender.Spigot = ???
}

View file

@ -0,0 +1,71 @@
package buttondevteam.discordplugin.mcchat.sender
import buttondevteam.discordplugin.mcchat.MCChatUtils
import buttondevteam.discordplugin.{DPUtils, DiscordPlugin}
import buttondevteam.lib.TBMCCoreAPI
import buttondevteam.lib.player.ChromaGamerBase
import discord4j.core.`object`.entity.User
import discord4j.core.`object`.entity.channel.MessageChannel
import org.bukkit.Bukkit
import org.bukkit.command.CommandSender
import org.bukkit.scheduler.BukkitTask
import java.util.UUID
/**
*
* @param user May be null.
* @param channel May not be null.
*/
abstract class DiscordSenderBase protected(var user: User, var channel: MessageChannel) extends CommandSender { // TODO: Move most of this to DiscordUser
private var msgtosend = ""
private var sendtask: BukkitTask = null
/**
* Returns the user. May be null.
*
* @return The user or null.
*/
def getUser: User = user
def getChannel: MessageChannel = channel
private var chromaUser: DiscordUser = null
/**
* Loads the user data on first query.
*
* @return A Chroma user of Discord or a Discord user of Chroma
*/
def getChromaUser: DiscordUser = {
if (chromaUser == null) chromaUser = ChromaGamerBase.getUser(user.getId.asString, classOf[DiscordUser])
chromaUser
}
override def sendMessage(message: String): Unit = try {
val broadcast = MCChatUtils.broadcastedMessages.contains(message);
if (broadcast) { //We're catching broadcasts using the Bukkit event
if (MCChatUtils.broadcastedMessages.size >= 4) { // We really don't need to store messages for long
MCChatUtils.broadcastedMessages.filterInPlace((_, time) => time > System.nanoTime() - 1000 * 1000 * 1000)
}
return ()
}
val sendmsg = DPUtils.sanitizeString(message)
this synchronized {
msgtosend += "\n" + sendmsg
if (sendtask == null) sendtask = Bukkit.getScheduler.runTaskLaterAsynchronously(DiscordPlugin.plugin, (() => {
channel.createMessage((if (user != null) user.getMention + "\n" else "") + msgtosend.trim).subscribe()
sendtask = null
msgtosend = ""
}): Runnable, 4) // Waits a 0.2 second to gather all/most of the different messages
}
} catch {
case e: Exception =>
TBMCCoreAPI.SendException("An error occured while sending message to DiscordSender", e, DiscordPlugin.plugin)
}
override def sendMessage(messages: String*): Unit = sendMessage(messages.mkString("\n"))
override def sendMessage(sender: UUID, message: String): Unit = sendMessage(message)
override def sendMessage(sender: UUID, messages: String*): Unit = sendMessage(messages : _*)
}

View file

@ -0,0 +1,38 @@
package buttondevteam.discordplugin.mcchat.sender
import buttondevteam.core.component.channel.Channel
import buttondevteam.discordplugin.DiscordPlugin
import buttondevteam.discordplugin.mcchat.MCChatPrivate
import buttondevteam.lib.player.{ChromaGamerBase, UserClass}
import discord4j.common.util.Snowflake
import discord4j.core.`object`.entity.User
@UserClass(foldername = "discord") class DiscordUser() extends ChromaGamerBase {
private var did: String = null
private var discordUser: User = null
// private @Getter @Setter boolean minecraftChatEnabled;
def getDiscordID: String = {
if (did == null) did = getFileName
did
}
def getDiscordUser: User = {
if (discordUser == null) discordUser = DiscordPlugin.dc.getUserById(Snowflake.of(getDiscordID)).block() // TODO: Don't do it blocking
discordUser
}
/**
* Returns true if player has the private Minecraft chat enabled. For setting the value, see
* [[MCChatPrivate.privateMCChat]]
*/
def isMinecraftChatEnabled: Boolean = MCChatPrivate.isMinecraftChatEnabled(this)
override def checkChannelInGroup(s: String): Channel.RecipientTestResult = ???
override def sendMessage(message: String): Unit = ??? // TODO: Somehow check which message is this a response to
override def sendMessage(message: Array[String]): Unit = ???
override def getName: String = ???
}

View file

@ -0,0 +1,7 @@
package buttondevteam.discordplugin.mcchat.sender
import org.bukkit.entity.Player
trait IMCPlayer[T] extends Player {
def getVanillaCmdListener: Null // TODO
}

Some files were not shown because too many files have changed in this diff Show more