From c29788e725e63181e020a640f16560741c466a6d Mon Sep 17 00:00:00 2001 From: Edward Harman Date: Fri, 21 Jan 2022 08:30:15 -0500 Subject: [PATCH 1/7] mucho refactoring to decouple with event listeners --- app/build.gradle | 1 + .../org/ethelred/minecraft/webhook/App.java | 6 +- .../minecraft/webhook/BackCompatUrlSetup.java | 25 +++++++ .../webhook/DiscordWebhookSender.java | 16 +++-- .../webhook/MinecraftServerEvent.java | 70 +++++++++++++++++++ .../webhook/MinecraftServerEventListener.java | 57 +++++++++++++++ .../ethelred/minecraft/webhook/Monitor.java | 31 ++++---- .../ethelred/minecraft/webhook/Options.java | 19 ++--- .../ethelred/minecraft/webhook/Sender.java | 2 +- .../webhook/SenderConfiguration.java | 52 ++++++++++++++ .../ethelred/minecraft/webhook/Tailer.java | 52 ++++++++++---- .../minecraft/webhook/MonitorSpec.groovy | 13 ++-- app/src/test/resources/config.yml | 7 ++ app/src/test/resources/log4j2-test.yml | 21 ++++++ 14 files changed, 318 insertions(+), 54 deletions(-) create mode 100644 app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java create mode 100644 app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java create mode 100644 app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java create mode 100644 app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java create mode 100644 app/src/test/resources/config.yml create mode 100644 app/src/test/resources/log4j2-test.yml diff --git a/app/build.gradle b/app/build.gradle index 37f615b..13365c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -44,6 +44,7 @@ dependencies { implementation 'jakarta.inject:jakarta.inject-api' implementation 'org.apache.logging.log4j:log4j-api:2.17.1' + implementation 'org.apache.commons:commons-text:1.9' runtimeOnly group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.17.1' runtimeOnly 'org.apache.logging.log4j:log4j-core:2.17.1' diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/App.java b/app/src/main/java/org/ethelred/minecraft/webhook/App.java index 6340942..3e5d2b8 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/App.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/App.java @@ -7,9 +7,13 @@ public class App { public static void main(String[] args) { + // tell Micronaut to look for config in known paths of the docker image + System.setProperty( + "micronaut.config.files", + "/config.yml" // in root of docker image + ); Micronaut .build(args) - .eagerInitSingletons(true) .mainClass(App.class) .defaultEnvironments("dev") .start(); diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java b/app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java new file mode 100644 index 0000000..b481106 --- /dev/null +++ b/app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java @@ -0,0 +1,25 @@ +package org.ethelred.minecraft.webhook; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.Context; +import io.micronaut.context.annotation.Property; +import io.micronaut.context.annotation.Requires; +import java.net.URL; + +@Context +@Requires(property = "mc-webhook.webhook-url") +public class BackCompatUrlSetup extends MinecraftServerEventListener { + + public BackCompatUrlSetup( + BeanContext context, + @Property(name = "mc-webhook.webhook-url") URL url + ) { + super(context, getConfiguration(url)); + } + + private static SenderConfiguration getConfiguration(URL url) { + var config = new SenderConfiguration(); + config.setUrl(url); // defaults are ok for other properties + return config; + } +} diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java b/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java index 4b8e464..87e8366 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java @@ -1,10 +1,13 @@ package org.ethelred.minecraft.webhook; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Prototype; import jakarta.inject.Inject; -import jakarta.inject.Singleton; +import jakarta.inject.Named; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.net.http.*; import java.net.http.HttpClient; import java.util.Optional; @@ -16,7 +19,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -@Singleton +@Prototype +@Named("discord") public class DiscordWebhookSender implements Sender { private static final long DEFAULT_DELAY = 3_000; @@ -30,9 +34,9 @@ public class DiscordWebhookSender implements Sender { private final BlockingQueue waiting; @Inject - public DiscordWebhookSender(Options options) { + public DiscordWebhookSender(@Parameter URL webhookUrl) { try { - this.webhook = options.getWebhookUrl().toURI(); + this.webhook = webhookUrl.toURI(); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } @@ -41,7 +45,7 @@ public DiscordWebhookSender(Options options) { this.waiting = new ArrayBlockingQueue<>(64); _scheduleNext(0L); LOGGER.info( - "DiscordWebhookSender initalized with URL {}", + "DiscordWebhookSender initialized with URL {}", this.webhook ); } @@ -136,7 +140,7 @@ private long _delayFromHeaderValue(Optional retryValue) { } @Override - public void sendMessage(String message) { + public void sendMessage(MinecraftServerEvent event, String message) { LOGGER.debug("sendMessage({})", message.trim()); //noinspection ResultOfMethodCallIgnored waiting.offer(message.trim()); diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java new file mode 100644 index 0000000..750050f --- /dev/null +++ b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java @@ -0,0 +1,70 @@ +package org.ethelred.minecraft.webhook; + +import io.micronaut.core.annotation.Introspected; +import io.micronaut.core.annotation.NonNull; +import io.micronaut.core.annotation.Nullable; + +@Introspected +public class MinecraftServerEvent { + + public MinecraftServerEvent( + @NonNull Type type, + @NonNull String containerId, + @NonNull String[] containerNames, + @NonNull String worldName, + @Nullable String playerName + ) { + this.type = type; + this.containerId = containerId; + this.containerNames = containerNames; + this.worldName = worldName; + this.playerName = playerName; + } + + enum Type { + PLAYER_CONNECTED, + PLAYER_DISCONNECTED, + SERVER_STARTED, + SERVER_STOPPED, + } + + @NonNull + public Type getType() { + return type; + } + + @NonNull + public String getContainerId() { + return containerId; + } + + @NonNull + public String[] getContainerNames() { + return containerNames; + } + + @NonNull + public String getWorldName() { + return worldName; + } + + @Nullable + public String getPlayerName() { + return playerName; + } + + @NonNull + private final Type type; + + @NonNull + private final String containerId; + + @NonNull + private final String[] containerNames; + + @NonNull + private final String worldName; + + @Nullable + private final String playerName; +} diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java new file mode 100644 index 0000000..16345dd --- /dev/null +++ b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java @@ -0,0 +1,57 @@ +package org.ethelred.minecraft.webhook; + +import io.micronaut.context.BeanContext; +import io.micronaut.context.annotation.EachBean; +import io.micronaut.context.event.ApplicationEventListener; +import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.inject.qualifiers.Qualifiers; +import io.micronaut.scheduling.annotation.Async; +import jakarta.inject.Singleton; +import org.apache.commons.text.StringSubstitutor; +import org.apache.commons.text.lookup.StringLookup; + +@Singleton +@EachBean(SenderConfiguration.class) +public class MinecraftServerEventListener + implements ApplicationEventListener { + + private static final BeanIntrospection eventIntrospection = BeanIntrospection.getIntrospection( + MinecraftServerEvent.class + ); + private final SenderConfiguration configuration; + private final Sender sender; + + public MinecraftServerEventListener( + BeanContext beanContext, + SenderConfiguration configuration + ) { + this.configuration = configuration; + this.sender = + beanContext.createBean( + Sender.class, + Qualifiers.byName(configuration.getType()), + configuration.getUrl() + ); + } + + @Async + @Override + public void onApplicationEvent(MinecraftServerEvent event) { + if (configuration.getEvents().containsKey(event.getType())) { + var substitutor = new StringSubstitutor(_eventLookup(event)); + var messageFormat = configuration.getEvents().get(event.getType()); + var message = substitutor.replace(messageFormat); + sender.sendMessage(event, message); + } + } + + private StringLookup _eventLookup(MinecraftServerEvent event) { + return key -> { + var property = eventIntrospection.getProperty(key); + return property + .map(p -> p.get(event)) + .map(String::valueOf) + .orElse(null); + }; + } +} diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java b/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java index dcf2e90..3a76a46 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java @@ -1,10 +1,12 @@ package org.ethelred.minecraft.webhook; import com.github.dockerjava.api.DockerClient; +import com.github.dockerjava.api.model.Container; import io.micronaut.context.ApplicationContext; +import io.micronaut.context.annotation.Context; import io.micronaut.scheduling.annotation.Scheduled; import jakarta.inject.Inject; -import jakarta.inject.Singleton; +import java.time.Instant; import java.util.Collection; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -14,13 +16,14 @@ /** * Lists docker containers to check for new/removed ones. Creates Tailers */ -@Singleton +@Context public class Monitor { private static final Logger LOGGER = LogManager.getLogger(Monitor.class); private final DockerClient docker; private final Collection imageNames; + private final Instant startTime; private final ApplicationContext applicationContext; @Inject @@ -32,37 +35,39 @@ public Monitor( this.applicationContext = applicationContext; this.docker = docker; this.imageNames = options.getImageNames(); - LOGGER.info("Constructed"); + this.startTime = Instant.now(); + LOGGER.debug("Constructed"); } private final Map tails = new ConcurrentHashMap<>(); @Scheduled(fixedRate = "${mc-webhook.options.monitor.rate:5s}") public void checkForContainers() { - LOGGER.info("Checking for containers"); + LOGGER.debug("Checking for containers"); docker .listContainersCmd() .withAncestorFilter(imageNames) .exec() - .forEach(c -> _checkContainer(c.getId())); + .forEach(this::_checkContainer); } - private void _checkContainer(String containerId) { + private void _checkContainer(Container container) { + var containerId = container.getId(); if (!tails.containsKey(containerId)) { - LOGGER.info("Adding container {}", containerId); + LOGGER.debug("Adding container {}", container); tails.putIfAbsent( containerId, - new Tailer( - docker, + applicationContext.createBean( + Tailer.class, containerId, - () -> onComplete(containerId), - applicationContext.getBean(Sender.class) + container.getNames(), + onComplete(containerId) ) ); } } - void onComplete(String containerId) { - tails.remove(containerId); + private Runnable onComplete(String containerId) { + return () -> tails.remove(containerId); } } diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Options.java b/app/src/main/java/org/ethelred/minecraft/webhook/Options.java index 968a2d3..f8bec0a 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Options.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Options.java @@ -2,11 +2,11 @@ import io.micronaut.context.annotation.ConfigurationProperties; import io.micronaut.context.annotation.Context; -import java.net.URL; import java.util.HashSet; import java.util.Set; import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * options for app @@ -15,6 +15,8 @@ @ConfigurationProperties("mc-webhook") public class Options { + private static final Logger LOGGER = LogManager.getLogger(); + private static final Set DEFAULT_IMAGE_NAMES = Set.of( "itzg/minecraft-bedrock-server" ); @@ -33,18 +35,5 @@ public void setImageName(String imageName) { this.imageNames.add(imageName); } - @NotNull( - message = "A webhook URL must be provided, for example by specifying the environment variable MC_WEBHOOK_WEBHOOK_URL." - ) - public URL getWebhookUrl() { - return webhook; - } - - public void setWebhookUrl(URL webhook) { - this.webhook = webhook; - } - private Set imageNames = new HashSet<>(); - - private URL webhook; } diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java b/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java index 28703aa..7d780a9 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java @@ -4,5 +4,5 @@ * I can't believe it's not Consumer */ public interface Sender { - void sendMessage(String message); + void sendMessage(MinecraftServerEvent event, String message); } diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java b/app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java new file mode 100644 index 0000000..dd55e73 --- /dev/null +++ b/app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java @@ -0,0 +1,52 @@ +package org.ethelred.minecraft.webhook; + +import io.micronaut.context.annotation.EachProperty; +import java.net.URL; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@EachProperty("mc-webhook.webhooks") +public class SenderConfiguration { + + @NotBlank + private String type = "discord"; + + @NotNull + private URL url; + + private Map events = Map.of( + MinecraftServerEvent.Type.SERVER_STARTED, + "World ${worldName} starting on ${containerNames}", + MinecraftServerEvent.Type.SERVER_STOPPED, + "World ${worldName} stopping on ${containerNames}", + MinecraftServerEvent.Type.PLAYER_CONNECTED, + "${playerName} connected to ${worldName}", + MinecraftServerEvent.Type.PLAYER_DISCONNECTED, + "${playerName} disconnected from ${worldName}" + ); + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public URL getUrl() { + return url; + } + + public void setUrl(URL url) { + this.url = url; + } + + public Map getEvents() { + return events; + } + + public void setEvents(Map events) { + this.events = events; + } +} diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java b/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java index 5b3cb93..5cc4fe4 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java @@ -3,6 +3,8 @@ import com.github.dockerjava.api.DockerClient; import com.github.dockerjava.api.async.ResultCallback; import com.github.dockerjava.api.model.Frame; +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.event.ApplicationEventPublisher; import jakarta.inject.Inject; import java.util.regex.Pattern; import org.apache.logging.log4j.LogManager; @@ -24,19 +26,24 @@ public class Tailer { " Player ([^ ]*connected): (.*), xuid" ); private final Runnable completionCallback; - private final Sender sender; + private final ApplicationEventPublisher eventPublisher; + private final String containerId; + private final String[] containerNames; private volatile String worldName = "Unknown"; @Inject public Tailer( + ApplicationEventPublisher eventPublisher, DockerClient docker, - @ContainerId String containerId, - Runnable completionCallback, - Sender sender + @Parameter String containerId, + @Parameter String[] containerNames, + @Parameter Runnable completionCallback ) { + this.eventPublisher = eventPublisher; this.completionCallback = completionCallback; - this.sender = sender; - LOGGER.info("Tailer is starting for {}", containerId); + this.containerId = containerId; + this.containerNames = containerNames; + LOGGER.info("Tailer is starting for {}", containerNames); _initial(docker, containerId); _follow(docker, containerId); @@ -66,6 +73,15 @@ public void onNext(Frame frame) { if (matcher.find()) { worldName = matcher.group(1).trim(); LOGGER.debug("Found world name {}", worldName); + eventPublisher.publishEventAsync( + new MinecraftServerEvent( + MinecraftServerEvent.Type.SERVER_STARTED, + containerId, + containerNames, + worldName, + null + ) + ); } } } @@ -78,12 +94,15 @@ public void onNext(Frame frame) { if (matcher.find()) { var connect = "connected".equals(matcher.group(1)); var player = matcher.group(2).trim(); - sender.sendMessage( - String.format( - "%s %s %s%n", - player, - connect ? "connected to" : "disconnected from", - worldName + eventPublisher.publishEventAsync( + new MinecraftServerEvent( + connect + ? MinecraftServerEvent.Type.PLAYER_CONNECTED + : MinecraftServerEvent.Type.PLAYER_DISCONNECTED, + containerId, + containerNames, + worldName, + player ) ); } @@ -91,6 +110,15 @@ public void onNext(Frame frame) { @Override public void onComplete() { + eventPublisher.publishEventAsync( + new MinecraftServerEvent( + MinecraftServerEvent.Type.SERVER_STOPPED, + containerId, + containerNames, + worldName, + null + ) + ); completionCallback.run(); } } diff --git a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy index aada93c..2a1bcc7 100644 --- a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy +++ b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy @@ -15,6 +15,10 @@ import org.testcontainers.utility.DockerImageName import spock.lang.Retry import spock.lang.Shared import spock.lang.Specification + +import java.util.stream.Collectors +import java.util.stream.Stream + import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response @@ -46,13 +50,10 @@ class MonitorSpec extends Specification { expectation = mockServerClient.when( request().withMethod("POST").withPath("/webhook")) .respond(response().withStatusCode(204))[0] - Options options = new Options( - imageName: mockBedrock.dockerImageName, - webhookUrl: "http://${mockServer.host}:${mockServer.serverPort}/webhook".toURL() - ) - + ApplicationContext applicationContext =ApplicationContext.builder() - .singletons(options) + .properties("mc-webhook.image-name": mockBedrock.dockerImageName, + "mc-webhook.webhook-url": "http://${mockServer.host}:${mockServer.serverPort}/webhook".toURL()) .start() Monitor monitor = applicationContext.createBean(Monitor) } diff --git a/app/src/test/resources/config.yml b/app/src/test/resources/config.yml new file mode 100644 index 0000000..cb7c84c --- /dev/null +++ b/app/src/test/resources/config.yml @@ -0,0 +1,7 @@ +mc-webhook: + imageNames: + - poop + webhooks: + discord1: + type: discord + url: http://www.example.com \ No newline at end of file diff --git a/app/src/test/resources/log4j2-test.yml b/app/src/test/resources/log4j2-test.yml new file mode 100644 index 0000000..b4a13dc --- /dev/null +++ b/app/src/test/resources/log4j2-test.yml @@ -0,0 +1,21 @@ +Configuration: + status: warn + name: config + + appenders: + Console: + name: STDOUT + target: SYSTEM_OUT + PatternLayout: + Pattern: "%d %p %C{1.} [%t] %m%n" + + Loggers: + logger: + - name: io.micronaut + level: debug + - name: org.ethelred + level: debug + Root: + level: info + AppenderRef: + ref: STDOUT From 706122e56b3cda7ae4246f9b5166640b8e192f4e Mon Sep 17 00:00:00 2001 From: Edward Harman Date: Fri, 21 Jan 2022 22:06:58 -0500 Subject: [PATCH 2/7] test for message content --- .../org/ethelred/minecraft/webhook/MonitorSpec.groovy | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy index 2a1bcc7..ae8c71c 100644 --- a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy +++ b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy @@ -4,6 +4,7 @@ import io.micronaut.context.ApplicationContext import io.micronaut.context.BeanContext import jakarta.inject.Inject import org.mockserver.client.MockServerClient +import org.mockserver.matchers.MatchType import org.mockserver.mock.Expectation import org.mockserver.model.ExpectationId import org.mockserver.verify.VerificationTimes @@ -21,6 +22,7 @@ import java.util.stream.Stream import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response +import static org.mockserver.model.JsonBody.json @Testcontainers class MonitorSpec extends Specification { @@ -50,7 +52,7 @@ class MonitorSpec extends Specification { expectation = mockServerClient.when( request().withMethod("POST").withPath("/webhook")) .respond(response().withStatusCode(204))[0] - + ApplicationContext applicationContext =ApplicationContext.builder() .properties("mc-webhook.image-name": mockBedrock.dockerImageName, "mc-webhook.webhook-url": "http://${mockServer.host}:${mockServer.serverPort}/webhook".toURL()) @@ -64,6 +66,10 @@ class MonitorSpec extends Specification { writeToBedrock("Player connected: Bob, xuid 12345") then: - mockServerClient.verify( request().withMethod("POST").withPath("/webhook"), VerificationTimes.atLeast(1)) + mockServerClient.verify( + request().withMethod("POST").withPath("/webhook") + .withBody(json("""{ + "content" : "Bob connected to MonitorSpec" + }""", MatchType.ONLY_MATCHING_FIELDS)), VerificationTimes.atLeast(1)) } } From 0b7e5b51b317bcc7828e27bb5cb5b814162cebde Mon Sep 17 00:00:00 2001 From: Edward Harman Date: Mon, 24 Jan 2022 08:56:06 -0500 Subject: [PATCH 3/7] =?UTF-8?q?multiple=20senders=20=F0=9F=91=8D.=20JSON?= =?UTF-8?q?=20webhook=20sender=20=F0=9F=91=8D.=20Customizable=20messages?= =?UTF-8?q?=20=F0=9F=91=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/ethelred/minecraft/webhook/App.java | 1 + .../minecraft/webhook/BackCompatUrlSetup.java | 8 +++- .../minecraft/webhook/ContainerId.java | 1 + .../minecraft/webhook/DefaultDocker.java | 1 + .../webhook/DiscordWebhookSender.java | 4 +- .../minecraft/webhook/JsonSender.java | 38 +++++++++++++++++++ .../webhook/MinecraftServerEvent.java | 11 +++--- .../webhook/MinecraftServerEventListener.java | 15 +++++++- .../ethelred/minecraft/webhook/Monitor.java | 1 + .../ethelred/minecraft/webhook/Options.java | 1 + .../ethelred/minecraft/webhook/Sender.java | 1 + .../webhook/SenderConfiguration.java | 9 +++-- .../ethelred/minecraft/webhook/Tailer.java | 13 ++++--- .../minecraft/webhook/MonitorSpec.groovy | 34 ++++++++++++++--- app/src/test/resources/config.yml | 7 ---- .../groovy/ethelred.java-conventions.gradle | 1 + 16 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/org/ethelred/minecraft/webhook/JsonSender.java delete mode 100644 app/src/test/resources/config.yml diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/App.java b/app/src/main/java/org/ethelred/minecraft/webhook/App.java index 3e5d2b8..aa67872 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/App.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/App.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import io.micronaut.runtime.Micronaut; diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java b/app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java index b481106..796dcd9 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/BackCompatUrlSetup.java @@ -1,20 +1,26 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import io.micronaut.context.BeanContext; import io.micronaut.context.annotation.Context; import io.micronaut.context.annotation.Property; import io.micronaut.context.annotation.Requires; +import io.micronaut.core.convert.ConversionService; import java.net.URL; +/** + * this bean maps the original webhook property into the updated system for backwards compatibility + */ @Context @Requires(property = "mc-webhook.webhook-url") public class BackCompatUrlSetup extends MinecraftServerEventListener { public BackCompatUrlSetup( BeanContext context, + ConversionService conversionService, @Property(name = "mc-webhook.webhook-url") URL url ) { - super(context, getConfiguration(url)); + super(context, conversionService, getConfiguration(url)); } private static SenderConfiguration getConfiguration(URL url) { diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/ContainerId.java b/app/src/main/java/org/ethelred/minecraft/webhook/ContainerId.java index 1b8b45e..e8bb0d5 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/ContainerId.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/ContainerId.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import jakarta.inject.Qualifier; diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/DefaultDocker.java b/app/src/main/java/org/ethelred/minecraft/webhook/DefaultDocker.java index 56f61de..c95875c 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/DefaultDocker.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/DefaultDocker.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import com.github.dockerjava.api.DockerClient; diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java b/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java index 87e8366..52d8d2b 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/DiscordWebhookSender.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import io.micronaut.context.annotation.Parameter; @@ -118,8 +119,7 @@ private void _sendMessage(String message) { } } catch (IOException | InterruptedException e) { // can't tell at this point whether the message was sent or not - just give up and log - System.err.println("Exception in _sendMessage"); - e.printStackTrace(System.err); + LOGGER.error("Exception in _sendMessage", e); delay = DEFAULT_DELAY; } finally { _scheduleNext(delay); diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/JsonSender.java b/app/src/main/java/org/ethelred/minecraft/webhook/JsonSender.java new file mode 100644 index 0000000..6188cfb --- /dev/null +++ b/app/src/main/java/org/ethelred/minecraft/webhook/JsonSender.java @@ -0,0 +1,38 @@ +/* (C) Edward Harman 2022 */ +package org.ethelred.minecraft.webhook; + +import io.micronaut.context.annotation.Parameter; +import io.micronaut.context.annotation.Prototype; +import io.micronaut.http.HttpRequest; +import io.micronaut.http.client.BlockingHttpClient; +import io.micronaut.http.client.HttpClient; +import jakarta.inject.Named; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +@Prototype +@Named("json") +public class JsonSender implements Sender { + + private static final Logger LOGGER = LogManager.getLogger(); + + private final BlockingHttpClient client; + private final URI url; + + public JsonSender(HttpClient client, @Parameter URL url) + throws URISyntaxException { + this.client = client.toBlocking(); + this.url = url.toURI(); + } + + @Override + public void sendMessage(MinecraftServerEvent event, String message) { + LOGGER.debug("Send message {}", event); + var request = HttpRequest.POST(url, event); + var response = client.exchange(request); + LOGGER.debug(response); + } +} diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java index 750050f..05f8f0b 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import io.micronaut.core.annotation.Introspected; @@ -10,13 +11,13 @@ public class MinecraftServerEvent { public MinecraftServerEvent( @NonNull Type type, @NonNull String containerId, - @NonNull String[] containerNames, + @NonNull String containerName, @NonNull String worldName, @Nullable String playerName ) { this.type = type; this.containerId = containerId; - this.containerNames = containerNames; + this.containerName = containerName; this.worldName = worldName; this.playerName = playerName; } @@ -39,8 +40,8 @@ public String getContainerId() { } @NonNull - public String[] getContainerNames() { - return containerNames; + public String getContainerName() { + return containerName; } @NonNull @@ -60,7 +61,7 @@ public String getPlayerName() { private final String containerId; @NonNull - private final String[] containerNames; + private final String containerName; @NonNull private final String worldName; diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java index 16345dd..cf5d77c 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEventListener.java @@ -1,9 +1,11 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import io.micronaut.context.BeanContext; import io.micronaut.context.annotation.EachBean; import io.micronaut.context.event.ApplicationEventListener; import io.micronaut.core.beans.BeanIntrospection; +import io.micronaut.core.convert.ConversionService; import io.micronaut.inject.qualifiers.Qualifiers; import io.micronaut.scheduling.annotation.Async; import jakarta.inject.Singleton; @@ -18,13 +20,17 @@ public class MinecraftServerEventListener private static final BeanIntrospection eventIntrospection = BeanIntrospection.getIntrospection( MinecraftServerEvent.class ); + private final SenderConfiguration configuration; private final Sender sender; + private final ConversionService conversionService; public MinecraftServerEventListener( BeanContext beanContext, + ConversionService conversionService, SenderConfiguration configuration ) { + this.conversionService = conversionService; this.configuration = configuration; this.sender = beanContext.createBean( @@ -38,7 +44,12 @@ public MinecraftServerEventListener( @Override public void onApplicationEvent(MinecraftServerEvent event) { if (configuration.getEvents().containsKey(event.getType())) { - var substitutor = new StringSubstitutor(_eventLookup(event)); + var substitutor = new StringSubstitutor( + _eventLookup(event), + "%", + "%", + '\\' + ); var messageFormat = configuration.getEvents().get(event.getType()); var message = substitutor.replace(messageFormat); sender.sendMessage(event, message); @@ -50,7 +61,7 @@ private StringLookup _eventLookup(MinecraftServerEvent event) { var property = eventIntrospection.getProperty(key); return property .map(p -> p.get(event)) - .map(String::valueOf) + .flatMap(o -> conversionService.convert(o, String.class)) .orElse(null); }; } diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java b/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java index 3a76a46..323e13e 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Monitor.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import com.github.dockerjava.api.DockerClient; diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Options.java b/app/src/main/java/org/ethelred/minecraft/webhook/Options.java index f8bec0a..cf8bbe5 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Options.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Options.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import io.micronaut.context.annotation.ConfigurationProperties; diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java b/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java index 7d780a9..5f4c253 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Sender.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; /** diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java b/app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java index dd55e73..0e88e8b 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/SenderConfiguration.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import io.micronaut.context.annotation.EachProperty; @@ -17,13 +18,13 @@ public class SenderConfiguration { private Map events = Map.of( MinecraftServerEvent.Type.SERVER_STARTED, - "World ${worldName} starting on ${containerNames}", + "World %worldName% starting on %containerName%", MinecraftServerEvent.Type.SERVER_STOPPED, - "World ${worldName} stopping on ${containerNames}", + "World %worldName% stopping on %containerName%", MinecraftServerEvent.Type.PLAYER_CONNECTED, - "${playerName} connected to ${worldName}", + "%playerName% connected to %worldName%", MinecraftServerEvent.Type.PLAYER_DISCONNECTED, - "${playerName} disconnected from ${worldName}" + "%playerName% disconnected from %worldName%" ); public String getType() { diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java b/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java index 5cc4fe4..9678e6b 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java @@ -1,3 +1,4 @@ +/* (C) Edward Harman 2022 */ package org.ethelred.minecraft.webhook; import com.github.dockerjava.api.DockerClient; @@ -28,7 +29,7 @@ public class Tailer { private final Runnable completionCallback; private final ApplicationEventPublisher eventPublisher; private final String containerId; - private final String[] containerNames; + private final String containerName; private volatile String worldName = "Unknown"; @Inject @@ -42,8 +43,8 @@ public Tailer( this.eventPublisher = eventPublisher; this.completionCallback = completionCallback; this.containerId = containerId; - this.containerNames = containerNames; - LOGGER.info("Tailer is starting for {}", containerNames); + this.containerName = String.join(",", containerNames); + LOGGER.info("Tailer is starting for {}", containerName); _initial(docker, containerId); _follow(docker, containerId); @@ -77,7 +78,7 @@ public void onNext(Frame frame) { new MinecraftServerEvent( MinecraftServerEvent.Type.SERVER_STARTED, containerId, - containerNames, + containerName, worldName, null ) @@ -100,7 +101,7 @@ public void onNext(Frame frame) { ? MinecraftServerEvent.Type.PLAYER_CONNECTED : MinecraftServerEvent.Type.PLAYER_DISCONNECTED, containerId, - containerNames, + containerName, worldName, player ) @@ -114,7 +115,7 @@ public void onComplete() { new MinecraftServerEvent( MinecraftServerEvent.Type.SERVER_STOPPED, containerId, - containerNames, + containerName, worldName, null ) diff --git a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy index ae8c71c..dcefbe0 100644 --- a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy +++ b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy @@ -34,8 +34,6 @@ class MonitorSpec extends Specification { .withCommand("/bin/sh", "-c","while true; do echo \"test\" >> /proc/1/fd/1; sleep 5; done") @Shared MockServerClient mockServerClient - @Shared - Expectation expectation def writeToBedrock(String message) { mockBedrock.execInContainer( @@ -49,13 +47,26 @@ class MonitorSpec extends Specification { writeToBedrock("Level Name: MonitorSpec") mockServerClient = new MockServerClient(mockServer.host, mockServer.serverPort) - expectation = mockServerClient.when( + mockServerClient.when( request().withMethod("POST").withPath("/webhook")) - .respond(response().withStatusCode(204))[0] + .respond(response().withStatusCode(204)) + mockServerClient.when( + request().withMethod("POST").withPath("/webhookjson")) + .respond(response().withStatusCode(204)) + mockServerClient.when( + request().withMethod("POST").withPath("/webhookmsg")) + .respond(response().withStatusCode(204)) ApplicationContext applicationContext =ApplicationContext.builder() - .properties("mc-webhook.image-name": mockBedrock.dockerImageName, - "mc-webhook.webhook-url": "http://${mockServer.host}:${mockServer.serverPort}/webhook".toURL()) + .properties( + "mc-webhook.image-name": mockBedrock.dockerImageName, + "mc-webhook.webhook-url": "http://${mockServer.host}:${mockServer.serverPort}/webhook".toURL(), + "mc-webhook.webhooks.discord2.type": "discord", + "mc-webhook.webhooks.discord2.url": "http://${mockServer.host}:${mockServer.serverPort}/webhookmsg".toURL(), + "mc-webhook.webhooks.discord2.events.PLAYER_CONNECTED": 'Why hello %playerName% on %containerName%', + "mc-webhook.webhooks.json1.type": "json", + "mc-webhook.webhooks.json1.url": "http://${mockServer.host}:${mockServer.serverPort}/webhookjson".toURL() + ) .start() Monitor monitor = applicationContext.createBean(Monitor) } @@ -64,6 +75,7 @@ class MonitorSpec extends Specification { def "player connected"() { when: writeToBedrock("Player connected: Bob, xuid 12345") + println mockServerClient.retrieveRecordedRequests(null) then: mockServerClient.verify( @@ -71,5 +83,15 @@ class MonitorSpec extends Specification { .withBody(json("""{ "content" : "Bob connected to MonitorSpec" }""", MatchType.ONLY_MATCHING_FIELDS)), VerificationTimes.atLeast(1)) + mockServerClient.verify( + request().withMethod("POST").withPath("/webhookmsg") + .withBody(json("""{ + "content" : "Why hello Bob on ${mockBedrock.containerName}" + }""", MatchType.ONLY_MATCHING_FIELDS)), VerificationTimes.atLeast(1)) + mockServerClient.verify( + request().withMethod("POST").withPath("/webhookjson") + .withBody(json("""{ + "containerName" : "${mockBedrock.containerName}" + }""", MatchType.ONLY_MATCHING_FIELDS)), VerificationTimes.atLeast(1)) } } diff --git a/app/src/test/resources/config.yml b/app/src/test/resources/config.yml deleted file mode 100644 index cb7c84c..0000000 --- a/app/src/test/resources/config.yml +++ /dev/null @@ -1,7 +0,0 @@ -mc-webhook: - imageNames: - - poop - webhooks: - discord1: - type: discord - url: http://www.example.com \ No newline at end of file diff --git a/buildSrc/src/main/groovy/ethelred.java-conventions.gradle b/buildSrc/src/main/groovy/ethelred.java-conventions.gradle index bcc2570..95a7a9d 100644 --- a/buildSrc/src/main/groovy/ethelred.java-conventions.gradle +++ b/buildSrc/src/main/groovy/ethelred.java-conventions.gradle @@ -15,6 +15,7 @@ spotless { importOrder() removeUnusedImports() prettier(['prettier': '2.3.2', 'prettier-plugin-java': '1.3.0']).config(['parser': 'java', 'tabWidth': 4]) + licenseHeader('/* (C) Edward Harman $YEAR */').updateYearWithLatest(true) } groovyGradle { greclipse() From bb41bc8c8cf8570f729d59677d137355fedca5d6 Mon Sep 17 00:00:00 2001 From: Edward Harman Date: Mon, 24 Jan 2022 09:48:07 -0500 Subject: [PATCH 4/7] publish snapshot docker image for pull requests --- .github/workflows/publish_mr.yml | 37 ++++++++++++++++++++++++++++++++ app/build.gradle | 16 +++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/publish_mr.yml diff --git a/.github/workflows/publish_mr.yml b/.github/workflows/publish_mr.yml new file mode 100644 index 0000000..91082b2 --- /dev/null +++ b/.github/workflows/publish_mr.yml @@ -0,0 +1,37 @@ +name: Publish Docker image + +on: + pull_request: + branches: + - main + +env: + REGISTRY: ghcr.io + IMAGE_NAME: edward3h/mc-webhook + +jobs: + build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Log in to the Container registry + uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Set up JDK 11 + uses: ayltai/setup-graalvm@v1 + with: + java-version: 11 + graalvm-version: 21.3.0 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew dockerPush -Pgithub_ref=${GITHUB_SHA} \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 13365c0..71e69e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ java { } } -version = "0.2" +version = "0.3" repositories { mavenCentral() @@ -80,8 +80,14 @@ tasks.named("dockerfile") { } tasks.named("dockerBuild") { - images = [ - "ghcr.io/edward3h/mc-webhook:latest", - "ghcr.io/edward3h/mc-webhook:${project.version}" - ] + if (project.hasProperty("github_ref")) { + images = [ + "ghcr.io/edward3h/mc-webhook:${project.version}-SNAPSHOT-${project.property("github_ref")}" + ] + } else { + images = [ + "ghcr.io/edward3h/mc-webhook:latest", + "ghcr.io/edward3h/mc-webhook:${project.version}" + ] + } } \ No newline at end of file From eaaa35ce83d3e524b90a990dbb2f04a014516b4c Mon Sep 17 00:00:00 2001 From: Edward Harman Date: Mon, 24 Jan 2022 14:40:51 -0500 Subject: [PATCH 5/7] empty config file in case one isn't mounted --- app/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/app/build.gradle b/app/build.gradle index 71e69e8..33434a7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -77,6 +77,7 @@ tasks.named('test') { tasks.named("dockerfile") { baseImage = "ghcr.io/graalvm/graalvm-ce:java11-21.3.0" + instruction """RUN touch /config.yml""" } tasks.named("dockerBuild") { From 5f88ae865a5063b0b12a48a173eb8fca939fc9fb Mon Sep 17 00:00:00 2001 From: Edward Harman Date: Thu, 27 Jan 2022 22:38:19 -0500 Subject: [PATCH 6/7] New config examples --- README.md | 84 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 52d88c0..18e8a45 100644 --- a/README.md +++ b/README.md @@ -33,44 +33,76 @@ services: stdin_open: true tty: true - backup: - image: kaiede/minecraft-bedrock-backup - container_name: backup - restart: "unless-stopped" - depends_on: - - "minecraft1" - environment: - DEBUG: "true" - BACKUP_INTERVAL: "6h" - TZ: "America/New_York" - UID: 1000 - GID: 998 - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - "${MC_HOME}/backups:/backups" - - "${MC_HOME}/server:/minecraft1" - webhook: - image: ghcr.io/edward3h/mc-webhook:latest + image: ghcr.io/edward3h/mc-webhook:0.3 restart: "unless-stopped" - env_file: .env volumes: - /var/run/docker.sock:/var/run/docker.sock + - ./config.yml:/config.yml ``` +### Configuration File +Since 0.3, mc-webhook supports multiple webhooks defined in a config file. It should be mounted into docker as `config.yml`. + +_config.yml_ +```yaml +mc-webhook: + image-names: itzg/minecraft-bedrock-server + webhooks: + discord1: + type: discord + url: https://discordapp.com/api/webhooks/[ids from your discord server] + # if events: are not specified, defaults to all events, with default messages + json1: + type: json + url: http://your.server/webhook + # POSTs a JSON object to the given URL. See below. + discord_custom: + type: discord + url: https://discordapp.com/api/webhooks/[ids from your discord server] + events: + PLAYER_CONNECTED: Hello %playerName%. + SERVER_STARTED: The world %worldName% is starting on %containerName% + # only these events will send a message + # values in %% will be substituted +``` +### Events +These are the current event types: +* PLAYER_CONNECTED +* PLAYER_DISCONNECTED +* SERVER_STARTED +* SERVER_STOPPED + +### JSON message +Using the `json` type will send a message like: +```json +{ + "type":"PLAYER_CONNECTED", + "containerId":"dd5f449daad5dbd0a3f659bbfabcde47605e1a69211e1f7a9d47b758cc54", + "containerName":"/minecraft1", + "worldName":"My Level", + "playerName":"Steve" +} +``` + +### Custom Message format +You can customize the message sent for each event by entering it after the event type (see above example). +Any of the following will be substituted into the text: +* %containerId% +* %containerName% +* %worldName% +* %playerName% + +### Legacy configuration +The previous environment variables are still supported, so you don't have to reconfigure from a previous version. + _.env_ ```ini -MC_HOME=/path/to/store/minecraft/data MC_WEBHOOK_WEBHOOK_URL=https://discordapp.com/api/webhooks/[ids from your discord server] ``` - -### Notes - -* MC_WEBHOOK_WEBHOOK_URL environment variable is **required** and should be copied from your Discord Server Settings. * The server defaults to looking for containers running 'itzg/minecraft-bedrock-server'. It should work for other images as long as the bedrock-server console output appears in the docker logs. To use a different image, specify it in `MC_WEBHOOK_IMAGE_NAME`. ## Links -* https://github.com/itzg/docker-minecraft-bedrock-server -* https://github.com/Kaiede/docker-minecraft-bedrock-backup +* https://github.com/itzg/docker-minecraft-bedrock-server \ No newline at end of file From ce75b86910f63097504aef17761c6e0efdfcb8e7 Mon Sep 17 00:00:00 2001 From: Edward Harman Date: Fri, 28 Jan 2022 11:04:25 -0500 Subject: [PATCH 7/7] xuid support --- README.md | 14 ++++--- .../webhook/MinecraftServerEvent.java | 21 +++++++++- .../ethelred/minecraft/webhook/Tailer.java | 12 +++--- .../minecraft/webhook/MonitorSpec.groovy | 40 ++++++++----------- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 18e8a45..43caa7b 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ mc-webhook: type: discord url: https://discordapp.com/api/webhooks/[ids from your discord server] events: - PLAYER_CONNECTED: Hello %playerName%. + PLAYER_CONNECTED: Hello %playerName% with id %playerXuid% SERVER_STARTED: The world %worldName% is starting on %containerName% # only these events will send a message # values in %% will be substituted @@ -77,11 +77,12 @@ These are the current event types: Using the `json` type will send a message like: ```json { - "type":"PLAYER_CONNECTED", - "containerId":"dd5f449daad5dbd0a3f659bbfabcde47605e1a69211e1f7a9d47b758cc54", - "containerName":"/minecraft1", - "worldName":"My Level", - "playerName":"Steve" + "type": "PLAYER_CONNECTED", + "containerId": "dd5f449daad5dbd0a3f659bbfabcde47605e1a69211e1f7a9d47b758cc54", + "containerName": "/minecraft1", + "worldName": "My Level", + "playerName": "Steve", + "playerXuid": "12345" } ``` @@ -92,6 +93,7 @@ Any of the following will be substituted into the text: * %containerName% * %worldName% * %playerName% +* %playerXuid% ### Legacy configuration The previous environment variables are still supported, so you don't have to reconfigure from a previous version. diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java index 05f8f0b..246bc11 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/MinecraftServerEvent.java @@ -8,18 +8,29 @@ @Introspected public class MinecraftServerEvent { + public MinecraftServerEvent( + @NonNull Type type, + @NonNull String containerId, + @NonNull String containerName, + @NonNull String worldName + ) { + this(type, containerId, containerName, worldName, null, null); + } + public MinecraftServerEvent( @NonNull Type type, @NonNull String containerId, @NonNull String containerName, @NonNull String worldName, - @Nullable String playerName + @Nullable String playerName, + @Nullable String playerXuid ) { this.type = type; this.containerId = containerId; this.containerName = containerName; this.worldName = worldName; this.playerName = playerName; + this.playerXuid = playerXuid; } enum Type { @@ -54,6 +65,11 @@ public String getPlayerName() { return playerName; } + @Nullable + public String getPlayerXuid() { + return playerXuid; + } + @NonNull private final Type type; @@ -68,4 +84,7 @@ public String getPlayerName() { @Nullable private final String playerName; + + @Nullable + private final String playerXuid; } diff --git a/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java b/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java index 9678e6b..3a10ba7 100644 --- a/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java +++ b/app/src/main/java/org/ethelred/minecraft/webhook/Tailer.java @@ -24,7 +24,7 @@ public class Tailer { [INFO] Player disconnected: Foxer191, xuid: 2535428717109723 */ private static final Pattern playerEvent = Pattern.compile( - " Player ([^ ]*connected): (.*), xuid" + " Player ([^ ]*connected): (.*), xuid: (\\d+)" ); private final Runnable completionCallback; private final ApplicationEventPublisher eventPublisher; @@ -79,8 +79,7 @@ public void onNext(Frame frame) { MinecraftServerEvent.Type.SERVER_STARTED, containerId, containerName, - worldName, - null + worldName ) ); } @@ -95,6 +94,7 @@ public void onNext(Frame frame) { if (matcher.find()) { var connect = "connected".equals(matcher.group(1)); var player = matcher.group(2).trim(); + var xuid = matcher.group(3).trim(); eventPublisher.publishEventAsync( new MinecraftServerEvent( connect @@ -103,7 +103,8 @@ public void onNext(Frame frame) { containerId, containerName, worldName, - player + player, + xuid ) ); } @@ -116,8 +117,7 @@ public void onComplete() { MinecraftServerEvent.Type.SERVER_STOPPED, containerId, containerName, - worldName, - null + worldName ) ); completionCallback.run(); diff --git a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy index dcefbe0..8b4df85 100644 --- a/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy +++ b/app/src/test/groovy/org/ethelred/minecraft/webhook/MonitorSpec.groovy @@ -1,25 +1,17 @@ package org.ethelred.minecraft.webhook import io.micronaut.context.ApplicationContext -import io.micronaut.context.BeanContext -import jakarta.inject.Inject import org.mockserver.client.MockServerClient import org.mockserver.matchers.MatchType -import org.mockserver.mock.Expectation -import org.mockserver.model.ExpectationId import org.mockserver.verify.VerificationTimes import org.testcontainers.containers.GenericContainer import org.testcontainers.containers.MockServerContainer -import org.testcontainers.images.builder.ImageFromDockerfile import org.testcontainers.spock.Testcontainers import org.testcontainers.utility.DockerImageName import spock.lang.Retry import spock.lang.Shared import spock.lang.Specification -import java.util.stream.Collectors -import java.util.stream.Stream - import static org.mockserver.model.HttpRequest.request import static org.mockserver.model.HttpResponse.response import static org.mockserver.model.JsonBody.json @@ -30,8 +22,8 @@ class MonitorSpec extends Specification { @Shared MockServerContainer mockServer = new MockServerContainer(DockerImageName.parse("mockserver/mockserver")) @Shared - GenericContainer mockBedrock = new GenericContainer( DockerImageName.parse("alpine")) - .withCommand("/bin/sh", "-c","while true; do echo \"test\" >> /proc/1/fd/1; sleep 5; done") + GenericContainer mockBedrock = new GenericContainer(DockerImageName.parse("alpine")) + .withCommand("/bin/sh", "-c", "while true; do echo \"test\" >> /proc/1/fd/1; sleep 5; done") @Shared MockServerClient mockServerClient @@ -42,8 +34,8 @@ class MonitorSpec extends Specification { } def setupSpec() { - mockServer.followOutput({println(it.getUtf8String().trim())}) - mockBedrock.followOutput({println(it.getUtf8String().trim())}) + mockServer.followOutput({ println(it.getUtf8String().trim()) }) + mockBedrock.followOutput({ println(it.getUtf8String().trim()) }) writeToBedrock("Level Name: MonitorSpec") mockServerClient = new MockServerClient(mockServer.host, mockServer.serverPort) @@ -57,24 +49,24 @@ class MonitorSpec extends Specification { request().withMethod("POST").withPath("/webhookmsg")) .respond(response().withStatusCode(204)) - ApplicationContext applicationContext =ApplicationContext.builder() - .properties( - "mc-webhook.image-name": mockBedrock.dockerImageName, - "mc-webhook.webhook-url": "http://${mockServer.host}:${mockServer.serverPort}/webhook".toURL(), - "mc-webhook.webhooks.discord2.type": "discord", - "mc-webhook.webhooks.discord2.url": "http://${mockServer.host}:${mockServer.serverPort}/webhookmsg".toURL(), - "mc-webhook.webhooks.discord2.events.PLAYER_CONNECTED": 'Why hello %playerName% on %containerName%', - "mc-webhook.webhooks.json1.type": "json", - "mc-webhook.webhooks.json1.url": "http://${mockServer.host}:${mockServer.serverPort}/webhookjson".toURL() - ) - .start() + ApplicationContext applicationContext = ApplicationContext.builder() + .properties( + "mc-webhook.image-name": mockBedrock.dockerImageName, + "mc-webhook.webhook-url": "http://${mockServer.host}:${mockServer.serverPort}/webhook".toURL(), + "mc-webhook.webhooks.discord2.type": "discord", + "mc-webhook.webhooks.discord2.url": "http://${mockServer.host}:${mockServer.serverPort}/webhookmsg".toURL(), + "mc-webhook.webhooks.discord2.events.PLAYER_CONNECTED": 'Why hello %playerName% on %containerName%', + "mc-webhook.webhooks.json1.type": "json", + "mc-webhook.webhooks.json1.url": "http://${mockServer.host}:${mockServer.serverPort}/webhookjson".toURL() + ) + .start() Monitor monitor = applicationContext.createBean(Monitor) } @Retry(delay = 1000, count = 3) def "player connected"() { when: - writeToBedrock("Player connected: Bob, xuid 12345") + writeToBedrock("Player connected: Bob, xuid: 12345") println mockServerClient.retrieveRecordedRequests(null) then: