Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
994057e
Add Chat-Bubbles Feature
Wueffi Apr 26, 2026
50c0923
Remove unnecessary (mb) line
Wueffi Apr 26, 2026
419034b
Ready for another review round!
Wueffi Apr 27, 2026
378d034
Changes!
Wueffi Apr 27, 2026
1b3effb
Changes!
Wueffi Apr 27, 2026
828c84d
Merge remote-tracking branch 'bubbles/master'
Wueffi Apr 27, 2026
250be24
The review of the review
Wueffi May 3, 2026
7a48000
Another round of review
Wueffi May 4, 2026
62418d2
Fix handleCommandException
paulikauro May 3, 2026
06c79bd
Refactor sender and prefix construction into one method
paulikauro May 4, 2026
7cf002b
Small simplification
paulikauro May 4, 2026
0caa0a4
Remove circular dependency as best as possible
Wueffi May 25, 2026
74a76ae
Make shouting go through chat confirmations and fix two other small t…
Wueffi May 25, 2026
ea8e6d4
Reformat code!
Wueffi May 27, 2026
f56ca1a
Update Gradle (8.14.1 -> 9.5.1)
paulikauro May 31, 2026
e082e7b
Update Gradle shadow plugin (8.3.6 -> 9.4.2)
paulikauro May 31, 2026
553d3f7
Prevent test task from failing if no tests are discovered
paulikauro May 31, 2026
dc971a2
Add trailing comma
paulikauro May 31, 2026
c559499
Add chat listener registration back
paulikauro May 31, 2026
7741c16
Remove some type annotations and @Single annotations
paulikauro May 31, 2026
261ca16
Use playerOrNull, merge else if
paulikauro May 31, 2026
061dc6a
Convert getBubbles() to property instead
paulikauro May 31, 2026
becb6fd
Fix bubble list formatting
paulikauro May 31, 2026
899c06d
Add hover with bubble info on bubble messages
paulikauro May 31, 2026
db1de3c
Convert to expression body
paulikauro May 31, 2026
474ef60
Tweak bubble messages a little
paulikauro May 31, 2026
0e1571f
Add join button to bubble invites
paulikauro May 31, 2026
d40cfd2
Hide Discord chat when in bubble and showGlobalChat is off
paulikauro May 31, 2026
9c3df33
Merge branch 'master'
paulikauro Jun 3, 2026
0461665
Tweak bubble commands and update README accordingly
paulikauro Jun 3, 2026
26e9637
Add bubble command help and command descriptions
paulikauro Jun 4, 2026
1048382
Add bubble chat to commandspy
paulikauro Jun 11, 2026
9da2277
Refactor chat confirmations to fix bubble confirms going to global chat
paulikauro Jun 11, 2026
1d5bfb5
Add sendError and recolor info messages
paulikauro Jun 11, 2026
a129464
Prevent confirmations from proceeding when bubble no longer exists
paulikauro Jun 11, 2026
f71d89a
Handle global chat confirmations uniformly
paulikauro Jun 11, 2026
9600c18
Bump version
paulikauro Jun 11, 2026
2a383bf
Small cleanups
paulikauro Jun 11, 2026
32d45b7
Move shout back to Bubble.kt and echo message when global chat disabled
paulikauro Jun 11, 2026
b0a202d
Refactor spying to encapsulate spy prefix into Spying.kt
paulikauro Jun 12, 2026
bc6881f
Use bubble manager permission more in commands
paulikauro Jun 12, 2026
feb7ea6
Use UserCache when listing bubble participants
paulikauro Jun 15, 2026
4f44c9e
Add bubble members to invite, cleanup
paulikauro Jun 16, 2026
944119d
Move some command completions into better locations
paulikauro Jun 16, 2026
f30c534
Add chat confirmations to DM
paulikauro Jun 16, 2026
bcac587
Extract chat confirmations into their own file
paulikauro Jun 16, 2026
afd0950
Cache settings in memory to reduce database traffic
paulikauro Jun 16, 2026
c8d5d1d
Separate spying into command and social spy
paulikauro Jun 16, 2026
8983b98
Add /msg to social spy
paulikauro Jun 16, 2026
dbf4a7b
Compress Message.kt
paulikauro Jun 16, 2026
523f287
Make BUBBLE_OWNED consistent in casing with other constants
paulikauro Jun 16, 2026
1e41f4d
Do not show syntax when not in a bubble
paulikauro Jun 16, 2026
121ce6d
Replace Component#append with Component#textOfChildren
paulikauro Jun 16, 2026
4db62f3
Bubble create and invite take list of players to invite
paulikauro Jun 16, 2026
6583432
Refuse invites to public bubbles
paulikauro Jun 17, 2026
fd19901
Small refactor
paulikauro Jun 17, 2026
0c9a6a8
Move setting default values into Setting class
paulikauro Jun 17, 2026
756ff8b
Update Bubble command descriptions and README
paulikauro Jun 17, 2026
09f8703
Optimize imports
paulikauro Jun 17, 2026
222c5c4
Enforce more trailing commas
paulikauro Jun 17, 2026
984d0b5
Add .kotlin to gitignore
paulikauro Jun 17, 2026
722a029
Add chat confirmations to funcommands, helpop, mail and player profile
paulikauro Jun 19, 2026
b5ad8c2
Fix chat confirmations having stale Player objects and add socialspy …
paulikauro Jun 19, 2026
8017078
Update Gradle and Kotlin
paulikauro Jun 27, 2026
88b6852
Use explicit backing field in BubbleManager
paulikauro Jun 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
.idea/*
!.idea/codeStyles/
build/
.kotlin/
4 changes: 4 additions & 0 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ Because we want to have a chat system that actually wOREks for us.
| `/profile about <player>` | `chattore.profile.about` | Set your about | `/playerprofile` |
| `/profile setabout <player> <about>` | `chattore.profile.about.others` | Set another player's about | `/playerprofile` |

## Bubble Commands

| Command | Permission | Description | Aliases |
|------------------------------------|--------------------------|--------------------------------------------------------|------------------------|
| `/bubble create [players]` | `chattore.bubble` | Create ("blow") a bubble and invite players | `/bb create\|/bb blow` |
| `/bubble invite <players>` | `chattore.bubble` | Invite players to your bubble (if it is private) | `/bb invite` |
| `/bubble join <player>` | `chattore.bubble` | Join a player's bubble | `/bb join` |
| `/bubble leave` | `chattore.bubble` | Leave your current bubble | `/bb leave` |
| `/bubble delete` | `chattore.bubble` | Delete ("pop") your own bubble | `/bb delete\|/bb pop` |
| `/bubble kick <player>` | `chattore.bubble` | Kick a player from your own bubble | `/bb kick` |
| `/bubble setprivate <boolean>` | `chattore.bubble` | Set the visibility of your own bubble | `/bb setprivate` |
| `/bubble list` | `chattore.bubble` | List all bubbles | `/bb list` |
| `/shout <message>` | `chattore.bubble` | Send a message to global chat when in a bubble | No aliases |
| `/bubble burst <player>` | `chattore.bubble.manage` | Burst (delete) someone's bubble | `/bb burst` |
| `/bubble showglobalchat <boolean>` | `chattore.bubble` | Control the visibility of global chat when in a bubble | `/bb sgc` |

## Other Commands

| Command | Permission | Description | Aliases |
Expand Down
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ plugins {
alias(libs.plugins.pluginYml) apply false
alias(libs.plugins.buildconfig) apply false
}

allprojects {
tasks.withType<Test> {
failOnNoDiscoveredTests = false
}
}
50 changes: 24 additions & 26 deletions chattore/src/main/kotlin/ChattORE.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package org.openredstone.chattore

import co.aikar.commands.BaseCommand
import co.aikar.commands.CommandIssuer
import co.aikar.commands.RegisteredCommand
import co.aikar.commands.VelocityCommandManager
import co.aikar.commands.*
import co.aikar.commands.velocity.contexts.OnlinePlayer
import com.google.inject.Inject
import com.velocitypowered.api.event.Subscribe
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent
import com.velocitypowered.api.plugin.Dependency
import com.velocitypowered.api.plugin.Plugin
import com.velocitypowered.api.plugin.annotation.DataDirectory
import com.velocitypowered.api.proxy.Player
import com.velocitypowered.api.proxy.ProxyServer
import net.luckperms.api.LuckPermsProvider
import org.openredstone.chattore.feature.*
Expand All @@ -26,12 +23,12 @@ import kotlin.io.path.exists
url = "https://openredstone.org",
description = "Because we want to have a chat system that actually wOREks for us.",
authors = ["Nickster258", "PaukkuPalikka", "StackDoubleFlow", "sodiboo", "Waffle [Wueffi]"],
dependencies = [Dependency(id = "luckperms")]
dependencies = [Dependency(id = "luckperms")],
)
class ChattORE @Inject constructor(
private val proxy: ProxyServer,
private val logger: Logger,
@DataDirectory private val dataFolder: Path,
@param:DataDirectory private val dataFolder: Path,
) {
@Subscribe
fun onProxyInitialization(event: ProxyInitializeEvent) {
Expand All @@ -42,33 +39,37 @@ class ChattORE @Inject constructor(
val pluginScope = PluginScope(this, ChattORE::class.java, proxy, dataFolder, logger, commandManager)
commandManager.apply {
setDefaultExceptionHandler(::handleCommandException, false)
commandCompletions.registerCompletion("username") { listOf(it.player.username) }
commandCompletions.registerStaticCompletion("boolean", arrayOf("true", "false"))
commandCompletions.setDefaultCompletion("boolean", Boolean::class.java)
commandCompletions.setDefaultCompletion("players", OnlinePlayer::class.java)
@Suppress("DEPRECATION")
enableUnstableAPI("help")
}
pluginScope.apply {
val emojis = createEmojiFeature()
val messenger = createMessenger(emojis, database, luckPerms, config.format.global)
val userCache = createUserCache(database.database)
val wiretap = createSpyingFeature(database, config.format)
val messenger = createMessenger(emojis, database, luckPerms, config.format, wiretap, userCache)
val chatConfirmations = createChatConfirmations(ChatConfirmationConfig(config.regexes))
val bubbleManager = createBubbleFeature(messenger, database, chatConfirmations, config.format, userCache)
createAliasFeature()
createChatFeature(
messenger,
ChatConfirmationConfig(config.regexes),
)
createChatFeature(messenger, chatConfirmations, bubbleManager)
createChattoreFeature()
createDiscordFeature(messenger, emojis, config.discord)
createFunCommandsFeature()
createHelpOpFeature()
createFunCommandsFeature(chatConfirmations)
createHelpOpFeature(chatConfirmations)
createJoinLeaveFeature(config.format)
createMailFeature(database, userCache)
createMessageFeature(messenger)
createMailFeature(database, userCache, chatConfirmations, wiretap)
createMessageFeature(messenger, chatConfirmations, wiretap)
createNicknameFeature(
database, userCache, NicknameConfig(
database, userCache,
NicknameConfig(
config.clearNicknameOnChange,
// IDK, this when config
config.nicknamePresets.mapValues { (_, v) -> NickPreset(v) }.toSortedMap(),
)
),
)
createProfileFeature(database, luckPerms, userCache)
createSpyingFeature(database)
createProfileFeature(database, luckPerms, userCache, chatConfirmations)
}
}

Expand All @@ -91,11 +92,8 @@ class ChattORE @Inject constructor(
): Boolean {
val exception = throwable as? ChattoreException ?: return false
val message = exception.message ?: "Something went wrong!"
if (sender is Player) {
sender.sendSimpleS("<b><red>Oh NO ! </red></b><gray>:</gray> <red><message></red>", message)
} else {
sender.sendMessage("Error: $message")
}
// cast ok because we're running on Velocity
(sender as VelocityCommandIssuer).issuer.sendError(message)
return true
}
// TODO reloading functionality
Expand Down
5 changes: 4 additions & 1 deletion chattore/src/main/kotlin/ChattOREConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ data class ChattOREConfig(
)

data class FormatConfig(
val global: String = "<prefix> <gray>|</gray> <sender><gray>:</gray> <message>",
val chatMessage: String = "<prefix> <gray>|</gray> <sender><gray>:</gray> <message>",
val join: String = "<yellow><player> has joined the network",
val leave: String = "<yellow><player> has left the network",
val bubblePrefix: String = "<bubble_info>\uD83D\uDCAC</bubble_info>",
Comment thread
Nickster258 marked this conversation as resolved.
val spyPrefix: String = "<hover:show_text:'Spy'>\uD83D\uDD75</hover>",
val shoutPrefix: String = "<hover:show_text:'You've disabled global chat in bubbles. You won't see if anyone responds.'>\uD83D\uDD15</hover>",
val joinDiscord: String = "**%player% has joined the network**",
val leaveDiscord: String = "**%player% has left the network**",
)
11 changes: 10 additions & 1 deletion chattore/src/main/kotlin/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,17 @@ private val removeUnnecessaryStuffAndReorganize: Migration = {
}
}

private val migrations = arrayOf<Migration>(
private val bubbleIntroduction: Migration = {
// format.global -> format.chatMessage
getObject("format").apply {
replace("chatMessage", get("global"))
remove("global")
}
}

private val migrations = arrayOf(
removeUnnecessaryStuffAndReorganize,
bubbleIntroduction,
)

private val currentConfigVersion: ConfigVersion = migrations.size
Expand Down
80 changes: 57 additions & 23 deletions chattore/src/main/kotlin/Messenger.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,42 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.Component.space
import net.kyori.adventure.text.TextReplacementConfig
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer
import net.luckperms.api.LuckPerms
import org.openredstone.chattore.feature.DiscordBroadcastEvent
import org.openredstone.chattore.feature.Emojis
import org.openredstone.chattore.feature.NickPreset
import org.openredstone.chattore.feature.*
import org.slf4j.Logger
import java.net.URI
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import kotlin.jvm.optionals.getOrNull

fun PluginScope.createMessenger(
emojis: Emojis,
database: Storage,
luckPerms: LuckPerms,
chatBroadcastFormat: String,
formatConfig: FormatConfig,
wiretap: Wiretap,
userCache: UserCache,
): Messenger {
val fileTypeMap = Json.parseToJsonElement(loadResourceAsString("filetypes.json"))
.jsonObject.mapValues { (_, value) -> value.jsonArray.map { it.jsonPrimitive.content } }
.onEach { (key, values) -> logger.info("Loaded ${values.size} of type $key") }
return Messenger(emojis, proxy, database, luckPerms, chatBroadcastFormat, fileTypeMap)
return Messenger(emojis, proxy, database, luckPerms, formatConfig, fileTypeMap, wiretap, logger, userCache)
}

class Messenger(
emojis: Emojis,
private val proxy: ProxyServer,
private val database: Storage,
private val luckPerms: LuckPerms,
private val chatBroadcastFormat: String,
private val formatConfig: FormatConfig,
private val fileTypeMap: Map<String, List<String>>,
private val wiretap: Wiretap,
private val logger: Logger,
private val userCache: UserCache,
) {
private val urlRegex = """<?((http|https)://([\w_-]+(?:\.[\w_-]+)+)([^\s'<>]+)?)>?""".toRegex()

Expand All @@ -44,6 +53,7 @@ class Messenger(
formatReplacement("~~", "st"),
buildEmojiReplacement(emojis),
)
val excludedFromGlobalChat: MutableSet<UUID> = ConcurrentHashMap.newKeySet()

private fun formatReplacement(key: String, tag: String): TextReplacementConfig =
TextReplacementConfig.builder()
Expand All @@ -67,36 +77,60 @@ class Messenger(
}
.build()

fun broadcastChatMessage(originServer: String, player: Player, message: String) {
val userId = player.uniqueId
val userManager = luckPerms.userManager
val luckUser = userManager.getUser(userId) ?: return
val name = database.getNickname(userId) ?: NickPreset(player.username)
val sender =
"<hover:show_text:'${player.username} | <i>Click for more</i>'><click:run_command:'/playerprofile info ${player.username}'><message></click></hover>"
.renderSimpleC(name.render(player.username))

private fun formatPrefix(player: Player): Component {
val luckUser = luckPerms.userManager.getUser(player.uniqueId)!! // online users guaranteed to be loaded
val prefix = luckUser.cachedData.metaData.prefix
?: luckUser.primaryGroup.replaceFirstChar(Char::uppercaseChar)
return prefix.legacyDeserialize()
}

val compoPrefix = prefix.legacyDeserialize()
proxy.all.sendRichMessage(
chatBroadcastFormat,
"message" toC prepareChatMessage(message, player),
"sender" toC sender,
"prefix" toC compoPrefix,
)
private fun formatSender(player: Player): Component {
val name = database.getNickname(player.uniqueId) ?: NickPreset(player.username)
return "<hover:show_text:'${player.username} | <i>Click for more</i>'><click:run_command:'/playerprofile info ${player.username}'><message></click></hover>"
.renderSimpleC(name.render(player.username))
}

fun formatChatMessage(
message: String,
player: Player,
sender: Component = formatSender(player),
prefix: Component = formatPrefix(player),
) = formatConfig.chatMessage.render(
"message" toC prepareChatMessage(message, player),
"sender" toC sender,
"prefix" toC prefix,
)

val globalChat = proxy.all { it.uniqueId !in excludedFromGlobalChat }

fun broadcastChatMessage(player: Player, message: String) {
logger.info("${player.username} (${player.uniqueId}): $message")
val originServer = player.currentServer.getOrNull()?.serverInfo?.name ?: "VOID"
val compoPrefix = formatPrefix(player)
globalChat.sendMessage(formatChatMessage(message, player, prefix = compoPrefix))

val plainPrefix = PlainTextComponentSerializer.plainText().serialize(compoPrefix)
val discordBroadcast = DiscordBroadcastEvent(
plainPrefix,
player.username,
originServer,
message
message,
)
proxy.eventManager.fireAndForget(discordBroadcast)
}

fun broadcastBubbleMessage(player: Player, message: String, bubble: Bubble) {
logger.info("[Bubble] ${player.username} (${player.uniqueId}): $message")
val formattedMessage = formatChatMessage(message, player)
val bubbleInfo = Placeholder.styling("bubble_info", bubble.formatInfo(userCache))
val renderedMessage =
Component.textOfChildren(formatConfig.bubblePrefix.render(bubbleInfo), space(), formattedMessage)
bubble.players.forEach { uuid ->
proxy.playerOrNull(uuid)?.sendMessage(renderedMessage)
}
wiretap(renderedMessage)
}

fun prepareChatMessage(
message: String,
player: Player?,
Expand Down
Loading