1 Commits

Author SHA1 Message Date
Wieland Schöbl
a8b148c4c7 no idea what that did 2021-01-08 15:30:22 +01:00
32 changed files with 1083 additions and 537 deletions

7
.gitignore vendored
View File

@@ -1,7 +1,6 @@
servers.json
config.json
admin.json
service_channels.json
*.hprof
/build/
/.gradle/
/.idea/
/build
/.gradle

12
.idea/HytaleUpdateBot.iml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id="HytaleUpdateBot:main" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="de.wulkanat" external.system.module.version="1.4.2" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -2,7 +2,6 @@
<dictionary name="wulkanat">
<words>
<w>crosspost</w>
<w>hytale</w>
</words>
</dictionary>
</component>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/HytaleUpdateBot.iml" filepath="$PROJECT_DIR$/.idea/HytaleUpdateBot.iml" />
</modules>
</component>
</project>

175
.idea/workspace.xml generated
View File

@@ -4,9 +4,9 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="no idea what that did">
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/kotlin/de/wulkanat/DiscordRpc.kt" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/kotlin/de/wulkanat/DiscordRpc.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main/kotlin/de/wulkanat/OwnerCli.kt" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/kotlin/de/wulkanat/OwnerCli.kt" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -31,6 +31,11 @@
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
</path>
</expand>
<select />
</tree_state>
@@ -51,7 +56,7 @@
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="not-sure" />
<entry key="$PROJECT_DIR$" value="add-twitter-integration" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -60,7 +65,7 @@
<setting file="file://$PROJECT_DIR$/build.gradle" root0="SKIP_INSPECTION" />
</component>
<component name="MacroExpansionManager">
<option name="directoryName" value="o7p0t8es" />
<option name="directoryName" value="5kk4ojxu" />
</component>
<component name="ProjectId" id="1g2oQiuUv1Bu6ZCW2NSVzB1V6Sc" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
@@ -71,26 +76,27 @@
<component name="PropertiesComponent">
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
<property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/build/libs" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/src/main/kotlin/de/wulkanat/cli" />
<property name="project.structure.last.edited" value="Modules" />
<property name="project.structure.proportion" value="0.15" />
<property name="project.structure.side.proportion" value="0.2" />
<property name="settings.editor.selected.configurable" value="reference.settingsdialog.project.gradle" />
</component>
<component name="RecentsManager">
<key name="MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY">
<recent name="de.wulkanat" />
</key>
<key name="CopyFile.RECENT_KEYS">
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot\build\libs" />
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot\src" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot" />
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot\src\main\kotlin\de\wulkanat" />
</key>
<key name="MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY">
<recent name="de.wulkanat" />
</key>
<key name="CopyFile.RECENT_KEYS">
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot\src\main\kotlin\de\wulkanat\cli" />
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot\build\libs" />
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot\src" />
</key>
</component>
<component name="RunManager" selected="Application.MainKt">
<component name="RunManager" selected="Gradle.HytaleUpdateBot [fatJar]">
<configuration name="HytaleUpdateBot [build]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
@@ -173,10 +179,11 @@
</configuration>
<recent_temporary>
<list>
<item itemvalue="Gradle.HytaleUpdateBot [fatJar]" />
<item itemvalue="Gradle.HytaleUpdateBot [build]" />
<item itemvalue="Gradle.HytaleUpdateBot [clean]" />
<item itemvalue="Application.MainKt" />
<item itemvalue="JAR Application.HytaleUpdateBot-all-1.0-SNAPSHOT.jar" />
<item itemvalue="Gradle.HytaleUpdateBot [fatJar]" />
<item itemvalue="Gradle.HytaleUpdateBot [clean]" />
<item itemvalue="Gradle.HytaleUpdateBot [build]" />
</list>
</recent_temporary>
</component>
@@ -227,28 +234,136 @@
<option name="project" value="LOCAL" />
<updated>1601042375685</updated>
</task>
<option name="localTasksCounter" value="6" />
<task id="LOCAL-00006" summary="fix connection failed message not disappearing">
<created>1601042672099</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1601042672099</updated>
</task>
<option name="localTasksCounter" value="7" />
<servers />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
<option name="oldMeFiltersMigrated" value="true" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Add auto publish feature" />
<MESSAGE value="[1.1]" />
<MESSAGE value="Add service announcement channel" />
<MESSAGE value="fix crash on missing permission&#10;add removeInactive command" />
<MESSAGE value="prepare twitter integration" />
<option name="LAST_COMMIT_MESSAGE" value="prepare twitter integration" />
<MESSAGE value="fix connection failed message not disappearing" />
<option name="LAST_COMMIT_MESSAGE" value="fix connection failed message not disappearing" />
</component>
<component name="WindowStateProjectService">
<state x="552" y="179" key="#Project_Structure" timestamp="1601148661909">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="552" y="179" key="#Project_Structure/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601148661909" />
<state x="-1050" y="581" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1597352463714">
<screen x="-1050" y="105" width="1050" height="1640" />
</state>
<state x="-1050" y="581" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597352463714" />
<state x="640" y="249" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog" timestamp="1597933755909">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="640" y="249" key="#com.intellij.openapi.updateSettings.impl.PluginUpdateInfoDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597933755909" />
<state x="633" y="446" key="#com.intellij.refactoring.move.MoveHandler.SelectRefactoringDialog" timestamp="1597362173063">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="633" y="446" key="#com.intellij.refactoring.move.MoveHandler.SelectRefactoringDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597362173063" />
<state x="690" y="268" key="#com.intellij.refactoring.safeDelete.UnsafeUsagesDialog" timestamp="1597831342920">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="690" y="268" key="#com.intellij.refactoring.safeDelete.UnsafeUsagesDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597831342920" />
<state x="739" y="173" width="484" height="693" key="#org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog" timestamp="1597362199927">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="739" y="173" width="484" height="693" key="#org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597362199927" />
<state x="128" y="270" width="490" height="591" key="#xdebugger.evaluate" timestamp="1597332665464">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="128" y="270" width="490" height="591" key="#xdebugger.evaluate/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597332665464" />
<state x="569" y="115" key="CommitChangelistDialog2" timestamp="1601042670025">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="569" y="115" key="CommitChangelistDialog2/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601042670025" />
<state x="740" y="238" key="FileChooserDialogImpl" timestamp="1597605616287">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="740" y="238" key="FileChooserDialogImpl/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597605616287" />
<state width="1877" height="161" key="GridCell.Tab.0.bottom" timestamp="1601059408217">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1877" height="161" key="GridCell.Tab.0.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601059408217" />
<state width="1877" height="161" key="GridCell.Tab.0.center" timestamp="1601059408216">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329411" />
<state width="1877" height="161" key="GridCell.Tab.0.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601059408216" />
<state width="1877" height="161" key="GridCell.Tab.0.left" timestamp="1601059408216">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329411" />
<state width="1877" height="161" key="GridCell.Tab.0.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601059408216" />
<state width="1877" height="161" key="GridCell.Tab.0.right" timestamp="1601059408216">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329411" />
<state width="1877" height="161" key="GridCell.Tab.0.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601059408216" />
<state width="1876" height="348" key="GridCell.Tab.1.bottom" timestamp="1597840755247">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1876" height="348" key="GridCell.Tab.1.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597840755247" />
<state width="1876" height="348" key="GridCell.Tab.1.center" timestamp="1597840755246">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1876" height="348" key="GridCell.Tab.1.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597840755246" />
<state width="1876" height="348" key="GridCell.Tab.1.left" timestamp="1597840755246">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1876" height="348" key="GridCell.Tab.1.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597840755246" />
<state width="1876" height="348" key="GridCell.Tab.1.right" timestamp="1597840755247">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1876" height="348" key="GridCell.Tab.1.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597840755247" />
<state x="540" y="255" key="IDE.errors.dialog" timestamp="1601148874155">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="540" y="255" key="IDE.errors.dialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601148874155" />
<state x="672" y="237" key="MultipleFileMergeDialog" timestamp="1597438068748">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="672" y="237" key="MultipleFileMergeDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438068748" />
<state x="94" y="257" key="SettingsEditor" timestamp="1597361509050">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="-1040" y="568" key="SettingsEditor/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597353858648" />
<state x="94" y="257" key="SettingsEditor/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597361509050" />
<state x="552" y="254" key="Vcs.Push.Dialog.v2" timestamp="1601042674727">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="552" y="254" key="Vcs.Push.Dialog.v2/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601042674727" />
<state x="777" y="434" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2" timestamp="1601042455663">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="777" y="434" key="com.intellij.openapi.vcs.update.UpdateOrStatusOptionsDialogupdate-v2/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1601042455663" />
<state x="497" y="233" key="new project wizard" timestamp="1597841063797">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="497" y="233" key="new project wizard/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597841063797" />
<state x="2582" y="100" key="new project wizard/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@1920.-213.2560.1400" timestamp="1597605657341" />
<state x="616" y="240" key="run.anything.popup" timestamp="1597325088886">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="616" y="240" key="run.anything.popup/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597325088886" />
<state x="623" y="225" width="672" height="678" key="search.everywhere.popup" timestamp="1597702900013">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="623" y="225" width="672" height="678" key="search.everywhere.popup/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597702900013" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>

View File

@@ -31,17 +31,15 @@ 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 `config.json`, `servers.json` and `service_channels.json`
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 `config.json`,
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 -Xmx1024m -jar [server-file-name]`. To stop it you can either type `!stop` in the Admin Console (Discord PM) or
`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]`
I'm not 100% certain how much RAM the bot needs, default is typically `-Xmx256m`, and that lead to some issues, `-Xmx512m` is probably plenty, because my server has
tons of unused ram I set it to `-Xmx2048m`, just try and look what works for you.
## Compiling yourself
I developed it under Windows, and had some trouble compiling it on Linux. You mileage may vary.
@@ -59,7 +57,7 @@ I developed it under Windows, and had some trouble compiling it on Linux. You mi
| !help | | Show a help dialog with all these commands |
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 `config.json`.
come from the admin registered in the `admin.json`.
## TODO
@@ -69,4 +67,4 @@ we were over the official Hytale Twitter.
## Other
Thanks to [Forcellrus](https://github.com/Forcellrus/Discord-Auto-Publisher) for discovering a way to auto publish messages
in news channels
in news channels

47
build.gradle Normal file
View File

@@ -0,0 +1,47 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.61'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.3.61'
}
group 'de.wulkanat'
version '1.4.2'
repositories {
mavenCentral()
jcenter()
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
compile 'net.dv8tion:JDA:4.2.0_189'
compile 'org.jsoup:jsoup:1.13.1'
compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
jar {
manifest {
attributes('Main-Class': 'de.wulkanat.MainKt')
}
}
task fatJar(type: Jar) {
baseName = project.name + '-all'
from((configurations.compile.findAll { !it.path.endsWith(".pom") }).collect {
it.isDirectory() ? it : zipTree(it)
})
with jar
manifest {
attributes 'Main-Class': 'de.wulkanat.MainKt',
'Implementation-Version': version
}
}
apply plugin: 'kotlinx-serialization'

View File

@@ -1,61 +0,0 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
val kotlinVersion = "1.4.10"
kotlin("jvm") version kotlinVersion
kotlin("plugin.serialization") version kotlinVersion
id("org.jetbrains.kotlin.kapt") version kotlinVersion
}
group = "de.wulkanat"
version = "2.0.0"
repositories {
mavenCentral()
jcenter()
maven("https://kotlin.bintray.com/koltinx")
maven("https://dl.bintray.com/kordlib/Kord")
}
dependencies {
testImplementation(kotlin("test-junit"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.jsoup:jsoup:1.13.1")
implementation("dev.kord:kord-common:0.7.0-RC")
implementation("com.gitlab.kordlib.kordx:kordx-commands-runtime-kord:0.3.4")
implementation("com.gitlab.kordlib:kordx.emoji:0.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2")
kapt("com.gitlab.kordlib.kordx:kordx-commands-processor:0.3.4")
}
tasks.test {
useJUnit()
}
tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}
tasks.withType<Jar> {
manifest {
attributes(mapOf(Pair("Main-Class", "de.wulkanat.MainKt")))
}
}
tasks.create<Jar>("fatJar") {
archiveBaseName.set("${project.name}-all")
manifest {
attributes["Implementation-Version"] = archiveVersion
attributes["Main-Class"] = "de.wulkanat.MainKt"
}
from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) })
with(tasks.jar.get() as CopySpec)
}

View File

@@ -1,5 +1,6 @@
#Thu Aug 13 18:41:46 CEST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

2
settings.gradle Normal file
View File

@@ -0,0 +1,2 @@
rootProject.name = 'HytaleUpdateBot'

View File

@@ -1,2 +0,0 @@
rootProject.name = "HytaleUpdateBot"

View File

@@ -1,82 +1,141 @@
package de.wulkanat
import com.gitlab.kordlib.common.entity.Snowflake
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.behavior.channel.createEmbed
import com.gitlab.kordlib.core.entity.User
import com.gitlab.kordlib.rest.builder.message.EmbedBuilder
import de.wulkanat.files.Config
import de.wulkanat.files.ServiceChannels
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.User
import java.awt.Color
object Admin {
var jda: Kord? = null
val userId: Long
val token: String
val updateMs: Long
val message: String
val offlineMessage: String
init {
val admin = Json(JsonConfiguration.Stable).parse(AdminFile.serializer(), ADMIN_FILE.readText())
userId = admin.adminId
token = admin.token
updateMs = admin.updateMs
message = admin.watchingMessage
offlineMessage = admin.offlineMessage
}
var jda: JDA? = null
set(value) {
field = value
GlobalScope.launch {
admin = value?.getUser(Snowflake(Config.adminId))
if (admin == null) {
kotlin.io.println("Connection to de.wulkanat.Admin failed!")
} else {
kotlin.io.println("Connected to ${admin!!.username}. No further errors will be printed here.")
}
admin = value?.retrieveUserById(userId)?.complete()
if (admin == null) {
kotlin.io.println("Connection to de.wulkanat.Admin failed!")
} else {
kotlin.io.println("Connected to ${admin!!.name}. No further errors will be printed here.")
}
}
var admin: User? = null
suspend fun println(msg: String) {
sendDevMessage(msg) {
title = msg
color = Color.WHITE
}
fun println(msg: String) {
sendDevMessage(
EmbedBuilder()
.setTitle(msg)
.setColor(Color.WHITE)
.build(),
msg
)
}
suspend fun error(msg: String, error: String, author: User? = null) {
sendDevMessage("$msg\n\n$error") {
title = msg
description = error
color = Color.RED
author?.let { author {
name = it.tag
icon = it.avatar.url
url = it.avatar.url
}}
}
fun printlnBlocking(msg: String) {
senDevMessageBlocking(
EmbedBuilder()
.setTitle(msg)
.setColor(Color.WHITE)
.build(),
msg
)
}
suspend fun warning(msg: String) {
sendDevMessage(msg) {
title = msg
color = Color.YELLOW
}
fun error(msg: String, error: String, author: User? = null) {
sendDevMessage(
EmbedBuilder()
.setTitle(msg)
.setDescription(error)
.setColor(Color.RED)
.run {
if (author == null) {
this
} else {
this.setAuthor(author.asTag, author.avatarUrl, author.avatarUrl)
}
}
.build()
, "$msg\n\n${error}"
)
}
suspend fun info() {
sendDevMessage("Now watching for new Hytale BlogPosts") {
title = "Now watching for new Hytale Blogposts every ${Config.updateMs / 1000}s"
description = """
${ServiceChannels.getServerNames().joinToString("\n")}
**_Service Channels_**
${ServiceChannels.getServiceChannelServers().joinToString("\n")}
""".trimIndent()
color = Color.GREEN
}
fun errorBlocking(msg: String, error: Exception) {
senDevMessageBlocking(
EmbedBuilder()
.setTitle(msg)
.setDescription(error.message)
.setColor(Color.RED)
.build()
, "$msg\n\n${error.message}"
)
}
fun warning(msg: String) {
sendDevMessage(
EmbedBuilder()
.setTitle(msg)
.setColor(Color.YELLOW)
.build(),
msg
)
}
fun info() {
sendDevMessage(
EmbedBuilder()
.setTitle("Now watching for new Hytale Blogposts every ${updateMs / 1000}s")
.setDescription("""
${Channels.getServerNames().joinToString("\n")}
**_Service Channels_**
${Channels.getServiceChannelServers().joinToString("\n")}
""".trimIndent())
.setColor(Color.GREEN)
.build(),
"Now watching for new Hytale BlogPosts"
)
}
fun silent(msg: String) {
kotlin.io.println(msg)
}
private suspend inline fun sendDevMessage(fallback: String, crossinline embed: EmbedBuilder.() -> Unit) {
val devChannel = admin?.getDmChannel() ?: kotlin.run {
private fun senDevMessageBlocking(messageEmbed: MessageEmbed, fallback: String) {
admin = jda!!.retrieveUserById(userId).complete()
val devChannel = admin?.openPrivateChannel() ?: kotlin.run {
kotlin.io.println(fallback)
return
}
devChannel.createEmbed(embed)
devChannel.complete()
.sendMessage(messageEmbed).complete()
}
fun sendDevMessage(messageEmbed: MessageEmbed, fallback: String) {
val devChannel = admin?.openPrivateChannel() ?: kotlin.run {
kotlin.io.println(fallback)
return
}
devChannel.queue {
it.sendMessage(messageEmbed).queue()
}
}
}

View File

@@ -0,0 +1,91 @@
package de.wulkanat
import de.wulkanat.model.BlogPostPreview
import net.dv8tion.jda.api.hooks.ListenerAdapter
import de.wulkanat.web.SiteWatcher
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent
import java.awt.Color
import kotlin.system.exitProcess
class AdminCli : ListenerAdapter() {
val prefix = "!"
override fun onPrivateMessageReceived(event: PrivateMessageReceivedEvent) {
val msg = event.message.contentRaw
if (event.author.idLong != Admin.userId ||
!msg.startsWith(prefix)
) {
return
}
val command = Regex("[^\\s`]+|`[^`]*`").findAll(msg.removePrefix("!")).toList()
when (command[0].value) {
"stop" -> exitProcess(1)
"fakeUpdate" -> {
SiteWatcher.newestBlog = BlogPostPreview(
title = "FakePost",
imgUrl = "",
fullPostUrl = "",
author = "wulkanat",
date = "now",
description = "Lorem Ipsum"
)
Admin.println("Posting on next update cycle.")
}
"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.refreshChannelsFromDisk()
Channels.serviceChannels = Channels.refreshServiceChannelsFromDisk()
Admin.info()
}
"removeInactive" -> {
Channels.channels.removeAll { channel ->
Channels.testServerId(channel.id) ?: run {
Admin.println("Removed ${channel.id}")
null
} == null
}
Admin.info()
Channels.saveChannels()
}
"help" -> {
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Help")
.setColor(Color.YELLOW)
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.setDescription(
"""
**${prefix}stop**
Stop the bot
**${prefix}fakeUpdate**
Post a fake update to every registered channel (can be used if bot missed the update)
**${prefix}info**
Show an overview over all registered channels
**${prefix}serviceMessage [title] [message]**
Show a service message (update info etc) to all registered service channels
**${prefix}refreshList**
Refresh server list from disk
**${prefix}removeInactive**
Remove inactive channels
**${prefix}help**
Show this message
""".trimIndent()
)
.build()
).queue()
}
}
}
}

View File

@@ -1,9 +1,6 @@
package de.wulkanat.files
package de.wulkanat
import com.gitlab.kordlib.core.Kord
import com.gitlab.kordlib.core.entity.Embed
import com.gitlab.kordlib.rest.builder.message.EmbedBuilder
import de.wulkanat.*
import de.wulkanat.extensions.crosspost
import kotlinx.serialization.list
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.JDA
@@ -12,8 +9,8 @@ import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.TextChannel
import java.awt.Color
object ServiceChannels {
var client: Kord? = null
object Channels {
var jda: JDA? = null
/**
* List of (ServerID, ChannelID)
@@ -21,13 +18,13 @@ object ServiceChannels {
var channels: MutableList<DiscordChannel> = refreshChannelsFromDisk()
var serviceChannels: MutableList<ServiceChannel> = refreshServiceChannelsFromDisk()
fun sentToAll(messageEmbed: Embed) {
if (client == null)
fun sentToAll(messageEmbed: MessageEmbed) {
if (jda == null)
return
for (channel_pair in channels) {
try {
val channel = client!!.getTextChannelById(channel_pair.id) ?: continue
val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue
val customMessage = channel_pair.message?.message ?: ""
if (channel_pair.mentionedRole != null) {
@@ -69,14 +66,13 @@ object ServiceChannels {
.build()
for (channelInfo in serviceChannels) {
val channel = client!!.getTextChannelById(channelInfo.id)
val channel = jda!!.getTextChannelById(channelInfo.id)
channel?.sendMessage(serviceMessage)?.queue()
}
Admin.println("Service message distributed to ${serviceChannels.size} channels.")
Admin.sendDevMessage(
serviceMessage, """
Admin.sendDevMessage(serviceMessage, """
***************
SERVICE MESSAGE
@@ -84,13 +80,12 @@ object ServiceChannels {
-------
$message
***************
""".trimIndent()
)
""".trimIndent())
}
fun checkEveryonePermission() {
for (channel_pair in channels) {
val channel = client!!.getTextChannelById(channel_pair.id) ?: continue
val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue
if (channel_pair.mentionedRole == "everyone" &&
channel.guild.selfMember.hasPermission(Permission.MESSAGE_MENTION_EVERYONE)
@@ -116,11 +111,11 @@ object ServiceChannels {
}
fun getServerNames(server: Long? = null): List<String> {
if (client == null)
if (jda == null)
return listOf()
return channels.filter { server == null || (client!!.getTextChannelById(it.id)?.guild?.idLong == server) }.map {
val channel = client!!.getTextChannelById(it.id)
return channels.filter { server == null || (jda!!.getTextChannelById(it.id)?.guild?.idLong == server) }.map {
val channel = jda!!.getTextChannelById(it.id)
if (channel == null) {
Admin.warning("Channel ${it.id} is no longer active!")
return@map "**${it.id}** *(inactive)*"
@@ -142,17 +137,17 @@ object ServiceChannels {
}
fun getServiceChannelServers(server: Long? = null): List<String> {
if (client == null)
if (jda == null)
return listOf()
return serviceChannels.filter { server == null || (client!!.getTextChannelById(it.id)?.guild?.idLong == server) }.map {
val channel = client!!.getTextChannelById(it.id)
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 client?.getTextChannelById(id)
return jda?.getTextChannelById(id)
}
fun addChannel(id: Long, role: String?): DiscordChannel? {

View File

@@ -10,7 +10,7 @@ import java.io.File
@Serializable
data class DiscordChannel(
val id: Long,
var mentionedRole: Long? = null,
var mentionedRole: String? = null,
var autoPublish: Boolean = false,
var message: CustomMessage? = null
)

View File

@@ -7,11 +7,9 @@ object DiscordRpc {
var jda: JDA? = null
fun updatePresence(available: Boolean) {
// jda ?: return
jda ?: return
// jda!!.presence.activity = Activity.watching(if (available) Admin.message else Admin.offlineMessage)
// jda!!.presence.isIdle = !available
// noop
if (available) Admin.println("Back online") else Admin.error("Gone offline", "Can't reach Hytale server")
jda!!.presence.activity = Activity.watching(if (available) Admin.message else Admin.offlineMessage)
jda!!.presence.isIdle = !available
}
}

View File

@@ -1,6 +1,5 @@
package de.wulkanat
import de.wulkanat.files.ServiceChannels
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.requests.GatewayIntent
@@ -19,7 +18,7 @@ fun main() {
builder.addEventListener(OwnerCli())
builder.awaitReady()
ServiceChannels.client = builder
Channels.jda = builder
Admin.jda = builder
DiscordRpc.jda = builder
Admin.info()
@@ -35,7 +34,7 @@ fun main() {
timer("Updater", daemon = true, initialDelay = 0L, period = Admin.updateMs) {
if (SiteWatcher.hasNewBlogPost()) {
ServiceChannels.sentToAll(SiteWatcher.newestBlog!!.toMessageEmbed())
Channels.sentToAll(SiteWatcher.newestBlog!!.toMessageEmbed())
}
}
}

View File

@@ -0,0 +1,306 @@
package de.wulkanat
import de.wulkanat.cli.Cli
import de.wulkanat.cli.makeCli
import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
class OwnerCli : ListenerAdapter() {
private val cli: Cli<PrivateMessageReceivedEvent> = makeCli(prefix = "!") {
command name "add" does "Add this channel to the notified list" through ::OwnerCliStuff.addChannel
command name "remove" does "Remove this channel to the notified list" through removeChannel
command name "publish" with { required literal argument with "on" or "off" } does
"[Community|Partner|Verified only] Auto publish the message if in an announcement channel" through publish
command name "ping" with { required string argument } does "What role to ping" through ping
command name "setMessage" with { required string argument } does "Set a custom message to show" through setMessage
}
}
object OwnerCliStuff {
private fun addChannel(_required: List<String>, _optional: MutableMap<String, String>, event: PrivateMessageReceivedEvent) {
val result = Channels.addChannel(event.channel.idLong, null)
if (result == null) {
event.message.channel.sendMessage("Already added.").queue()
} else {
event.message.channel.sendMessage("Added.").queue()
Admin.info()
}
}
private val removeChannel =
{ _: List<String>, _: MutableMap<String, String>, event: PrivateMessageReceivedEvent ->
val result = Channels.channels.removeAll { it.id == event.channel.idLong }
Channels.saveChannels()
if (result) {
event.message.channel.sendMessage("Removed.").queue()
} else {
event.message.channel.sendMessage("This channel is not registered.").queue()
}
}
private val publish =
publish@{ required: List<String>, _: MutableMap<String, String>, event: PrivateMessageReceivedEvent ->
val channel = Channels.channels.find { it.id == event.channel.idLong } ?: run {
event.message.channel.sendMessage("Channel not registered.").queue()
return@publish
}
channel.autoPublish = required.first() == "on"
Channels.saveChannels()
event.message.channel.sendMessage("Auto publish is now ${required.first()}").queue()
}
private val ping =
ping@{ required: List<String>, _: MutableMap<String, String>, event: PrivateMessageReceivedEvent ->
val channel = Channels.channels.find { it.id == event.channel.idLong } ?: run {
event.message.channel.sendMessage("Channel is not registered.").queue()
return@ping
}
val roleName = required.first()
val role = event.message.guild.getRolesByName(required.first(), false).firstOrNull()
channel.mentionedRole = when {
roleName == "everyone" -> {
event.message.channel.sendMessage("Now pinging $roleName.").queue()
roleName
}
roleName == "none" -> {
event.message.channel.sendMessage("Now pinging $roleName.").queue()
null
}
role != null -> {
event.message.channel.sendMessage("Now pinging ${role.name}").queue()
role.id
}
else -> {
event.message.channel.sendMessage("Unknown role.").queue()
channel.mentionedRole
}
}
Channels.saveChannels()
}
private val setMessage =
setMessage@{ required: List<String>, _: MutableMap<String, String>, event: PrivateMessageReceivedEvent ->
val result = Channels.channels.find { it.id == event.channel.channelId } ?: run {
event.message.channel.sendMessage("Channel is not registered.").queue()
return@setMessage
}
val message = required.first()
result.message = CustomMessage(message)
Channels.saveChannels()
event.message.channel.sendMessage("Set `$message` as message.").queue()
}
}
/*class OwnerCli2 : ListenerAdapter() {
private val prefix = "%!"
override fun onMessageReceived(event: MessageReceivedEvent) {
val msg = event.message.contentRaw
// Only accept admin requests
if (event.message.member?.hasPermission(Permission.ADMINISTRATOR) != true || !msg.startsWith(prefix)) {
return
}
val command = msg.removePrefix(prefix).split(Regex("\\s+"))
val channelId = event.message.channel.idLong
when (command.first()) {
"add" -> {
val result = Channels.addChannel(channelId, null)
if (result == null) {
event.message.channel.sendMessage("Already added.").queue()
} else {
event.message.channel.sendMessage("Added.").queue()
Admin.info()
}
}
"remove" -> {
val result = Channels.channels.removeAll { it.id == channelId }
Channels.saveChannels()
if (result) {
event.message.channel.sendMessage("Removed.").queue()
} else {
event.message.channel.sendMessage("This channel is not registered.").queue()
}
}
"publish" -> {
val result = Channels.channels.find { it.id == channelId }
if (result != null) {
if (command.size > 1 && listOf("on", "off").contains(command[1])) {
result.autoPublish = command[1] == "on"
Channels.saveChannels()
event.message.channel.sendMessage("Auto publish is now ${command[1]}").queue()
} else {
event.message.channel.sendMessage("Usage: `${prefix}publish [on|off]`")
}
} else {
event.message.channel.sendMessage("Channel not registered.").queue()
}
}
"ping" -> {
val result = Channels.channels.find { it.id == channelId }
if (result != null) {
if (command.size > 1) {
val roles = event.message.guild.getRolesByName(command[1], false)
result.mentionedRole = when {
command[1] == "everyone" -> {
event.message.channel.sendMessage("Now pinging everyone.").queue()
"everyone"
}
command[1] == "none" -> {
event.message.channel.sendMessage("Now pinging none.").queue()
null
}
roles.firstOrNull() != null -> {
event.message.channel.sendMessage("Now pinging ${roles.first().name}").queue()
roles.first().id
}
else -> {
event.message.channel.sendMessage("Unknown role.").queue()
result.mentionedRole
}
}
Channels.saveChannels()
} else {
event.message.channel.sendMessage("Usage: `${prefix}ping [everyone|none|roleName]`")
}
} else {
event.message.channel.sendMessage("Channel is not registered.").queue()
}
}
"setMessage" -> {
val result = Channels.channels.find { it.id == channelId }
if (result != null) {
if (command.size > 1) {
val message = event.message.contentRaw.removePrefix("${prefix}setMessage").trim()
result.message = CustomMessage(message)
Channels.saveChannels()
event.message.channel.sendMessage("Set `$message` as message.").queue()
} else {
event.message.channel.sendMessage("Usage: `${prefix}setMessage [message]`")
}
} else {
event.message.channel.sendMessage("Channel is not registered.").queue()
}
}
"resetMessage" -> {
val result = Channels.channels.find { it.id == channelId }
if (result != null) {
result.message = null
Channels.saveChannels()
event.message.channel.sendMessage("Reset to no message.").queue()
} else {
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) {
if (result.message != null) {
if (command.size > 1 && listOf("on", "off").contains(command[1])) {
result.message?.pushAnnouncement = command[1] == "on"
Channels.saveChannels()
event.message.channel.sendMessage("Auto publish (message) is now ${command[1]}").queue()
} else {
event.message.channel.sendMessage("Usage: `${prefix}publishMessage [on|off]`")
}
} else {
event.message.channel.sendMessage("Channel has no custom message.").queue()
}
} else {
event.message.channel.sendMessage("Channel not registered.").queue()
}
}
"info" -> {
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Server overview")
.setColor(Color.GREEN)
.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()
}
"report" -> {
val errorReport = event.message.contentRaw.removePrefix("${prefix}report")
Admin.error(event.message.guild.name, errorReport, event.author)
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Error Report Received")
.setColor(Color.RED)
.setDescription(errorReport)
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.build()
).queue()
}
"help" -> {
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Help")
.setColor(Color.YELLOW)
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.setDescription(
"""
**${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]**
[Community|Partner|Verified only] Auto publish the message if in an announcement channel
**${prefix}ping [none|everyone|roleName]**
What role to ping
**${prefix}setMessage [message]**
Set a custom message to show
**${prefix}resetMessage**
Reset the message
**${prefix}info**
Show an overview about all channels registered on this server
**${prefix}report**
Report an issue to the Bot Admin (this will share your user name so they can contact you)
**${prefix}help**
Show this message
""".trimIndent()
)
.build()
).queue()
}
}
}*
}*/

View File

@@ -1,109 +0,0 @@
@file:AutoWired
package de.wulkanat.cli
import com.gitlab.kordlib.core.entity.channel.DmChannel
import com.gitlab.kordlib.kordx.commands.annotation.AutoWired
import com.gitlab.kordlib.kordx.commands.argument.primitive.BooleanArgument
import com.gitlab.kordlib.kordx.commands.argument.text.StringArgument
import com.gitlab.kordlib.kordx.commands.kord.model.precondition.precondition
import com.gitlab.kordlib.kordx.commands.kord.model.prefix.kord
import com.gitlab.kordlib.kordx.commands.kord.model.prefix.mention
import com.gitlab.kordlib.kordx.commands.kord.model.respondEmbed
import com.gitlab.kordlib.kordx.commands.kord.module.module
import com.gitlab.kordlib.kordx.commands.model.command.invoke
import com.gitlab.kordlib.kordx.commands.model.prefix.literal
import com.gitlab.kordlib.kordx.commands.model.prefix.or
import com.gitlab.kordlib.kordx.commands.model.prefix.prefix
import de.wulkanat.Admin
import de.wulkanat.extensions.alsoIf
import de.wulkanat.extensions.isBotAdmin
import de.wulkanat.files.ServiceChannels
import de.wulkanat.model.BlogPostPreview
import de.wulkanat.web.SiteWatcher
val prefixes = prefix {
kord { mention() or literal("%!") }
}
fun adminCommands() = module("admin-commands") {
precondition { author.isBotAdmin && channel.asChannelOrNull() is DmChannel }
command("stop") {
invoke {
respond("Shutting down...")
kord.shutdown()
}
}
command("info") {
invoke {
Admin.info()
}
}
command("fakeUpdate") {
invoke {
respond("THIS WILL CAUSE A MESSAGE ON **ALL** SERVERS.\nContinue? [y/n]")
if (read(BooleanArgument(trueValue = "y", falseValue = "n"))) {
respond("Sending fake update on next cycle")
SiteWatcher.newestBlog = BlogPostPreview(
title = "FakePost",
imgUrl = "",
fullPostUrl = "",
author = "wulkanat",
date = "now",
description = "Lorem Ipsum"
)
} else {
respond("Aborting")
}
}
}
command("serviceMessage") {
invoke {
respond("What's the title?")
val title = read(StringArgument)
respond("What's the message?")
val message = read(StringArgument)
respondEmbed {
this.title = title
description = message
footer {
text = "Is that correct? [y/n]"
}
}
if (read(BooleanArgument(trueValue = "y", falseValue = "n"))) {
respond("Sending")
ServiceChannels.sendServiceMessage(title, message)
} else {
respond("Aborting")
}
}
}
command("refreshList") {
invoke {
ServiceChannels.channels = ServiceChannels.refreshChannelsFromDisk()
ServiceChannels.serviceChannels = ServiceChannels.refreshServiceChannelsFromDisk()
Admin.info()
}
}
command("removeInactive") {
invoke {
respondEmbed {
title = "Channels removed"
ServiceChannels.channels.removeAll { channel ->
(ServiceChannels.testServerId(channel.id) == null).alsoIf(true) {
field { name = channel.id.toString() }
}
}
}
ServiceChannels.saveChannels()
}
}
}

View File

@@ -0,0 +1,18 @@
package de.wulkanat.cli
enum class ArgumentType(val match: Regex, val stringName: String) {
INT(Regex("\\d+"), "int"),
FLOAT(Regex("\\d+(?:.\\d+)?"), "float"),
STRING(Regex("[\\s\\S]+"), "string"),
BOOLEAN(Regex("true|false"), "bool"),
LITERAL(Regex("[\\s\\S]+"), "literal"),
EXISTS(Regex("[\\s\\S]+"), "existence");
fun usage(literals: Map<String, List<String>>, name: String): String {
return when (this) {
LITERAL -> "${literals[name]?.joinToString(separator = "|")}"
EXISTS -> ""
else -> stringName
}
}
}

View File

@@ -0,0 +1,216 @@
package de.wulkanat.cli
class Cli<T>(var prefix: String = ".") {
val commands = mutableMapOf<String, Command<T>>()
fun parse(
command: String,
passThrough: T,
helpMessage: (Cli<T>) -> Unit = {},
commandMisuse: (Command<T>, String) -> Unit = { _, _ -> }
): Boolean? {
if (!command.startsWith(prefix)) return false // not a command
val msg =
Regex("[^\\s`]+|`[^`]*`").findAll(command.removePrefix(prefix)).toList().map { it.value }
if (msg[0] == "help") {
helpMessage(this)
return true
}
val realCommand = commands[msg[0]] ?: return false // command not found
val (required, optional) = realCommand.arguments
if (msg.size < required.list.size + 1) {
commandMisuse(realCommand, "Too few arguments!")
return null
}
val requiredOut: MutableList<String> = mutableListOf()
val optionalOut: MutableMap<String, String> = mutableMapOf()
for (i in 1..required.list.size) {
val (name, type) = required.list[i - 1]
requiredOut.add(
when (type) {
ArgumentType.LITERAL -> required.literals[name]?.find { it == msg[i] }?.toString()
else -> type.match.matchEntire(msg[i])?.value
} ?: kotlin.run {
commandMisuse(realCommand, "Argument '${msg[i]}' is not of type ${type.stringName}!")
return@parse null
}
)
}
var i = required.list.size + 1
while (i < required.list.size + 1) {
val key = optional.shorts[msg[i]] ?: msg[i]
val value = optional.list[optional.shorts[msg[i]] ?: msg[i]] ?: kotlin.run {
commandMisuse(realCommand, "Unknown optional argument '$key'")
return@parse null
}
optionalOut[key] = when (value) {
ArgumentType.LITERAL -> optional.literals[key]?.find { it == msg[i] }?.toString()
else -> value.match.matchEntire(msg[i])?.value
} ?: kotlin.run {
commandMisuse(realCommand, "Argument '$key' is not of type ${value.stringName}!")
return@parse null
}
i += if (value == ArgumentType.EXISTS) 1 else 2
}
realCommand.action(requiredOut, optionalOut, passThrough)
return true // success
}
fun usage(): String {
return commands.map { "$prefix${it.value.usage()}" }.joinToString("\n")
}
infix fun prefix(func: Cli<T>.() -> Unit): Cli<T> {
func()
return this
}
inner class CommandBuilder {
infix fun name(name: String): CommandBuilder2 {
return CommandBuilder2(name)
}
}
inner class CommandBuilder2(val name: String) {
val argumentBuilder = ArgumentBuilder()
var descriptionLocal = ""
infix fun does(description: String): DoesHelper {
descriptionLocal = description
return DoesHelper()
}
inner class DoesHelper {
infix fun through(action: (required: List<String>, optional: MutableMap<String, String>, passthrough: T) -> Unit): Command<T> {
return Command(name, descriptionLocal, action, argumentBuilder).also { commands[name] = it }
}
}
infix fun with(action: ArgumentBuilder.() -> Unit): CommandBuilder2 {
argumentBuilder.action()
return this
}
inner class ArgumentBuilder {
val required = RequiredArgHelper()
val optional = OptionalArgHelper()
operator fun component1() = required
operator fun component2() = optional
inner class RequiredArgHelper {
val list: MutableList<Pair<String, ArgumentType>> = mutableListOf()
val literals: MutableMap<String, MutableList<String>> = mutableMapOf()
infix fun int(name: String) {
list.add(Pair(name, ArgumentType.INT))
}
infix fun float(name: String) {
list.add(Pair(name, ArgumentType.FLOAT))
}
infix fun string(name: String) {
list.add(Pair(name, ArgumentType.STRING))
}
infix fun literal(name: String): LiteralHelper {
list.add(Pair(name, ArgumentType.LITERAL))
return LiteralHelper(name)
}
infix fun bool(name: String) {
list.add(Pair(name, ArgumentType.BOOLEAN))
}
inner class LiteralHelper(val name: String) {
infix fun with(literalsList: String): LiteralHelperHelper {
val list = mutableListOf(literalsList)
literals[name] = list
return LiteralHelperHelper(list)
}
inner class LiteralHelperHelper(private val listListList: MutableList<String>) {
infix fun or(other: String): LiteralHelperHelper {
listListList.add(other)
return this
}
}
}
}
inner class OptionalArgHelper {
val list: MutableMap<String, ArgumentType> = mutableMapOf()
val shorts: MutableMap<String, String> = mutableMapOf()
val literals: MutableMap<String, List<String>> = mutableMapOf()
inner class ShortsHelper(val name: String, val shortsMap: MutableMap<String, String>) {
infix fun short(shortName: String) {
shortsMap[shortName] = name
}
}
infix fun int(name: String): ShortsHelper {
list[name] = ArgumentType.INT
return ShortsHelper(name, shorts)
}
infix fun float(name: String): ShortsHelper {
list[name] = ArgumentType.FLOAT
return ShortsHelper(name, shorts)
}
infix fun string(name: String): ShortsHelper {
list[name] = ArgumentType.STRING
return ShortsHelper(name, shorts)
}
infix fun bool(name: String): ShortsHelper {
list[name] = ArgumentType.BOOLEAN
return ShortsHelper(name, shorts)
}
infix fun literal(name: String): LiteralHelper {
list[name] = ArgumentType.LITERAL
return LiteralHelper(name)
}
infix fun existence(name: String): ShortsHelper {
list[name] = ArgumentType.EXISTS
return ShortsHelper(name, shorts)
}
inner class LiteralHelper(val name: String) {
infix fun with(literalsList: String): LiteralHelperHelper {
val list = mutableListOf<String>()
literals[name] = list
return LiteralHelperHelper(list)
}
inner class LiteralHelperHelper(private val listListList: MutableList<String>) {
infix fun or(other: String): LiteralHelperHelper {
listListList.add(other)
return this
}
}
}
}
}
}
val argument = "REQUIRED_TYPE"
val command = CommandBuilder()
val nothing: (List<String>, MutableMap<String, String>, T) -> Unit = { _, _, _ -> }
}

View File

@@ -0,0 +1,52 @@
package de.wulkanat.cli
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.entities.MessageEmbed
fun <T> Command<T>.discordUsage(): String {
return "${name}_ ${arguments.required.list
.joinToString(separator = " ") {
"**[**${it.first/*it.second.discordUsage(
arguments.required.literals,
it.first
)*/}**]**"
}} ${arguments.optional.list
.map {
"--${it.key}${arguments.optional.shorts[name]?.let { short -> " _-${short}_ " } ?: ""
}${if (it.value == ArgumentType.EXISTS) "" else " **<**${it.value.stringName}**>**"}"
}
.joinToString(separator = " ")}"
}
fun ArgumentType.discordUsage(literals: Map<String, List<String>>, name: String): String {
return when (this) {
ArgumentType.LITERAL -> "${literals[name]?.joinToString(separator = "**|**")}"
ArgumentType.EXISTS -> ""
else -> stringName
}
}
fun <T> Command<T>.discordUsageEmbed(footer: String?): MessageEmbed {
return EmbedBuilder()
.setTitle("Usage:")
.setDescription("_${discordUsage()}")
.also { builder -> footer?.let { builder.setFooter(footer) } }
.build()
}
fun <T> Cli<T>.discordUsage(): String {
return commands.map { "_$prefix${it.value.discordUsage()}" }.joinToString("\n")
}
fun <T> Cli<T>.discordUsageEmbed(): MessageEmbed {
return EmbedBuilder()
.setTitle("Help")
.also {
commands.map { Pair(it.value.description, "_$prefix${it.value.discordUsage()}") }
.forEach { (title, description) ->
it.addField(title, description, false)
}
}
.setFooter("Commands are case-sensitive.")
.build()
}

View File

@@ -0,0 +1,7 @@
package de.wulkanat.cli
fun <T>makeCli(prefix: String = "!", func: Cli<T>.() -> Unit): Cli<T> {
val cli = Cli<T>(prefix)
cli.func()
return cli
}

View File

@@ -0,0 +1,23 @@
package de.wulkanat.cli
class Command<T>(
val name: String,
val description: String,
val action: (required: List<String>, optional: MutableMap<String, String>, passthrough: T) -> Unit,
val arguments: Cli<T>.CommandBuilder2.ArgumentBuilder
) {
fun usage(): String {
return "$name ${arguments.required.list
.joinToString(separator = " ") {
"[${it.second.usage(
arguments.required.literals,
it.first
)}]"
}} ${arguments.optional.list
.map {
"--${it.key}${arguments.optional.shorts[name]?.let { short -> " -$short " } ?: ""
}${if (it.value == ArgumentType.EXISTS) "" else " <${it.value.stringName}>"}"
}
.joinToString(separator = " ")}"
}
}

View File

@@ -1,159 +0,0 @@
@file:AutoWired
package de.wulkanat.cli
import com.gitlab.kordlib.common.entity.Permission
import com.gitlab.kordlib.kordx.commands.annotation.AutoWired
import com.gitlab.kordlib.kordx.commands.argument.primitive.BooleanArgument
import com.gitlab.kordlib.kordx.commands.argument.text.StringArgument
import com.gitlab.kordlib.kordx.commands.kord.argument.RoleArgument
import com.gitlab.kordlib.kordx.commands.kord.model.precondition.precondition
import com.gitlab.kordlib.kordx.commands.kord.model.respondEmbed
import com.gitlab.kordlib.kordx.commands.kord.module.module
import com.gitlab.kordlib.kordx.commands.model.command.invoke
import de.wulkanat.Admin
import de.wulkanat.CustomMessage
import de.wulkanat.ServiceChannel
import de.wulkanat.files.ServiceChannels
import java.awt.Color
// TODO: channel argument?
fun ownerCommands() = module("owner-commands") {
precondition {
message.getAuthorAsMember()?.getPermissions()?.contains(Permission.Administrator) ?: false
}
command("add") {
invoke {
if (ServiceChannels.addChannel(channel.id.longValue, null) == null) {
respond("Already added.")
} else {
respond("Added.")
Admin.info()
}
}
}
command("remove") {
invoke {
val result = ServiceChannels.channels.removeAll { it.id == channel.id.longValue }
ServiceChannels.saveChannels()
if (result) {
respond("Removed.")
} else {
respond("This channel is not registered.")
}
}
}
command("publish") {
invoke(BooleanArgument(trueValue = "on", falseValue = "off")) { doAutoPublish ->
ServiceChannels.channels.find { it.id == channel.id.longValue }?.also {
it.autoPublish = doAutoPublish
ServiceChannels.saveChannels()
} ?: respond("Channel not registered")
}
}
command("ping") {
invoke(RoleArgument) { role ->
ServiceChannels.channels.find { it.id == channel.id.longValue }?.also {
// TODO: @everyone
it.mentionedRole = role.id.longValue
ServiceChannels.saveChannels()
} ?: respond("Channel not registered")
}
}
command("setMessage") {
invoke(StringArgument) { message ->
ServiceChannels.channels.find { it.id == channel.id.longValue}?.also {
it.message = CustomMessage(message)
respond("Set `$message` as a message.")
} ?: respond("Channel not registered!")
}
}
command("resetMessage") {
invoke {
ServiceChannels.channels.find { it.id == channel.id.longValue }?.also {
it.message = null
respond("Reset to no message")
} ?: respond("Channel not registered!")
}
}
command("serviceChannel") {
invoke(BooleanArgument(trueValue = "add", falseValue = "remove")) { addChannel ->
if (addChannel) {
ServiceChannels.serviceChannels.find { it.id == channel.id.longValue }?.also {
respond("Already a service channel")
} ?: run {
ServiceChannels.serviceChannels.add(ServiceChannel(channel.id.longValue))
respond("Added as a service channel")
}
} else {
respond(if (ServiceChannels.serviceChannels.removeAll { it.id == channel.id.longValue })
"Channel removed" else "Not a service channel")
}
}
}
command("publishMessage") {
invoke(BooleanArgument(trueValue = "on", falseValue = "off")) { doAutoPublish ->
ServiceChannels.channels.find { it.id == channel.id.longValue }?.also {
it.message?.pushAnnouncement = doAutoPublish
ServiceChannels.saveChannels()
respond("Auto publish is now ${if (doAutoPublish) "on" else "off"}")
} ?: respond("Channel not registered!")
}
}
command("info") {
invoke {
respondEmbed {
title = "Server Overview"
color = Color.GREEN
description = """
${ServiceChannels.getServerNames(guild?.id?.longValue).joinToString("\n")}
**_Service Channels_**
${ServiceChannels.getServiceChannelServers(guild?.id?.longValue).joinToString("\n")}
""".trimIndent()
Admin.admin?.let {
author {
name = it.username
icon = it.avatar.url
url = "https://github.com/wulkanat/BlogShot"
}
}
}
}
}
command("report") {
invoke {
respond("What is the error you encountered?")
val errorReport = read(StringArgument)
respondEmbed {
title = "Error Report Preview"
color = Color.RED
description = errorReport
message.author?.let {
author {
name = it.username
icon = it.avatar.url
}
}
}
respond("Send? [y/n]")
if (read(BooleanArgument(trueValue = "y", falseValue = "n"))) {
respond("Sent")
Admin.error(guild?.asGuildOrNull()?.name ?: "Unknown Guild", errorReport, author)
} else {
respond("Aborting")
}
}
}
}

View File

@@ -1,8 +0,0 @@
package de.wulkanat.extensions
inline fun <T> Boolean.alsoIf(other: T, body: () -> Unit): Boolean {
if (this == other) {
body()
}
return this
}

View File

@@ -1,7 +0,0 @@
package de.wulkanat.extensions
import com.gitlab.kordlib.core.entity.User
import de.wulkanat.files.Config
val User.isBotAdmin: Boolean
get() = id.longValue == Config.adminId

View File

@@ -1,26 +0,0 @@
package de.wulkanat.files
import de.wulkanat.files.concept.SerializableObject
import kotlinx.serialization.Serializable
object Config : SerializableObject<Config.Data>("config.json", Data(), Data.serializer()) {
val adminId: Long
get() = instance.adminId
val token: String
get() = instance.token
val updateMs: Long
get() = instance.updateMs
val watchingMessage: String
get() = instance.watchingMessage
val offlineMessage: String
get() = instance.offlineMessage
@Serializable
data class Data(
val adminId: Long = 12345,
val token: String = "12345",
val updateMs: Long = 30000,
val watchingMessage: String = "for new Blogposts",
val offlineMessage: String = "CONNECTION FAILED"
)
}

View File

@@ -1,4 +0,0 @@
package de.wulkanat.files
object Servers {
}

View File

@@ -1,24 +0,0 @@
package de.wulkanat.files.concept
import de.wulkanat.extensions.ensureExists
import kotlinx.serialization.KSerializer
import kotlinx.serialization.json.Json
import java.io.File
abstract class SerializableObject<T>(
fileName: String,
defaultText: T? = null,
private val childSerializer: KSerializer<T>
) {
private val json = Json { allowStructuredMapKeys = true }
private val file = File(fileName).ensureExists(defaultText?.let { json.encodeToString(childSerializer, it) })
var instance: T = json.decodeFromString(childSerializer, file.readText())
fun refresh() {
instance = json.decodeFromString(childSerializer, file.readText())
}
fun save() {
file.writeText(json.encodeToString(childSerializer, instance))
}
}

View File

@@ -3,8 +3,6 @@ package de.wulkanat.web
import de.wulkanat.Admin
import de.wulkanat.DiscordRpc
import de.wulkanat.model.BlogPostPreview
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import java.io.IOException
@@ -13,14 +11,9 @@ object SiteWatcher {
var newestBlog: BlogPostPreview? = null
private var siteOnline = false
suspend fun hasNewBlogPost(): Boolean {
fun hasNewBlogPost(): Boolean {
try {
val doc = withContext(Dispatchers.IO) {
// solved by `withContext`
// https://stackoverflow.com/a/63332658
@Suppress("BlockingMethodInNonBlockingContext")
Jsoup.connect(BLOG_INDEX_URL).get()
}
val doc = Jsoup.connect(BLOG_INDEX_URL).get()
val newBlog = BlogPostParser.getFistBlog(doc)
if (newestBlog == newBlog) {

8
twitter.json Normal file
View File

@@ -0,0 +1,8 @@
{
"env": "dev",
"accessToken": "1075173710557011968-OfPSjYUF6IDYtOl8yeo1x1EzXVlZWD",
"accessTokenSecret": "ikcLkku0lSNMVY7kgWcj7j7tfk3IHpHUYIkvJACp0zZXh",
"apiKey": "8rm4wAHVLXYauBbRDKAVro0kw",
"apiSecretKey": "vPZgiPxwqqHZXQGMPyxyuQrg8y45t1fAlOar9DpVlPa10JRAfC",
"bearerToken": "AAAAAAAAAAAAAAAAAAAAAJNnHwEAAAAAKVRDEbr2dzzum2wswMGOyJOQHJw%3DgniWSdhaXgcey4XBHQckZSVVXtP6y83wAp0sxnSp3CwbWpXoA3"
}