diff --git a/bukkit/src/main/java/com/discordsrv/bukkit/config/main/BukkitConfig.java b/bukkit/src/main/java/com/discordsrv/bukkit/config/main/BukkitConfig.java index 396c8450..f8812169 100644 --- a/bukkit/src/main/java/com/discordsrv/bukkit/config/main/BukkitConfig.java +++ b/bukkit/src/main/java/com/discordsrv/bukkit/config/main/BukkitConfig.java @@ -20,7 +20,7 @@ package com.discordsrv.bukkit.config.main; import com.discordsrv.common.config.configurate.annotation.Order; import com.discordsrv.common.config.main.MainConfig; -import com.discordsrv.common.config.main.PluginIntegrationConfig; +import com.discordsrv.common.config.main.PresenceUpdaterConfig; import com.discordsrv.common.config.main.channels.base.BaseChannelConfig; import com.discordsrv.common.config.main.channels.base.server.ServerBaseChannelConfig; import com.discordsrv.common.config.main.channels.base.server.ServerChannelConfig; @@ -42,11 +42,8 @@ public class BukkitConfig extends MainConfig { @Order(5) public BukkitRequiredLinkingConfig requiredLinking = new BukkitRequiredLinkingConfig(); - @Order(100) - public PluginIntegrationConfig integrations = new PluginIntegrationConfig(); - @Override - public PluginIntegrationConfig integrations() { - return integrations; + public PresenceUpdaterConfig defaultPresenceUpdater() { + return new PresenceUpdaterConfig.Server(); } } diff --git a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java index 4c47d576..6edbdf2e 100644 --- a/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java +++ b/common/src/main/java/com/discordsrv/common/AbstractDiscordSRV.java @@ -78,6 +78,7 @@ import com.discordsrv.common.placeholder.PlaceholderServiceImpl; import com.discordsrv.common.placeholder.context.GlobalDateFormattingContext; import com.discordsrv.common.placeholder.context.GlobalTextHandlingContext; import com.discordsrv.common.placeholder.result.ComponentResultStringifier; +import com.discordsrv.common.presence.PresenceUpdaterModule; import com.discordsrv.common.profile.ProfileManager; import com.discordsrv.common.storage.Storage; import com.discordsrv.common.storage.StorageType; @@ -590,6 +591,7 @@ public abstract class AbstractDiscordSRV< registerModule(DiscordInviteModule::new); registerModule(MentionCachingModule::new); registerModule(LinkingModule::new); + registerModule(PresenceUpdaterModule::new); // Integrations registerIntegration("com.discordsrv.common.integration.LuckPermsIntegration"); @@ -608,6 +610,7 @@ public abstract class AbstractDiscordSRV< protected final void startedMessage() { registerModule(StartMessageModule::new); registerModule(StopMessageModule::new); + Optional.ofNullable(getModule(PresenceUpdaterModule.class)).ifPresent(PresenceUpdaterModule::serverStarted); } private StorageType getStorageType() { diff --git a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java index 0dd92013..f06668b5 100644 --- a/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java +++ b/common/src/main/java/com/discordsrv/common/config/main/MainConfig.java @@ -81,6 +81,12 @@ public abstract class MainConfig implements Config { public LinkedAccountConfig linkedAccounts = new LinkedAccountConfig(); + public PresenceUpdaterConfig presenceUpdater = defaultPresenceUpdater(); + + protected PresenceUpdaterConfig defaultPresenceUpdater() { + return new PresenceUpdaterConfig(); + } + public TimedUpdaterConfig timedUpdater = new TimedUpdaterConfig(); @Comment("Configuration options for group-role synchronization") @@ -109,7 +115,12 @@ public abstract class MainConfig implements Config { @Constants.Comment("%player_avatar_url%") public AvatarProviderConfig avatarProvider = new AvatarProviderConfig(); - public abstract PluginIntegrationConfig integrations(); + @Order(100) + public PluginIntegrationConfig integrations = defaultIntegrations(); + + protected PluginIntegrationConfig defaultIntegrations() { + return new PluginIntegrationConfig(); + } @Order(1000) public MemberCachingConfig memberCaching = new MemberCachingConfig(); diff --git a/common/src/main/java/com/discordsrv/common/config/main/PresenceUpdaterConfig.java b/common/src/main/java/com/discordsrv/common/config/main/PresenceUpdaterConfig.java new file mode 100644 index 00000000..a9a16073 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/config/main/PresenceUpdaterConfig.java @@ -0,0 +1,121 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.config.main; + +import com.discordsrv.common.config.configurate.annotation.Constants; +import com.discordsrv.common.logging.Logger; +import net.dv8tion.jda.api.OnlineStatus; +import net.dv8tion.jda.api.entities.Activity; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.Comment; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +@ConfigSerializable +public class PresenceUpdaterConfig { + + @Comment("The amount of seconds between presence updates\n" + + "Minimum value: %1s") + @Constants.Comment("30") + public int updaterRateInSeconds = 90; + + public List presences = new ArrayList<>(Collections.singleton(new Presence())); + + @ConfigSerializable + public static class Server extends PresenceUpdaterConfig { + + @Comment("The presence to use while the server is starting") + public boolean useStartingPresence = true; + public Presence startingPresence = new Presence(OnlineStatus.DO_NOT_DISTURB, "Starting..."); + + @Comment("The presence to use while the server is stopping") + public boolean useStoppingPresence = true; + public Presence stoppingPresence = new Presence(OnlineStatus.IDLE, "Stopping..."); + + } + + @ConfigSerializable + public static class Presence { + + public Presence() {} + + public Presence(OnlineStatus status, String activity) { + this.status = status; + this.activity = activity; + } + + @Comment("Valid options: %1") + @Constants.Comment({"online, idle, do_not_disturb, invisible"}) + public OnlineStatus status = OnlineStatus.ONLINE; + + @Comment("This may be prefixed by one of the following to specify the activity type: %1\n" + + "You can prefix the value with %2 and a YouTube or Twitch link to use the Streaming activity type") + @Constants.Comment({ + "\"playing\", \"listening\", \"watching\", \"competing in\"", + "\"streaming\"" + }) + public String activity = "playing Minecraft"; + + public Activity activity(Logger logger) { + Activity.ActivityType activityType = Activity.ActivityType.CUSTOM_STATUS; + String activity = this.activity; + String url = null; + + for (Activity.ActivityType type : Activity.ActivityType.values()) { + String name = type.name().toLowerCase(Locale.ROOT); + if (type == Activity.ActivityType.COMPETING) { + name = "competing in"; + } + name += " "; + + if (!activity.toLowerCase(Locale.ROOT).startsWith(name)) { + continue; + } + + String namePart = activity.substring(name.length()); + if (namePart.trim().isEmpty()) { + continue; + } + if (type == Activity.ActivityType.STREAMING) { + String[] parts = namePart.split(" ", 2); + if (parts.length == 2) { + String link = parts[0]; + if (!Activity.isValidStreamingUrl(link)) { + if (logger != null) { + logger.warning("Invalid streaming presence URL: " + link); + } + } else { + url = parts[0]; + namePart = parts[1]; + } + } + } + + activityType = type; + activity = namePart; + break; + } + + return Activity.of(activityType, activity, url); + } + } +} diff --git a/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java b/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java index 109dff8c..577def9b 100644 --- a/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java +++ b/common/src/main/java/com/discordsrv/common/module/type/PluginIntegration.java @@ -44,7 +44,7 @@ public abstract class PluginIntegration
extends AbstractM @Override @OverridingMethodsMustInvokeSuper public boolean isEnabled() { - if (discordSRV.config().integrations().disabledIntegrations.contains(getIntegrationName())) { + if (discordSRV.config().integrations.disabledIntegrations.contains(getIntegrationName())) { return false; } return super.isEnabled(); diff --git a/common/src/main/java/com/discordsrv/common/presence/PresenceUpdaterModule.java b/common/src/main/java/com/discordsrv/common/presence/PresenceUpdaterModule.java new file mode 100644 index 00000000..6863c640 --- /dev/null +++ b/common/src/main/java/com/discordsrv/common/presence/PresenceUpdaterModule.java @@ -0,0 +1,125 @@ +/* + * This file is part of DiscordSRV, licensed under the GPLv3 License + * Copyright (c) 2016-2024 Austin "Scarsz" Shapiro, Henri "Vankka" Schubin and DiscordSRV contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.discordsrv.common.presence; + +import com.discordsrv.api.DiscordSRVApi; +import com.discordsrv.api.event.bus.EventPriority; +import com.discordsrv.api.event.bus.Subscribe; +import com.discordsrv.api.event.events.lifecycle.DiscordSRVShuttingDownEvent; +import com.discordsrv.common.DiscordSRV; +import com.discordsrv.common.config.main.PresenceUpdaterConfig; +import com.discordsrv.common.logging.NamedLogger; +import com.discordsrv.common.module.type.AbstractModule; +import net.dv8tion.jda.api.JDA; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public class PresenceUpdaterModule extends AbstractModule { + + private Future future; + private final AtomicInteger currentIndex = new AtomicInteger(0); + private final AtomicReference serverState = new AtomicReference<>(ServerState.PRE_START); + + public PresenceUpdaterModule(DiscordSRV discordSRV) { + super(discordSRV, new NamedLogger(discordSRV, "PRESENCE_UPDATER")); + } + + public void serverStarted() { + serverState.set(ServerState.STARTED); + setPresenceOrSchedule(); + } + + @Subscribe(priority = EventPriority.EARLIEST) + public void onDiscordSRVShuttingDown(DiscordSRVShuttingDownEvent event) { + serverState.set(ServerState.STOPPING); + setPresenceOrSchedule(); + } + + @Override + public void reload(Consumer resultConsumer) { + setPresenceOrSchedule(); + + // Log problems with presences + for (PresenceUpdaterConfig.Presence presence : discordSRV.config().presenceUpdater.presences) { + presence.activity(logger()); + } + } + + private void setPresence(PresenceUpdaterConfig.Presence config) { + JDA jda = discordSRV.jda(); + if (jda == null) { + // Guess not + return; + } + jda.getPresence().setPresence(config.status, config.activity(null)); + } + + private void setPresenceOrSchedule() { + boolean alreadyScheduled = future != null; + if (future != null) { + future.cancel(true); + } + + PresenceUpdaterConfig config = discordSRV.config().presenceUpdater; + if (config instanceof PresenceUpdaterConfig.Server) { + PresenceUpdaterConfig.Server serverConfig = (PresenceUpdaterConfig.Server) config; + switch (serverState.get()) { + case PRE_START: + if (serverConfig.useStartingPresence) { + setPresence(serverConfig.startingPresence); + return; + } + case STOPPING: + if (serverConfig.useStoppingPresence) { + setPresence(serverConfig.stoppingPresence); + return; + } + } + } + + List presences = config.presences; + int count = config.presences.size(); + if (count == 1) { + setPresence(presences.get(0)); + return; + } + + Duration duration = Duration.ofSeconds(config.updaterRateInSeconds); + future = discordSRV.scheduler().runAtFixedRate(() -> { + int index = currentIndex.getAndUpdate(value -> { + if (count <= value) { + return 0; + } + return value + 1; + }); + setPresence(presences.get(index)); + }, alreadyScheduled ? duration : Duration.ZERO, duration); + } + + private enum ServerState { + PRE_START, + STARTED, + STOPPING + } +} diff --git a/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java b/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java index 0c0d1b1d..09661dab 100644 --- a/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java +++ b/common/src/test/java/com/discordsrv/common/MockDiscordSRV.java @@ -29,7 +29,6 @@ import com.discordsrv.common.config.configurate.manager.MessagesConfigManager; import com.discordsrv.common.config.configurate.manager.abstraction.ServerConfigManager; import com.discordsrv.common.config.connection.ConnectionConfig; import com.discordsrv.common.config.main.MainConfig; -import com.discordsrv.common.config.main.PluginIntegrationConfig; import com.discordsrv.common.config.main.channels.base.ChannelConfig; import com.discordsrv.common.config.main.generic.DestinationConfig; import com.discordsrv.common.config.main.generic.ThreadConfig; @@ -230,12 +229,7 @@ public class MockDiscordSRV extends AbstractDiscordSRV