Skip to content

Commit

Permalink
Merge pull request #37 from Mineaurion/url-pr
Browse files Browse the repository at this point in the history
Will Closes #31
  • Loading branch information
Yann151924 authored Jan 27, 2024
2 parents b1628e2 + 2bec2cf commit 76090d9
Show file tree
Hide file tree
Showing 20 changed files with 470 additions and 69 deletions.
41 changes: 29 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,21 @@

### What is it ?


AurionChat is a cross chat plugin beetween server for bukkit and sponge. We can describe as a chat channel plugin too. This plugin work with rabbitmq to transfer the message beetween the server if you don't know what i'm talking please refer to [this](https://www.rabbitmq.com/)
AurionChat is a cross chat plugin beetween server for bukkit and sponge. We can describe as a chat channel plugin too. This plugin work with rabbitmq to transfer the message beetween the server if you don't know what i'm talking please refer to [this](https://www.rabbitmq.com/)

### How it works ?

When you send the message if other people listen to the same channel as you typed, they will receive the message. The same apply for you :)

### Features


- Chat channel
- Formating chat
- Bukkit 1.7 to 1.20.1 - Sponge 8 - Fabric 5.1.20 - Forge 1.16 and 1.18 support
- Bukkit 1.7 to 1.20.1 - Sponge 8 - Fabric 5.1.20 - Forge 1.16 and 1.18 support
- Automessage

### Commands


- `channel` : List the channel listened
- `channel join <channelName>` : Listen and Join the desired channel
- `channel leave <channelName>` : Leave the desired channel
Expand All @@ -33,7 +30,6 @@ When you send the message if other people listen to the same channel as you type
- `aurionchat.joinchannel.<channelName>` : Auto join the channel
- `aurionchat.listenchannel.<channelName>` : Listen to the channel


---

## Chat
Expand All @@ -46,6 +42,7 @@ You can have more info about the uri here : https://www.rabbitmq.com/uri-spec.ht
The plugin/mod assume that a global channel is always defined and player who join the server will listen to the channel and the channel of the server by default

This an example to declare a channel :

```
channels:
#You can put any alphanumeric name you want.
Expand All @@ -64,9 +61,10 @@ At this time you can use only this token :
You can put any color code beetwen the token or any characters if you want.
```

## Forge 1.16

The mod need FTBRanks to be working with a minimal version of 1605.1.5.
The mod need FTBRanks to be working with a minimal version of 1605.1.5.

The config file is the same syntax as the sponge one.

Expand All @@ -88,7 +86,7 @@ java.lang.NoClassDefFoundError: org/slf4j/spi/SLF4JServiceProvider
```

You need to delete this folder from your server : `libraries/org/apache/logging/log4j/log4j-slf4j18-impl` and you can start again the server.

</details>

## Forge 1.18
Expand All @@ -101,16 +99,36 @@ Follow the [1.16](https://github.com/Mineaurion/aurionchat#forge-116) section fo

The mod need [Luckperms](https://modrinth.com/mod/luckperms/versions) to be working with a minimal version of 5.1.20. The config file is the same syntax as the sponge one.

## URL Mode

Theres a variety of options available for handling URLs. Options are as follows:

The option are based on enum in the code : https://github.com/Mineaurion/Aurionchat/blob/master/common/src/main/java/com/mineaurion/aurionchat/common/Utils.java#L24

| Name | Value | Description |
|-------------------------|------------------------|--------------------------------------------------------------------|
| Disallow URL and Domain | `DISALLOW` | Disallow domain and url and replace it with "[url removed]" |
| Disallow URL | `DISALLOW_URL` | Disallow only url and domain remain intact |
| Display only Domain | `DISPLAY_ONLY_DOMAINS` | Replace any url with only the domain, the url is instact for click |
| Force Https | `FORCE_HTTPS` | Replace any http url with https instead |
| Allow only domain click | `CLICK_DOMAIN` | Domain will be only clickable |
| Allow URLs | `ALLOW` | Allow url to be clickable and domain |


The `options.url_mode` configuration value is an array so you can add multiple to value to it. Be careful to respect the syntax of the value or the config will not work.

## Automessage

For this part to work you need to install this [plugin](https://github.com/Mineaurion/AurionChat-AutoMessage) on one of our server.
For this part to work you need to install this [plugin](https://github.com/Mineaurion/AurionChat-AutoMessage) on one of our server.

You can disable the automessage for one server in the configurtion just switch this :

```
enable=true
```

To this :
To this :

```
enable=false
```
Expand All @@ -121,7 +139,6 @@ Permission of every automessage channel is : `aurionchat.automessage.<channelNam

If you need support regarding our plugin, come on our [discord](https://discord.gg/Zn4ZbP9)


### Development

For debugging purpose and test, there is a docker-compose.yaml available to setup a simple rabbitmq
For debugging purpose and test, there is a docker-compose.yaml available to setup a simple rabbitmq
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public Map<String, Channel> getChannels(){
channel,
new Channel(
this.configuration.getString("channels." + channel + ".format"),
this.configuration.getString("channels." + channel + ".alias")
this.configuration.getString("channels." + channel + ".alias"),
stringListToUrlMode(this.configuration.getStringList("channels." + channel + ".url_mode"))
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.mineaurion.aurionchat.bukkit.AurionChat;
import com.mineaurion.aurionchat.common.AurionChatPlayer;
import com.mineaurion.aurionchat.common.ChatService;
import com.mineaurion.aurionchat.common.Utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
Expand Down Expand Up @@ -37,7 +36,8 @@ public void onAsyncPlayerChatEvent(AsyncPlayerChatEvent event){
Component messageFormat = Utils.processMessage(
plugin.getConfigurationAdapter().getChannels().get(currentChannel).format,
LegacyComponentSerializer.legacy('&').deserialize(event.getMessage()).asComponent(),
aurionChatPlayer
aurionChatPlayer,
plugin.getConfigurationAdapter().getChannels().get(currentChannel).urlMode
);

try{
Expand Down
5 changes: 4 additions & 1 deletion bukkit/src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ channels:
global:
format: "[GLOBAL] {prefix}{display_name} : &f{message}"
alias: "g"
url_mode: ["ALLOW"] # check https://github.com/Mineaurion/Aurionchat/#url-mode for reference
infinity:
format: "[&6I&f] {prefix}{display_name} : &f{message}"
alias: "inf"
url_mode: ["ALLOW"] # check https://github.com/Mineaurion/Aurionchat/#url-mode for reference
revelation:
format: "[&5R&f] {prefix}{display_name} : &f{message}"
alias: "reve"
alias: "reve"
url_mode: ["ALLOW"] # check https://github.com/Mineaurion/Aurionchat/#url-mode for reference
7 changes: 6 additions & 1 deletion common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ plugins {
}

test {
useJUnitPlatform {}
useJUnit()
include '**/Test*.class'
}

jacocoTestReport {
Expand All @@ -15,6 +16,7 @@ dependencies {

compileOnly 'com.google.code.gson:gson:2.7'
compileOnly 'com.google.guava:guava:19.0'
compileOnly 'org.jetbrains:annotations:21.+'

api('net.kyori:adventure-api:4.11.0') {
exclude(module: 'adventure-bom')
Expand Down Expand Up @@ -52,4 +54,7 @@ dependencies {
api('org.spongepowered:configurate-yaml:4.1.2')
api('org.spongepowered:configurate-gson:4.1.2')
api('org.spongepowered:configurate-hocon:4.1.2')

testImplementation('junit:junit:4.13.2')
testImplementation('org.easymock:easymock:3.6')
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import com.mineaurion.aurionchat.common.config.ConfigurationAdapter;
import com.mineaurion.aurionchat.common.logger.PluginLogger;
import com.mineaurion.aurionchat.common.player.PlayerFactory;
import net.luckperms.api.LuckPermsProvider;

import java.io.IOException;
import java.io.InputStream;
Expand Down
148 changes: 117 additions & 31 deletions common/src/main/java/com/mineaurion/aurionchat/common/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,153 @@

import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static net.kyori.adventure.text.format.NamedTextColor.WHITE;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Utils {
public static Component processMessage(String format, Component message, AurionChatPlayer aurionChatPlayer){
if(!aurionChatPlayer.isAllowedColors()){
private static final Pattern URL_PATTERN = Pattern.compile("(?xi) # comments and case insensitive \n" +
"(.*?)( # prepend least possible of anything and group actual result \n" +
"(?<protocol>https?://)? # http/s \n" +
"(?<domain>([\\w-]+\\.)*([\\w-]+(\\.[\\w-]+)+?)) # domains with * subdomains and +? endings \n" +
"(?<url>(?<path>(/[\\w-_.]+)*/?)\n(\\#(?<fragment>[\\w_-]+))?\n(?<parameters>\\?(([\\w_%-]+)=([\\w_%-]+)&?))? # all of the url \n" +
")).*? # append anything");

public enum URL_MODE {
DISALLOW,
DISSALLOW_URL,
DISPLAY_ONLY_DOMAINS,
ALLOW,
FORCE_HTTPS,
CLICK_DOMAIN
}

public static Component processMessage(String format, Component message, AurionChatPlayer aurionChatPlayer, List<URL_MODE> urlModes) {
Component processedMessage = urlScanning(message, urlModes);
if (!aurionChatPlayer.isAllowedColors()) {
Component messageWithoutStyle = Component.text("");
if(!message.children().isEmpty()){
for (Component component: message.children()) {
if (!processedMessage.children().isEmpty()) {
for (Component component : processedMessage.children()) {
messageWithoutStyle = messageWithoutStyle.append(removeAllStyleAndColor(component));
}
} else {
messageWithoutStyle = messageWithoutStyle.append(removeAllStyleAndColor(message));
messageWithoutStyle = messageWithoutStyle.append(removeAllStyleAndColor(processedMessage));
}
message = messageWithoutStyle;
processedMessage = messageWithoutStyle;
}

String[] formatSplit = format.split("\\{message\\}");

Component beforeMessage = replaceToken(formatSplit[0], aurionChatPlayer);
Component afterMessage = replaceToken(formatSplit.length == 2 ? formatSplit[1] : "", aurionChatPlayer);
return Component.text()
.append(replaceToken(formatSplit[0], aurionChatPlayer))
.append(processedMessage.children())
.append(replaceToken(formatSplit.length == 2 ? formatSplit[1] : "", aurionChatPlayer))
.build()
;
}

/**
* Sanitize url or add click event on it
*/
private static Component urlScanning(Component message, List<URL_MODE> urlModes) {
final Component urlRemoved = Component.text("[url removed]");
TextComponent.Builder component = Component.text();

if(!message.children().isEmpty()){
message.children().forEach(children -> component.append(urlScanning(children, urlModes)));
}

if(message instanceof TextComponent){
TextComponent.Builder builder = Component.text();

final String display = ((TextComponent) message).content();
final Matcher matcher = URL_PATTERN.matcher(display);

if(matcher.find()){
matcher.reset(); // Discard internal state
int eIndex = display.length();
while(matcher.find()){
eIndex = matcher.end();
// append text before url
builder.append(Component.text(matcher.group(1)));

if(urlModes.contains(URL_MODE.DISALLOW)){
builder.append(urlRemoved);
} else {
String urlDisplay = matcher.group(2);
String urlAction = "";

return beforeMessage.append(message).append(afterMessage);
if(urlModes.contains(URL_MODE.FORCE_HTTPS) && urlDisplay.startsWith("http:")){
urlDisplay = urlDisplay.replace("http:", "https:");
}
if(matcher.group("protocol") != null || !matcher.group("url").isEmpty() ){
if(urlModes.contains(URL_MODE.DISSALLOW_URL)){
builder.append(urlRemoved);
continue;
}
}
if(urlModes.contains(URL_MODE.DISPLAY_ONLY_DOMAINS)){
urlDisplay = matcher.group("domain");
}

if(urlModes.contains(URL_MODE.CLICK_DOMAIN) || urlModes.contains(URL_MODE.ALLOW)){
String protocol = urlDisplay.startsWith("http") ? "" : "https://";
urlAction = protocol + urlDisplay + matcher.group("url");
}

Component clickComponent = urlAction.isEmpty() ? Component.text(urlDisplay) : Component.text(urlDisplay).clickEvent(ClickEvent.openUrl(urlAction));

builder.append(clickComponent);
}
}
// Append last non match string
builder.append(Component.text(display.substring(eIndex)));
} else {
// Don't match anything then just append the string to the component
builder.append(Component.text(display));
}
component.append(builder.build());
}
return component.build();
}

public static String getDisplayString(Component component) {
return Stream.concat(Stream.of(component), component.children().stream())
.map(it -> {
if (it instanceof TextComponent)
return ((TextComponent) it).content();
// todo: support other types
//else if (it instanceof BlockNBTComponent) ;
//else if (it instanceof EntityNBTComponent) ;
//else if (it instanceof KeybindComponent) ;
//else if (it instanceof ScoreComponent) ;
//else if (it instanceof SelectorComponent) ;
//else if (it instanceof StorageNBTComponent) ;
//else if (it instanceof TranslatableComponent) ;
return "";
})
.collect(Collectors.joining());
StringBuilder content = new StringBuilder();

if(!component.children().isEmpty()){
component.children().forEach( child -> content.append(getDisplayString(child)));
}

if(component instanceof TextComponent){
content.append(((TextComponent) component).content());
}
// todo: support other types
//else if (it instanceof BlockNBTComponent) ;
//else if (it instanceof EntityNBTComponent) ;
//else if (it instanceof KeybindComponent) ;
//else if (it instanceof ScoreComponent) ;
//else if (it instanceof SelectorComponent) ;
//else if (it instanceof StorageNBTComponent) ;
//else if (it instanceof TranslatableComponent) ;
return content.toString();
}

private static Component replaceToken(String text, AurionChatPlayer aurionChatPlayer){
private static Component replaceToken(String text, AurionChatPlayer aurionChatPlayer) {
return LegacyComponentSerializer.legacy('&').deserialize(
text.replace("{prefix}", aurionChatPlayer.getPlayer().getPreffix())
.replace("{suffix}", aurionChatPlayer.getPlayer().getSuffix())
.replace("{display_name}", aurionChatPlayer.getPlayer().getDisplayName())
);
}

private static Component removeAllStyleAndColor(Component component){
private static Component removeAllStyleAndColor(Component component) {
return component
.style(Style.empty())
.decorations(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import com.mineaurion.aurionchat.common.AbstractAurionChat;
import com.mineaurion.aurionchat.common.AurionChatPlayer;
import com.mineaurion.aurionchat.common.ChatService;
import com.mineaurion.aurionchat.common.Utils;
import com.mineaurion.aurionchat.common.exception.ChannelNotFoundException;
import net.kyori.adventure.text.Component;

import java.io.IOException;
import java.util.Collections;
import java.util.Set;

import static net.kyori.adventure.text.Component.*;
Expand Down Expand Up @@ -128,7 +128,7 @@ public boolean execute(AurionChatPlayer aurionChatPlayer, String channel, Action

public boolean onCommand(AurionChatPlayer aurionChatPlayers, Component message, String channel, String format){
aurionChatPlayers.addChannel(channel);
Component messageFormat = Utils.processMessage(format, message, aurionChatPlayers);
Component messageFormat = Utils.processMessage(format, message, aurionChatPlayers, Collections.singletonList(Utils.URL_MODE.ALLOW));
try {
plugin.getChatService().send(channel, messageFormat);
return true;
Expand Down
Loading

0 comments on commit 76090d9

Please sign in to comment.