diff --git a/.gitignore b/.gitignore index 2ed1642..b4ac5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ servers.json admin.json -test.json +service_channels.json *.hprof /build /.gradle \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 85f887a..f503bfd 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -2,7 +2,19 @@ + + + + + + + + + + + + + + + + file://$PROJECT_DIR$/src/main/kotlin/de/wulkanat/AdminCli.kt + 18 + + + diff --git a/README.md b/README.md index 9099bbb..ea85ad4 100644 --- a/README.md +++ b/README.md @@ -6,85 +6,61 @@ the bot to your server. Please note that only people with *Administrator* permis configure it. You can type `%!info` to get an overview over all available commands. +## Commands + +| **Command** | **Arguments** | **Info** | +|------------------|--------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| %!add | | Add current channel to the notified list | +| %!remove | | Remove current channel to the notified list | +| %!publish | on | off | [Community|Partner|Verified only] Auto publish the message if in an announcement channel | +| %!ping | none | everyone | roleName | What role to ping | +| %!setMessage | message | Set a custom message when a blogpost arrives | +| %!resetMessage | | Reset the custom message to none | +| %!serviceChannel | add | remove | Add/remove channel from service notification list | +| %!publishMessage | on | off | [Community|Partner|Verified only] Auto publish the custom message if in an announcement channel | +| %!info | | Show an overview about all channels registered on this server | +| %!report | Your message | Report an issue to the Bot Admin (this will share your user name so they can contact you) | +| %!help | | Show a help dialog with all these commands | ## Self Hosting Okay, this isn't really meant for you to setup, but if you *really* want to set it up yourself, fine. -* first go to the release tab, download the jar, and put it in a folder -* Add two files in the root of the repo, an `admin.json` and a `servers.json`. -Add your Discord ID (not name), Bot token, and update frequency to the `admin.json`: -```json -{ - "adminId": 12345678910, - "token": "AOGH@(AKnjsfjiJijaig3ijgG92jaij", - "updateMs": 30000, - "watchingMessage": "for new Blogposts" -} -``` -* add your servers to `servers.json` -```json -[ - { - "id": 15050067772322222, - "mentionedRole": "everyone", - "autoPublish": true, - "message": null - }, - { - "id": 74050067772325222, - "mentionedRole": null, - "autoPublish":false, - "message": null - }, - { - "id": 74050067772325222, - "mentionedRole": "74036067771625222", - "autoPublish":false, - "message": null - } -] -``` -* add a `test.json` with the same schema as the `server.json`. When -you enable test mode, the servers from there will be used instead allowing -you to test if it works. +Go to the release tab, download the jar, and put it in a folder. + +Start the server with `java -jar [server-file-name]` If you put in everything correctly, +the bot should message you on Discord. + +*Note:* You need to invite the bot into a server before it can message you. + +Run it once (it should crash or print an error), so `admin.json`, `servers.json` and `service_channels.json` +are being created. +Add your Discord ID `adminId` (not name), Bot token `token`, and update frequency `updateMs` to the `admin.json`, +optionally you can add your own messages for when the bot is looking and when it can't reach Hytale Servers. + +If you verified that everything works correctly, you can start the server in the background, on Linux that is +`nohup java -jar [server-file-name]`. To stop it you can either type `!stop` in the Admin Console (Discord PM) or +if the bot is unresponsive the the PID of it through `ps -ef` and `kill [pid]` + ## Compiling yourself I developed it under Windows, and had some trouble compiling it on Linux. You mileage may vary. ## Admin commands -Start the server with `java -jar [server-file-name]` If you put in everything correctly, the bot should message you on Discord. -### Adding Servers -Please edit the JSON file. -You can force an update by calling -``` -%!refreshList -``` -### Testing -Switching between test and production files -``` -%!testMode -%!fakeUpdate -``` -``` -%!productionMode -``` -**WARNING**: Initiating a fake update is not being cancelled by switching -to production. -### Stop the server from within Discord -``` -%!stop -``` -### Show servers, channels and roles -``` -%!info -``` +| **Command** | **Arguments** | **Info** | +|------------------|-------|---------------------| +| !info | | Show all registered channels and servers. | +| !stop | | Stop the server (useful when running in `nohup`) | +| !serviceMessage | message | Send a service message to all registered channels | +| !fakeUpdate | | Cause a fake update (**WARNING**: This will show on **ALL** registered servers) | +| !refreshList | | Refresh servers and service channels from disk (if you manually edit the JSON files) | -These commands will work in every channel, but will be ignored if they don't come from you, however the bot will always respond in a private message. -It will also print errors directly in a Discord private message. +These commands will only work by private messaging the bot (and will be ignored if they don't +come from the admin registered in the `admin.json`. ## TODO -Mainly reaction roles for convenience, self setup on invite to server, Twitter integration. +Mainly reaction roles for convenience, Twitter integration to either be even faster or to brag how much faster +we were over the official Hytale Twitter. ## Other diff --git a/build.gradle b/build.gradle index d7adb7d..825896d 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ plugins { } group 'de.wulkanat' -version '1.3' +version '1.4' repositories { mavenCentral() diff --git a/src/main/kotlin/de/wulkanat/Admin.kt b/src/main/kotlin/de/wulkanat/Admin.kt index a72da95..c442d41 100644 --- a/src/main/kotlin/de/wulkanat/Admin.kt +++ b/src/main/kotlin/de/wulkanat/Admin.kt @@ -14,23 +14,7 @@ object Admin { val token: String val updateMs: Long val message: String - - var testModeEnabled: Boolean = false - set(value) { - if (field == value) - return - - field = value - - if (value) { - jda?.presence?.setPresence(Activity.of(Activity.ActivityType.DEFAULT, "Testing mode, hold on..."), true) - } else { - jda?.presence?.setPresence(Activity.watching("for new Blogposts"), false) - } - - Channels.channels = Channels.refreshFromDisk() - Admin.info() - } + val offlineMessage: String init { val admin = Json(JsonConfiguration.Stable).parse(AdminFile.serializer(), ADMIN_FILE.readText()) @@ -38,6 +22,7 @@ object Admin { token = admin.token updateMs = admin.updateMs message = admin.watchingMessage + offlineMessage = admin.offlineMessage } var jda: JDA? = null @@ -116,7 +101,12 @@ object Admin { sendDevMessage( EmbedBuilder() .setTitle("Now watching for new Hytale Blogposts every ${updateMs / 1000}s") - .setDescription(Channels.getServerNames().joinToString("\n")) + .setDescription(""" + ${Channels.getServerNames().joinToString("\n")} + + **_Service Channels_** + ${Channels.getServiceChannelServers().joinToString("\n")} + """.trimIndent()) .setColor(Color.GREEN) .build(), "Now watching for new Hytale BlogPosts" @@ -138,7 +128,7 @@ object Admin { .sendMessage(messageEmbed).complete() } - private fun sendDevMessage(messageEmbed: MessageEmbed, fallback: String) { + fun sendDevMessage(messageEmbed: MessageEmbed, fallback: String) { val devChannel = admin?.openPrivateChannel() ?: kotlin.run { kotlin.io.println(fallback) return diff --git a/src/main/kotlin/de/wulkanat/AdminCli.kt b/src/main/kotlin/de/wulkanat/AdminCli.kt index 74e7632..dc40ea8 100644 --- a/src/main/kotlin/de/wulkanat/AdminCli.kt +++ b/src/main/kotlin/de/wulkanat/AdminCli.kt @@ -1,10 +1,8 @@ package de.wulkanat import de.wulkanat.model.BlogPostPreview -import net.dv8tion.jda.api.events.message.MessageReceivedEvent import net.dv8tion.jda.api.hooks.ListenerAdapter import de.wulkanat.web.SiteWatcher -import net.dv8tion.jda.api.events.ExceptionEvent import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent import kotlin.system.exitProcess @@ -16,9 +14,9 @@ class AdminCli : ListenerAdapter() { ) { return } - val command = msg.removePrefix("!").split(Regex("\\s+")) + val command = Regex("[^\\s`]+|`[^`]*`").findAll(msg.removePrefix("!")).toList() - when (command[0]) { + when (command[0].value) { "stop" -> exitProcess(1) "fakeUpdate" -> { SiteWatcher.newestBlog = BlogPostPreview( @@ -35,16 +33,18 @@ class AdminCli : ListenerAdapter() { "info" -> { Admin.info() } + "serviceMessage" -> { + if (command.size != 3) { + Admin.println("Enclose message and title in backticks (`)") + } else { + Channels.sendServiceMessage(command[1].value.trim('`'), command[2].value.trim('`')) + } + } "refreshList" -> { - Channels.channels = Channels.refreshFromDisk() + Channels.channels = Channels.refreshChannelsFromDisk() + Channels.serviceChannels = Channels.refreshServiceChannelsFromDisk() Admin.info() } - "testMode" -> { - Admin.testModeEnabled = true - } - "productionMode" -> { - Admin.testModeEnabled = false - } } } } \ No newline at end of file diff --git a/src/main/kotlin/de/wulkanat/Channels.kt b/src/main/kotlin/de/wulkanat/Channels.kt index 3f2dcf2..7307463 100644 --- a/src/main/kotlin/de/wulkanat/Channels.kt +++ b/src/main/kotlin/de/wulkanat/Channels.kt @@ -4,20 +4,22 @@ import de.wulkanat.extensions.crosspost import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonConfiguration import kotlinx.serialization.list +import net.dv8tion.jda.api.EmbedBuilder import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.Permission import net.dv8tion.jda.api.entities.MessageEmbed import net.dv8tion.jda.api.entities.TextChannel import net.dv8tion.jda.api.exceptions.ErrorResponseException +import java.awt.Color object Channels { var jda: JDA? = null - val json = Json(JsonConfiguration.Stable) /** * List of (ServerID, ChannelID) */ - var channels: MutableList = refreshFromDisk() + var channels: MutableList = refreshChannelsFromDisk() + var serviceChannels: MutableList = refreshServiceChannelsFromDisk() fun sentToAll(messageEmbed: MessageEmbed) { if (jda == null) @@ -57,6 +59,33 @@ object Channels { } } + fun sendServiceMessage(title: String, message: String) { + val serviceMessage = EmbedBuilder() + .setTitle(title) + .setDescription(message) + .setColor(Color.WHITE) + .setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl) + .setFooter("This was sent by a human.") + .build() + + for (channelInfo in serviceChannels) { + val channel = jda!!.getTextChannelById(channelInfo.id) + + channel?.sendMessage(serviceMessage)?.queue() + } + + Admin.println("Service message distributed to ${serviceChannels.size} channels.") + Admin.sendDevMessage(serviceMessage, """ + *************** + SERVICE MESSAGE + + $title + ------- + $message + *************** + """.trimIndent()) + } + fun checkEveryonePermission() { for (channel_pair in channels) { val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue @@ -72,13 +101,15 @@ object Channels { } } - fun refreshFromDisk(): MutableList { + fun refreshChannelsFromDisk(): MutableList { return json.parse( - DiscordChannel.serializer().list, (if (Admin.testModeEnabled) { - TEST_FILE - } else { - SERVERS_FILE - }).readText() + DiscordChannel.serializer().list, (SERVERS_FILE).readText() + ).toMutableList() + } + + fun refreshServiceChannelsFromDisk(): MutableList { + return json.parse( + ServiceChannel.serializer().list, (SERVICE_CHANNELS_FILE).readText() ).toMutableList() } @@ -99,7 +130,7 @@ object Channels { else -> " @${channel.guild.getRoleById(it.mentionedRole ?: "")?.name}" } val publish = if (it.autoPublish) " (publish)" else "" - "**${channel.guild.name}**\n#${channel.name}${role}${publish}${if (it.message == null) { + "**${channel.guild.name}** #${channel.name}${role}${publish}${if (it.message == null) { "" } else { "\n*${it.message!!.message}*${if (it.message!!.pushAnnouncement) " (publish)" else ""}" @@ -108,6 +139,16 @@ object Channels { } } + fun getServiceChannelServers(server: Long? = null): List { + if (jda == null) + return listOf() + + return serviceChannels.filter { server == null || (jda!!.getTextChannelById(it.id)?.guild?.idLong == server) }.map { + val channel = jda!!.getTextChannelById(it.id) + "**${channel?.guild?.name ?: it.id}** #${channel?.name ?: "(inactive)"}" + } + } + fun testServerId(id: Long): TextChannel? { return jda?.getTextChannelById(id) } @@ -129,5 +170,11 @@ object Channels { channels ) ) + SERVICE_CHANNELS_FILE.writeText( + json.stringify( + ServiceChannel.serializer().list, + serviceChannels + ) + ) } } \ No newline at end of file diff --git a/src/main/kotlin/de/wulkanat/DataIO.kt b/src/main/kotlin/de/wulkanat/DataIO.kt index dd78425..d84571a 100644 --- a/src/main/kotlin/de/wulkanat/DataIO.kt +++ b/src/main/kotlin/de/wulkanat/DataIO.kt @@ -1,6 +1,10 @@ package de.wulkanat +import de.wulkanat.extensions.ensureExists import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonConfiguration +import kotlinx.serialization.list import java.io.File @Serializable @@ -11,6 +15,11 @@ data class DiscordChannel( var message: CustomMessage? = null ) +@Serializable +data class ServiceChannel( + val id: Long +) + @Serializable data class CustomMessage( var message: String, @@ -19,12 +28,16 @@ data class CustomMessage( @Serializable data class AdminFile( - val adminId: Long, - val token: String, - val updateMs: Long, - val watchingMessage: String + val adminId: Long = 12345, + val token: String = "12345", + val updateMs: Long = 30000, + val watchingMessage: String = "for new Blogposts", + val offlineMessage: String = "CONNECTION FAILED" ) -val SERVERS_FILE = File("servers.json") -val TEST_FILE = File("test.json") -val ADMIN_FILE = File("admin.json") +val json = Json(JsonConfiguration.Stable) + +val SERVERS_FILE = File("servers.json").ensureExists(json.stringify(DiscordChannel.serializer().list, listOf())) +val SERVICE_CHANNELS_FILE = + File("service_channels.json").ensureExists(json.stringify(ServiceChannel.serializer().list, listOf())) +val ADMIN_FILE = File("admin.json").ensureExists(json.stringify(AdminFile.serializer(), AdminFile())) diff --git a/src/main/kotlin/de/wulkanat/DiscordRpc.kt b/src/main/kotlin/de/wulkanat/DiscordRpc.kt new file mode 100644 index 0000000..94137b6 --- /dev/null +++ b/src/main/kotlin/de/wulkanat/DiscordRpc.kt @@ -0,0 +1,15 @@ +package de.wulkanat + +import net.dv8tion.jda.api.JDA +import net.dv8tion.jda.api.entities.Activity + +object DiscordRpc { + var jda: JDA? = null + + fun updatePresence(available: Boolean) { + jda ?: return + + jda!!.presence.activity = Activity.watching(if (available) Admin.message else Admin.offlineMessage) + jda!!.presence.isIdle = !available + } +} \ No newline at end of file diff --git a/src/main/kotlin/de/wulkanat/Main.kt b/src/main/kotlin/de/wulkanat/Main.kt index 3954c72..f409e59 100644 --- a/src/main/kotlin/de/wulkanat/Main.kt +++ b/src/main/kotlin/de/wulkanat/Main.kt @@ -20,6 +20,7 @@ fun main() { Channels.jda = builder Admin.jda = builder + DiscordRpc.jda = builder Admin.info() Runtime.getRuntime().addShutdownHook(object : Thread() { diff --git a/src/main/kotlin/de/wulkanat/OwnerCli.kt b/src/main/kotlin/de/wulkanat/OwnerCli.kt index 7a20eee..3c7cfec 100644 --- a/src/main/kotlin/de/wulkanat/OwnerCli.kt +++ b/src/main/kotlin/de/wulkanat/OwnerCli.kt @@ -109,6 +109,28 @@ class OwnerCli : ListenerAdapter() { event.message.channel.sendMessage("Channel is not registered.").queue() } } + "serviceChannel" -> { + if (command.size > 1 && listOf("add", "remove").contains(command[1])) { + if (command[1] == "add") { + if (Channels.serviceChannels.find { it.id == channelId } != null) { + event.message.channel.sendMessage("Already a service channel.").queue() + } else { + Channels.serviceChannels.add(ServiceChannel(channelId)) + Channels.saveChannels() + event.message.channel.sendMessage("Added as service channel.").queue() + } + } else { + event.message.channel.sendMessage( + if (Channels.serviceChannels.removeAll { it.id == channelId }) "Channel removed." + else "Not a service channel." + ).queue() + } + Channels.saveChannels() + } else { + event.message.channel.sendMessage("Usage: `${prefix}serviceChannel [add|remove]`") + } + + } "publishMessage" -> { val result = Channels.channels.find { it.id == channelId } if (result != null) { @@ -133,7 +155,12 @@ class OwnerCli : ListenerAdapter() { EmbedBuilder() .setTitle("Server overview") .setColor(Color.GREEN) - .setDescription(Channels.getServerNames(event.message.guild.idLong).joinToString("\n")) + .setDescription(""" + ${Channels.getServerNames(event.message.guild.idLong).joinToString("\n")} + + **_Service Channels_** + ${Channels.getServiceChannelServers(event.message.guild.idLong).joinToString("\n")} + """.trimIndent()) .setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl) .build() ).queue() @@ -160,6 +187,8 @@ class OwnerCli : ListenerAdapter() { """ **${prefix}add** Add this channel to the notified list + **${prefix}serviceChannel [add|remove]** + Add or remove this channel to receive service message from the bot developer (recommended) **${prefix}remove** Remove this channel to the notified list **${prefix}publish [on|off]** diff --git a/src/main/kotlin/de/wulkanat/extensions/File.kt b/src/main/kotlin/de/wulkanat/extensions/File.kt new file mode 100644 index 0000000..6ab8b3a --- /dev/null +++ b/src/main/kotlin/de/wulkanat/extensions/File.kt @@ -0,0 +1,11 @@ +package de.wulkanat.extensions + +import java.io.File + +fun File.ensureExists(defaultText: String? = null): File { + if (!this.exists()) { + this.createNewFile() + this.writeText(defaultText ?: return this) + } + return this +} \ No newline at end of file diff --git a/src/main/kotlin/de/wulkanat/web/SiteWatcher.kt b/src/main/kotlin/de/wulkanat/web/SiteWatcher.kt index 1471749..db76ed8 100644 --- a/src/main/kotlin/de/wulkanat/web/SiteWatcher.kt +++ b/src/main/kotlin/de/wulkanat/web/SiteWatcher.kt @@ -1,6 +1,7 @@ package de.wulkanat.web import de.wulkanat.Admin +import de.wulkanat.DiscordRpc import de.wulkanat.model.BlogPostPreview import org.jsoup.Jsoup import java.io.IOException @@ -8,6 +9,7 @@ import java.io.IOException object SiteWatcher { private const val BLOG_INDEX_URL = "https://www.hytale.com/news" var newestBlog: BlogPostPreview? = null + private var siteOnline = false fun hasNewBlogPost(): Boolean { try { @@ -26,10 +28,17 @@ object SiteWatcher { } } catch (e: IOException) { Admin.error("Connection to Hytale Server failed", e.message ?: e.localizedMessage) + siteOnline = false + DiscordRpc.updatePresence(siteOnline) return false } + if (siteOnline) { + siteOnline = true + DiscordRpc.updatePresence(siteOnline) + } + return true } } \ No newline at end of file