10 Commits

Author SHA1 Message Date
Wieland Schöbl
736c36d069 Change CLI to use kordx.commands 2021-02-21 13:55:35 +01:00
Wieland Schöbl
69220fba32 Change CLI to use kordx.commands 2021-01-08 17:40:46 +01:00
Wieland Schöbl
a8ec59c7ca Update Gradle to 6.7.1
Use kts build script
Update dependencies
2021-01-08 16:02:32 +01:00
Wieland Schöbl
fcaa8377c1 Move connection failed message to Admin 2021-01-08 15:41:37 +01:00
Wieland Schöbl
4c4e4dc992 Update README.md 2020-10-28 16:24:26 +01:00
Wieland Schöbl
8b98d4ba3c fix connection failed message not disappearing 2020-09-25 16:04:31 +02:00
Wieland Schöbl
0877883e3c fix crash 2020-08-31 20:03:05 +02:00
Wieland Schöbl
a78c2343da Add service announcement channel 2020-08-19 14:25:54 +02:00
Wieland Schöbl
490a5dcd41 Add ability to customize message 2020-08-18 10:35:13 +02:00
Wieland Schöbl
5969a2f221 Add self-configuration feature 2020-08-18 00:39:15 +02:00
28 changed files with 882 additions and 558 deletions

9
.gitignore vendored
View File

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

View File

@@ -1,6 +1,22 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="kotlin">

6
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

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

2
.idea/misc.xml generated
View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="11" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

228
.idea/workspace.xml generated
View File

@@ -1,9 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="[1.1]">
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="no idea what that did">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build.gradle" beforeDir="false" afterPath="$PROJECT_DIR$/build.gradle" 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" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -28,52 +31,6 @@
<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="Source Sets" type="e897c970:GradleViewContributor$SourceSetsNode" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
<item name="build" type="c8890929:TasksNode$1" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
<item name="build setup" type="c8890929:TasksNode$1" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
<item name="documentation" type="c8890929:TasksNode$1" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
<item name="help" type="c8890929:TasksNode$1" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
<item name="other" type="c8890929:TasksNode$1" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Tasks" type="e4a08cd1:TasksNode" />
<item name="verification" type="c8890929:TasksNode$1" />
</path>
</expand>
<select />
</tree_state>
@@ -84,27 +41,37 @@
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Kotlin Object" />
<option value="Kotlin File" />
<option value="Class" />
<option value="Kotlin Class" />
<option value="Kotlin Object" />
<option value="Kotlin File" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="not-sure" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/build.gradle" root0="SKIP_INSPECTION" />
</component>
<component name="MacroExpansionManager">
<option name="directoryName" value="o7p0t8es" />
</component>
<component name="ProjectId" id="1g2oQiuUv1Bu6ZCW2NSVzB1V6Sc" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showExcludedFiles" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<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$/src" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/build/libs" />
<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" />
@@ -115,6 +82,7 @@
<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">
@@ -122,7 +90,7 @@
<recent name="E:\Projects\Kotlin_Proj\HytaleUpdateBot\src\main\kotlin\de\wulkanat" />
</key>
</component>
<component name="RunManager" selected="Gradle.HytaleUpdateBot [fatJar]">
<component name="RunManager" selected="Application.MainKt">
<configuration name="HytaleUpdateBot [build]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
@@ -139,7 +107,9 @@
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
<configuration name="HytaleUpdateBot [clean]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
@@ -158,7 +128,9 @@
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
<configuration name="HytaleUpdateBot [fatJar]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
@@ -177,7 +149,9 @@
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
<configuration name="HytaleUpdateBot-all-1.0-SNAPSHOT.jar" type="JarApplication" temporary="true">
@@ -200,13 +174,13 @@
<recent_temporary>
<list>
<item itemvalue="Gradle.HytaleUpdateBot [fatJar]" />
<item itemvalue="Kotlin.MainKt" />
<item itemvalue="Gradle.HytaleUpdateBot [build]" />
<item itemvalue="Gradle.HytaleUpdateBot [clean]" />
<item itemvalue="JAR Application.HytaleUpdateBot-all-1.0-SNAPSHOT.jar" />
</list>
</recent_temporary>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="project-level" UseSingleDictionary="true" transferred="true" />
<component name="SvnConfiguration">
<configuration />
</component>
@@ -232,106 +206,60 @@
<option name="project" value="LOCAL" />
<updated>1597438052596</updated>
</task>
<option name="localTasksCounter" value="3" />
<task id="LOCAL-00003" summary="[1.1]">
<created>1597438317540</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1597438317540</updated>
</task>
<task id="LOCAL-00004" summary="Add service announcement channel">
<created>1597839954908</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1597839954909</updated>
</task>
<task id="LOCAL-00005" summary="prepare twitter integration">
<created>1601042375685</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1601042375685</updated>
</task>
<option name="localTasksCounter" value="6" />
<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]" />
<option name="LAST_COMMIT_MESSAGE" value="[1.1]" />
</component>
<component name="WindowStateProjectService">
<state x="552" y="179" key="#Project_Structure" timestamp="1597434105164">
<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="1597434105164" />
<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="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="1597428556346">
<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="1597428556346" />
<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="1597438290366">
<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="1597438290366" />
<state width="1876" height="161" key="GridCell.Tab.0.bottom" timestamp="1597438298864">
<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="1876" height="161" key="GridCell.Tab.0.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1876" height="161" key="GridCell.Tab.0.center" timestamp="1597438298864">
<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="1876" height="161" key="GridCell.Tab.0.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1876" height="161" key="GridCell.Tab.0.left" timestamp="1597438298864">
<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="1876" height="161" key="GridCell.Tab.0.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1876" height="161" key="GridCell.Tab.0.right" timestamp="1597438298864">
<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="1876" height="161" key="GridCell.Tab.0.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1006" height="588" key="GridCell.Tab.1.bottom" timestamp="1597366506508">
<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="1006" height="588" key="GridCell.Tab.1.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506508" />
<state width="1006" height="588" key="GridCell.Tab.1.center" timestamp="1597366506506">
<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="1006" height="588" key="GridCell.Tab.1.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506506" />
<state width="1006" height="588" key="GridCell.Tab.1.left" timestamp="1597366506505">
<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="1006" height="588" key="GridCell.Tab.1.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506505" />
<state width="1006" height="588" key="GridCell.Tab.1.right" timestamp="1597366506507">
<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="1006" height="588" key="GridCell.Tab.1.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506507" />
<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="1597438121430">
<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="1597438121430" />
<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="1597363843473">
<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="1597363843473" />
<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" />
</component>
<component name="XDebuggerManager">
<breakpoint-manager>
<breakpoints>
<line-breakpoint enabled="true" type="kotlin-line">
<url>file://$PROJECT_DIR$/src/main/kotlin/de/wulkanat/AdminCli.kt</url>
<line>22</line>
<option name="timeStamp" value="1" />
</line-breakpoint>
</breakpoints>
</breakpoint-manager>
<watches-manager>
<configuration name="JetRunConfigurationType">
<watch expression="((org.jsoup.nodes.Element.NodeList)((Document)doc).childNodes).get(2)" custom="org.jsoup.nodes.Element.NodeList,org.jsoup.nodes.Document" />

122
README.md
View File

@@ -1,82 +1,72 @@
# BlogShot
A bot that automatically polls the newest blogpost from [Hytale News Tab](https://www.hytale.com/news) and posts a message into servers if there is a new one.
## Setup
Okay, this isn't really meant for you to setup, if you want it though it first is easier to just dm me on Twitter [@tale_talk](https://twitter.com/tale_talk) so I can add you to the server list.
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
}
```
* add your servers to `servers.json`
```json
[
{
"id": 15050067772322222,
"mentionedRole": "everyone",
"autoPublish":true
},
{
"id": 74050067772325222,
"mentionedRole": null,
"autoPublish":false
},
{
"id": 74050067772325222,
"mentionedRole": "74036067771625222",
"autoPublish":false
}
]
```
* 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.
## Add to your server
Click [this](https://discord.com/api/oauth2/authorize?client_id=743447329901641799&permissions=150528&scope=bot) link to invite
the bot to your server. Please note that only people with *Administrator* permission will be able to
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 &#124; off | [Community&#124;Partner&#124;Verified only] Auto publish the message if in an announcement channel |
| %!ping | none &#124; everyone &#124; roleName | What role to ping |
| %!setMessage | message | Set a custom message when a blogpost arrives |
| %!resetMessage | | Reset the custom message to none |
| %!serviceChannel | add &#124; remove | Add/remove channel from service notification list |
| %!publishMessage | on &#124; off | [Community&#124;Partner&#124;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.
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 `config.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`,
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
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.
## 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) |
| !removeInactive | | Remove inactive channels |
| !help | | Show a help dialog with all these commands |
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 `config.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
Thanks to [Forcellrus](https://github.com/Forcellrus/Discord-Auto-Publisher) for discovering a way to auto publish messages
in news channels
in news channels

View File

@@ -1,47 +0,0 @@
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.1'
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'

61
build.gradle.kts Normal file
View File

@@ -0,0 +1,61 @@
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,6 +1,5 @@
#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
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

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

2
settings.gradle.kts Normal file
View File

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

View File

@@ -1,142 +1,82 @@
package de.wulkanat
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 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 java.awt.Color
object Admin {
val userId: Long
val token: String
val updateMs: Long
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()
}
init {
val admin = Json(JsonConfiguration.Stable).parse(AdminFile.serializer(), ADMIN_FILE.readText())
userId = admin.adminId
token = admin.token
updateMs = admin.updateMs
}
var jda: JDA? = null
var jda: Kord? = null
set(value) {
field = value
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.")
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.")
}
}
}
private var admin: User? = null
var admin: User? = null
fun println(msg: String) {
sendDevMessage(
EmbedBuilder()
.setTitle(msg)
.setColor(Color.WHITE)
.build(),
msg
)
suspend fun println(msg: String) {
sendDevMessage(msg) {
title = msg
color = Color.WHITE
}
}
fun printlnBlocking(msg: String) {
senDevMessageBlocking(
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 error(msg: String, error: String) {
sendDevMessage(
EmbedBuilder()
.setTitle(msg)
.setDescription(error)
.setColor(Color.RED)
.build()
, "$msg\n\n${error}"
)
suspend fun warning(msg: String) {
sendDevMessage(msg) {
title = msg
color = Color.YELLOW
}
}
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"))
.setColor(Color.GREEN)
.build(),
"Now watching for new Hytale BlogPosts"
)
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 silent(msg: String) {
kotlin.io.println(msg)
}
private fun senDevMessageBlocking(messageEmbed: MessageEmbed, fallback: String) {
admin = jda!!.retrieveUserById(userId).complete()
val devChannel = admin?.openPrivateChannel() ?: kotlin.run {
private suspend inline fun sendDevMessage(fallback: String, crossinline embed: EmbedBuilder.() -> Unit) {
val devChannel = admin?.getDmChannel() ?: kotlin.run {
kotlin.io.println(fallback)
return
}
devChannel.complete()
.sendMessage(messageEmbed).complete()
}
private fun sendDevMessage(messageEmbed: MessageEmbed, fallback: String) {
val devChannel = admin?.openPrivateChannel() ?: kotlin.run {
kotlin.io.println(fallback)
return
}
devChannel.queue {
it.sendMessage(messageEmbed).queue()
}
devChannel.createEmbed(embed)
}
}

View File

@@ -1,106 +0,0 @@
package de.wulkanat
import de.wulkanat.extensions.crosspost
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
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
object Channels {
var jda: JDA? = null
val json = Json(JsonConfiguration.Stable)
/**
* List of (ServerID, ChannelID)
*/
var channels: MutableList<DiscordChannel> = refreshFromDisk()
fun sentToAll(messageEmbed: MessageEmbed) {
if (jda == null)
return
for (channel_pair in channels) {
val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue
if (channel_pair.mentionedRole != null) {
val message = if (channel_pair.mentionedRole == "everyone") {
"New Blogpost @everyone"
} else {
"New Blogpost <@&${channel_pair.mentionedRole}>"
}
channel.sendMessage(message).queue()
}
channel.sendMessage(messageEmbed).queue {
if (channel_pair.autoPublish) {
it.crosspost().queue()
}
}
}
}
fun checkEveryonePermission() {
for (channel_pair in channels) {
val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue
if (channel_pair.mentionedRole == "everyone" &&
channel.guild.selfMember.hasPermission(Permission.MESSAGE_MENTION_EVERYONE)
) {
Admin.warning("Cannot mention everyone on ${channel.guild.name}")
} else if (channel.guild.selfMember.hasPermission(Permission.MESSAGE_WRITE)) {
Admin.warning("Cannot send any messages on ${channel.guild.name}")
}
}
}
fun refreshFromDisk(): MutableList<DiscordChannel> {
return json.parse(
DiscordChannel.serializer().list, (if (Admin.testModeEnabled) {
TEST_FILE
} else {
SERVERS_FILE
}).readText()
).toMutableList()
}
fun getServerNames(): List<String> {
if (jda == null)
return listOf()
return channels.map {
val channel = jda!!.getTextChannelById(it.id)
if (channel == null) {
Admin.warning("Channel ${it.id} is no longer active!")
return@map "**${it.id}** *(inactive)*"
}
val role = when (it.mentionedRole) {
null -> ""
"everyone" -> " @everyone"
else -> " @${channel.guild.getRoleById(it.mentionedRole)?.name}"
}
"**${channel.guild.name}**\n#${channel.name}${role}"
}
}
fun testServerId(id: Long): TextChannel? {
return jda?.getTextChannelById(id)
}
fun addChannel(id: Long, role: String?) {
channels.add(DiscordChannel(id, role))
saveChannels()
}
private fun saveChannels() {
SERVERS_FILE.writeText(
json.stringify(
DiscordChannel.serializer().list,
channels
)
)
}
}

View File

@@ -1,50 +0,0 @@
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
class Cli : ListenerAdapter() {
override fun onPrivateMessageReceived(event: PrivateMessageReceivedEvent) {
val msg = event.message.contentRaw
if (event.author.idLong != Admin.userId ||
!msg.startsWith("!")
) {
return
}
val command = msg.removePrefix("!").split(Regex("\\s+"))
when (command[0]) {
"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()
}
"refreshList" -> {
Channels.channels = Channels.refreshFromDisk()
Admin.info()
}
"testMode" -> {
Admin.testModeEnabled = true
}
"productionMode" -> {
Admin.testModeEnabled = false
}
}
}
}

View File

@@ -1,22 +1,43 @@
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
data class DiscordChannel(
val id: Long,
val mentionedRole: String? = null,
val autoPublish: Boolean = false
var mentionedRole: Long? = null,
var autoPublish: Boolean = false,
var message: CustomMessage? = null
)
@Serializable
data class ServiceChannel(
val id: Long
)
@Serializable
data class CustomMessage(
var message: String,
var pushAnnouncement: Boolean = false
)
@Serializable
data class AdminFile(
val adminId: Long,
val token: String,
val updateMs: Long
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()))

View File

@@ -0,0 +1,17 @@
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
// noop
if (available) Admin.println("Back online") else Admin.error("Gone offline", "Can't reach Hytale server")
}
}

View File

@@ -1,5 +1,6 @@
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
@@ -10,15 +11,17 @@ fun main() {
val builder = JDABuilder.createLight(
Admin.token,
GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
.setActivity(Activity.watching("for new Blogposts"))
.setActivity(Activity.watching(Admin.message))
.build()
builder.addEventListener(Cli())
builder.addEventListener(AdminCli())
builder.addEventListener(ErrorHandler())
builder.addEventListener(OwnerCli())
builder.awaitReady()
Channels.jda = builder
ServiceChannels.client = builder
Admin.jda = builder
DiscordRpc.jda = builder
Admin.info()
Runtime.getRuntime().addShutdownHook(object : Thread() {
@@ -32,7 +35,7 @@ fun main() {
timer("Updater", daemon = true, initialDelay = 0L, period = Admin.updateMs) {
if (SiteWatcher.hasNewBlogPost()) {
Channels.sentToAll(SiteWatcher.newestBlog!!.toMessageEmbed())
ServiceChannels.sentToAll(SiteWatcher.newestBlog!!.toMessageEmbed())
}
}
}

View File

@@ -0,0 +1,109 @@
@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,159 @@
@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

@@ -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
}

View File

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

View File

@@ -0,0 +1,7 @@
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

@@ -0,0 +1,26 @@
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

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

View File

@@ -0,0 +1,182 @@
package de.wulkanat.files
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 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 java.awt.Color
object ServiceChannels {
var client: Kord? = null
/**
* List of (ServerID, ChannelID)
*/
var channels: MutableList<DiscordChannel> = refreshChannelsFromDisk()
var serviceChannels: MutableList<ServiceChannel> = refreshServiceChannelsFromDisk()
fun sentToAll(messageEmbed: Embed) {
if (client == null)
return
for (channel_pair in channels) {
try {
val channel = client!!.getTextChannelById(channel_pair.id) ?: continue
val customMessage = channel_pair.message?.message ?: ""
if (channel_pair.mentionedRole != null) {
val message = if (channel_pair.mentionedRole == "everyone") {
"@everyone $customMessage"
} else {
"<@&${channel_pair.mentionedRole}> $customMessage"
}
channel.sendMessage(message).queue {
if (channel_pair.message?.pushAnnouncement == true) {
it.crosspost().queue()
}
}
} else if (channel_pair.message != null) {
channel.sendMessage(customMessage).queue {
if (channel_pair.message?.pushAnnouncement == true) {
it.crosspost().queue()
}
}
}
channel.sendMessage(messageEmbed).queue {
if (channel_pair.autoPublish) {
it.crosspost().queue()
}
}
} catch (e: Exception) {
Admin.error("Error in server ${channel_pair.id}", e.message ?: e.localizedMessage)
}
}
}
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 = client!!.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 = client!!.getTextChannelById(channel_pair.id) ?: continue
if (channel_pair.mentionedRole == "everyone" &&
channel.guild.selfMember.hasPermission(Permission.MESSAGE_MENTION_EVERYONE)
) {
Admin.warning("Cannot mention everyone on ${channel.guild.name}")
} else if (channel.guild.selfMember.hasPermission(Permission.MESSAGE_WRITE)) {
Admin.warning("Cannot send any messages on ${channel.guild.name}")
}
}
}
fun refreshChannelsFromDisk(): MutableList<DiscordChannel> {
return json.parse(
DiscordChannel.serializer().list, (SERVERS_FILE).readText()
).toMutableList()
}
fun refreshServiceChannelsFromDisk(): MutableList<ServiceChannel> {
return json.parse(
ServiceChannel.serializer().list, (SERVICE_CHANNELS_FILE).readText()
).toMutableList()
}
fun getServerNames(server: Long? = null): List<String> {
if (client == null)
return listOf()
return channels.filter { server == null || (client!!.getTextChannelById(it.id)?.guild?.idLong == server) }.map {
val channel = client!!.getTextChannelById(it.id)
if (channel == null) {
Admin.warning("Channel ${it.id} is no longer active!")
return@map "**${it.id}** *(inactive)*"
}
val role = when (it.mentionedRole) {
null -> ""
"everyone" -> " @everyone"
else -> " @${channel.guild.getRoleById(it.mentionedRole ?: "")?.name}"
}
val publish = if (it.autoPublish) " (publish)" else ""
"**${channel.guild.name}** #${channel.name}${role}${publish}${if (it.message == null) {
""
} else {
"\n*${it.message!!.message}*${if (it.message!!.pushAnnouncement) " (publish)" else ""}"
}
}"
}
}
fun getServiceChannelServers(server: Long? = null): List<String> {
if (client == null)
return listOf()
return serviceChannels.filter { server == null || (client!!.getTextChannelById(it.id)?.guild?.idLong == server) }.map {
val channel = client!!.getTextChannelById(it.id)
"**${channel?.guild?.name ?: it.id}** #${channel?.name ?: "(inactive)"}"
}
}
fun testServerId(id: Long): TextChannel? {
return client?.getTextChannelById(id)
}
fun addChannel(id: Long, role: String?): DiscordChannel? {
if (channels.find { it.id == id } != null) {
return null
}
val out = DiscordChannel(id, role)
channels.add(out)
saveChannels()
return out
}
fun saveChannels() {
SERVERS_FILE.writeText(
json.stringify(
DiscordChannel.serializer().list,
channels
)
)
SERVICE_CHANNELS_FILE.writeText(
json.stringify(
ServiceChannel.serializer().list,
serviceChannels
)
)
}
}

View File

@@ -0,0 +1,24 @@
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

@@ -1,19 +1,26 @@
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
object SiteWatcher {
private const val BLOG_INDEX_URL = "https://www.hytale.com/news"
var newestBlog: BlogPostPreview? = null
private var siteOnline = false
fun hasNewBlogPost(): Boolean {
Admin.silent("Updating...")
suspend fun hasNewBlogPost(): Boolean {
try {
val doc = Jsoup.connect(BLOG_INDEX_URL).get()
val doc = withContext(Dispatchers.IO) {
// solved by `withContext`
// https://stackoverflow.com/a/63332658
@Suppress("BlockingMethodInNonBlockingContext")
Jsoup.connect(BLOG_INDEX_URL).get()
}
val newBlog = BlogPostParser.getFistBlog(doc)
if (newestBlog == newBlog) {
@@ -28,10 +35,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
}
}