26 Commits

Author SHA1 Message Date
Wieland Schöbl
c508b6e854 Fix data upgrader 2021-06-02 21:40:13 +02:00
UnrealValentin
cfad5c0611 added multiple alert types:
new aler types:
   - twitter
   - job
2021-06-02 20:42:12 +02:00
Wieland Schöbl
126e630a80 Change Webhook 2021-05-29 22:46:33 +02:00
Waterdev
9f5a9734bc Create build.gradle 2021-05-29 14:02:19 +02:00
Waterdev
310dcfecd5 Update build.gradle 2021-05-29 14:01:40 +02:00
Waterdev
27a1e5aa7f Create gradle.yml 2021-05-29 13:41:57 +02:00
Waterdev
862df48e5f Update build.gradle 2021-05-29 13:18:01 +02:00
Wieland Schöbl
3d06de11dd Add file tests 2021-05-29 13:05:14 +02:00
Wieland Schöbl
163b201828 Add more tests 2021-05-29 12:36:41 +02:00
Wieland Schöbl
68ed19db7a Refactoring, Tests 2021-05-29 12:16:23 +02:00
Wieland Schöbl
cc7dc8d9e4 Changes by Valentin 2021-05-28 22:41:17 +02:00
Wieland Schöbl
d3d7b234a7 Bug fixes 2021-05-28 22:41:09 +02:00
Wieland Schöbl
712ff091af Update .gitignore 2021-05-28 22:29:14 +02:00
Wieland Schöbl
8e323a8446 Add more extensive fake update capabilities 2021-05-28 22:28:06 +02:00
Wieland Schöbl
ccb074d81d Changes by Valentin 2021-05-28 22:10:13 +02:00
Wieland Schöbl
91e6552772 Changes by Valentin 2021-05-28 22:10:00 +02:00
Wieland Schöbl
f23a4d9ce5 Add Twitter integration
Add Job Listening Integration
Various Refactorings
2021-05-28 22:09:32 +02:00
Wieland Schöbl
fa00466eb0 Add shards count configuration 2021-05-28 20:26:40 +02:00
Wieland Schöbl
0c123a6567 Update stuff 2021-05-28 20:03:12 +02:00
UnrealValentin
7b29417678 twitter gods dont cancel me pls 2021-05-28 20:01:13 +02:00
Wieland Schöbl
77f5733144 Update formatting 2021-05-28 19:08:55 +02:00
Wieland Schöbl
7f207674ba Update Json Serialization 2021-05-28 19:07:47 +02:00
UnrealValentin
f0f6015d6f sussy 2021-05-28 18:43:30 +02:00
Wieland Schöbl
288e56f035 Update dependencies 2021-05-28 18:08:40 +02: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
49 changed files with 1896 additions and 846 deletions

29
.github/workflows/gradle.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
# This workflow will build a Java project with Gradle
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
name: Java CI with Gradle
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Test with Gradle
run: ./gradlew test
- name: Build with Gradle
run: ./gradlew build

3
.gitignore vendored
View File

@@ -1,6 +1,9 @@
servers.json
admin.json
service_channels.json
webhooks.json
blog_state.json
jobs_state.json
*.hprof
/build
/.gradle

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
HytaleUpdateBot

View File

@@ -1,22 +1,6 @@
<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">

View File

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

View File

@@ -21,5 +21,10 @@
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://m2.dv8tion.net/releases" />
</remote-repository>
</component>
</project>

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="libraries-with-intellij-classes">
<option name="intellijApiContainingLibraries">
<list>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIU" />
<option name="groupId" value="com.jetbrains.intellij.idea" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIU" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIC" />
<option name="groupId" value="com.jetbrains.intellij.idea" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="ideaIC" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPY" />
<option name="groupId" value="com.jetbrains.intellij.pycharm" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPY" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPC" />
<option name="groupId" value="com.jetbrains.intellij.pycharm" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="pycharmPC" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="clion" />
<option name="groupId" value="com.jetbrains.intellij.clion" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="clion" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="riderRD" />
<option name="groupId" value="com.jetbrains.intellij.rider" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="riderRD" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="goland" />
<option name="groupId" value="com.jetbrains.intellij.goland" />
</LibraryCoordinatesState>
<LibraryCoordinatesState>
<option name="artifactId" value="goland" />
<option name="groupId" value="com.jetbrains" />
</LibraryCoordinatesState>
</list>
</option>
</component>
</project>

124
.idea/uiDesigner.xml generated Normal file
View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

550
.idea/workspace.xml generated
View File

@@ -4,12 +4,10 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="fix crash on missing permission&#10;add removeInactive command">
<change afterPath="$PROJECT_DIR$/.idea/compiler.xml" afterDir="false" />
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="Add file tests">
<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/web/SiteWatcher.kt" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/kotlin/de/wulkanat/web/SiteWatcher.kt" afterDir="false" />
</list>
<list id="44283a45-f406-407f-bce2-a31bb9bfc0cc" name="Changes by Valentin" comment="Changes by Valentin" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@@ -33,6 +31,57 @@
<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>
<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>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Dependencies" type="6de06a37:ExternalSystemViewDefaultContributor$MyDependenciesNode" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Run Configurations" type="7b0102dc:RunConfigurationsNode" />
</path>
</expand>
<select />
</tree_state>
@@ -43,23 +92,43 @@
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Class" />
<option value="Kotlin Class" />
<option value="Kotlin Object" />
<option value="Kotlin File" />
<option value="Enum" />
<option value="Class" />
<option value="Kotlin Class" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="add-twitter-integration" />
<entry key="$PROJECT_DIR$" value="not-sure" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitSEFilterConfiguration">
<file-type-list>
<filtered-out-file-type name="LOCAL_BRANCH" />
<filtered-out-file-type name="REMOTE_BRANCH" />
<filtered-out-file-type name="TAG" />
<filtered-out-file-type name="COMMIT_BY_MESSAGE" />
</file-type-list>
</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/build.gradle" root0="SKIP_INSPECTION" />
<setting file="file://$PROJECT_DIR$/src/main/kotlin/org/hmcore/extensions/Embed.kt" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/main/kotlin/org/hmcore/extensions/File.kt" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/main/kotlin/org/hmcore/extensions/Jsoup.kt" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/main/kotlin/org/hmcore/model/JobListingPreview.kt" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/main/kotlin/org/hmcore/model/BlogPostPreview.kt" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/test/kotlin/org/hmcore/extensions/ColorTest.kt" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/main/kotlin/org/hmcore/extensions/Color.kt" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/test/kotlin/org/hmcore/extensions/EmbedTest.kt" root0="FORCE_HIGHLIGHTING" />
</component>
<component name="MacroExpansionManager">
<option name="directoryName" value="o7p0t8es" />
</component>
<component name="ProjectId" id="1g2oQiuUv1Bu6ZCW2NSVzB1V6Sc" />
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
@@ -68,112 +137,180 @@
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">
<property name="Downloaded.Files.Path.Enabled" value="false" />
<property name="Repository.Attach.Annotations" value="false" />
<property name="Repository.Attach.JavaDocs" value="false" />
<property name="Repository.Attach.Sources" value="false" />
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
<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="project.structure.last.edited" value="Modules" />
<property name="WebServerToolWindowFactoryState" value="false" />
<property name="aspect.path.notification.shown" value="true" />
<property name="codeWithMe.voiceChat.enabledByDefault" value="false" />
<property name="last_opened_file_path" value="$PROJECT_DIR$/src/test" />
<property name="node.js.detected.package.eslint" value="true" />
<property name="node.js.detected.package.tslint" value="true" />
<property name="node.js.selected.package.eslint" value="(autodetect)" />
<property name="node.js.selected.package.tslint" value="(autodetect)" />
<property name="project.structure.last.edited" value="Project" />
<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" />
<property name="settings.editor.selected.configurable" value="preferences.pluginManager" />
</component>
<component name="RecentsManager">
<key name="MoveKotlinTopLevelDeclarationsDialog.RECENTS_KEY">
<recent name="de.wulkanat" />
<key name="CreateClassDialog.RecentsKey">
<recent name="org.hmcore" />
</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:\Git\Orbot\src\main\kotlin\org\hmcore" />
<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="org.hmcore.model" />
<recent name="de.wulkanat" />
</key>
<key name="MoveClassesOrPackagesDialog.RECENTS_KEY">
<recent name="org" />
</key>
</component>
<component name="RunManager" selected="Gradle.HytaleUpdateBot [fatJar]">
<configuration name="HytaleUpdateBot [build]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="build" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
<configuration name="HytaleUpdateBot [clean]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="clean" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
<configuration name="HytaleUpdateBot [fatJar]" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value="fatJar" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
<method v="2" />
</configuration>
<configuration name="HytaleUpdateBot-all-1.0-SNAPSHOT.jar" type="JarApplication" temporary="true">
<option name="JAR_PATH" value="$PROJECT_DIR$/build/libs/HytaleUpdateBot-all-1.0-SNAPSHOT.jar" />
<method v="2" />
</configuration>
<configuration name="MainKt" type="JetRunConfigurationType" temporary="true" nameIsGenerated="true">
<module name="HytaleUpdateBot.main" />
<option name="VM_PARAMETERS" />
<option name="PROGRAM_PARAMETERS" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<option name="MAIN_CLASS_NAME" value="de.wulkanat.MainKt" />
<option name="WORKING_DIRECTORY" />
<component name="RunManager" selected="Kotlin.Main">
<configuration default="true" type="ArquillianJUnit" factoryName="" nameIsGenerated="true">
<option name="arquillianRunConfiguration">
<value>
<option name="containerStateName" value="" />
</value>
</option>
<option name="TEST_OBJECT" value="class" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="BlogPostPreviewTest" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--tests &quot;org.hmcore.model.BlogPostPreviewTest&quot;" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":test" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
<configuration name="DiscordWebhookEmbed.Webhook should not throw and return false if supplied invalid URL" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--tests &quot;org.hmcore.webhook.DiscordWebhookEmbed.Webhook should not throw and return false if supplied invalid URL&quot;" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":test" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
<configuration name="EnumTest.Enum serialization" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--tests &quot;org.hmcore.serialization.EnumTest.Enum serialization&quot;" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":test" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
<configuration name="FileTest.Ensure exists should create a file if supplied with null but not write text" type="GradleRunConfiguration" factoryName="Gradle" temporary="true">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value="--tests &quot;org.hmcore.extensions.FileTest.Ensure exists should create a file if supplied with null but not write text&quot;" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list>
<option value=":test" />
</list>
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
<configuration name="All Tests" type="JUnit" factoryName="JUnit">
<module name="HytaleUpdateBot.test" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="directory" />
<dir value="$PROJECT_DIR$/src/test" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="Main" type="JetRunConfigurationType" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="org.hmcore.Main" />
<module name="HytaleUpdateBot.main" />
<option name="PROGRAM_PARAMETERS" value="serverDataConvert1" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<list>
<item itemvalue="Gradle.FileTest.Ensure exists should create a file if supplied with null but not write text" />
<item itemvalue="Gradle.DiscordWebhookEmbed.Webhook should not throw and return false if supplied invalid URL" />
<item itemvalue="Gradle.EnumTest.Enum serialization" />
<item itemvalue="Gradle.BlogPostPreviewTest" />
<item itemvalue="JUnit.All Tests" />
<item itemvalue="Kotlin.Main" />
</list>
<recent_temporary>
<list>
<item itemvalue="Kotlin.MainKt" />
<item itemvalue="Gradle.HytaleUpdateBot [fatJar]" />
<item itemvalue="Gradle.HytaleUpdateBot [build]" />
<item itemvalue="Gradle.HytaleUpdateBot [clean]" />
<item itemvalue="JAR Application.HytaleUpdateBot-all-1.0-SNAPSHOT.jar" />
<item itemvalue="Kotlin.Main" />
<item itemvalue="Gradle.BlogPostPreviewTest" />
<item itemvalue="Gradle.EnumTest.Enum serialization" />
<item itemvalue="Gradle.DiscordWebhookEmbed.Webhook should not throw and return false if supplied invalid URL" />
<item itemvalue="Gradle.FileTest.Ensure exists should create a file if supplied with null but not write text" />
</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>
@@ -184,6 +321,7 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1597322033373</updated>
<workItem from="1622225780094" duration="9225000" />
</task>
<task id="LOCAL-00001" summary="Add auto publish feature">
<created>1597437833375</created>
@@ -220,139 +358,137 @@
<option name="project" value="LOCAL" />
<updated>1601042375685</updated>
</task>
<option name="localTasksCounter" value="6" />
<task id="LOCAL-00006" summary="Update Json Serialization">
<created>1622221667210</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1622221667210</updated>
</task>
<task id="LOCAL-00007" summary="Update stuff">
<created>1622224992757</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1622224992757</updated>
</task>
<task id="LOCAL-00008" summary="Add shards count configuration">
<created>1622226400158</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1622226400158</updated>
</task>
<task id="LOCAL-00009" summary="Add Twitter integration&#10;Add Job Listening Integration&#10;Various Refactorings">
<created>1622232573135</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1622232573135</updated>
</task>
<task id="LOCAL-00010" summary="Changes by Valentin">
<created>1622232601071</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1622232601071</updated>
</task>
<task id="LOCAL-00011" summary="Changes by Valentin">
<created>1622232613740</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1622232613740</updated>
</task>
<task id="LOCAL-00012" summary="Add more extensive fake update capabilities">
<created>1622233686973</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1622233686973</updated>
</task>
<task id="LOCAL-00013" summary="Update .gitignore">
<created>1622233754088</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1622233754088</updated>
</task>
<task id="LOCAL-00014" summary="Bug fixes">
<created>1622234469645</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1622234469645</updated>
</task>
<task id="LOCAL-00015" summary="Changes by Valentin">
<created>1622234477364</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1622234477364</updated>
</task>
<task id="LOCAL-00016" summary="Refactoring, Tests">
<created>1622283383268</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1622283383268</updated>
</task>
<task id="LOCAL-00017" summary="Add more tests">
<created>1622284601372</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1622284601372</updated>
</task>
<option name="localTasksCounter" value="18" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</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" />
</component>
<component name="WindowStateProjectService">
<state x="552" y="179" key="#Project_Structure" timestamp="1597687666334">
<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="1597687666334" />
<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="1598896749015">
<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="1598896749015" />
<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="1876" height="161" key="GridCell.Tab.0.bottom" timestamp="1598896802046">
<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="1598896802046" />
<state width="1876" height="161" key="GridCell.Tab.0.center" timestamp="1598896802046">
<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="1598896802046" />
<state width="1876" height="161" key="GridCell.Tab.0.left" timestamp="1598896802046">
<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="1598896802046" />
<state width="1876" height="161" key="GridCell.Tab.0.right" timestamp="1598896802046">
<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="1598896802046" />
<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="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="1597839957675">
<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="1597839957675" />
<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" />
<MESSAGE value="Update Json Serialization" />
<MESSAGE value="Update stuff" />
<MESSAGE value="Add shards count configuration" />
<MESSAGE value="Add Twitter integration&#10;Add Job Listening Integration&#10;Various Refactorings" />
<MESSAGE value="Add more extensive fake update capabilities" />
<MESSAGE value="Update .gitignore" />
<MESSAGE value="Bug fixes" />
<MESSAGE value="Changes by Valentin" />
<MESSAGE value="Refactoring, Tests" />
<MESSAGE value="Add more tests" />
<option name="LAST_COMMIT_MESSAGE" value="Add more tests" />
</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" />
</configuration>
</watches-manager>
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/HytaleUpdateBot$All_Tests.ic" NAME="All Tests Coverage Results" MODIFIED="1622286395486" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="idea" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" />
</component>
<component name="se.expertsystem.intellij.aop.aspectj.AspectJProjectComponent">
<setting name="isActivated" value="false" />
</component>
</project>

View File

@@ -37,9 +37,11 @@ Add your Discord ID `adminId` (not name), Bot token `token`, and update frequenc
optionally you can add your own messages for when the bot is looking and when it can't reach Hytale Servers.
If you verified that everything works correctly, you can start the server in the background, on Linux that is
`nohup java -jar [server-file-name]`. To stop it you can either type `!stop` in the Admin Console (Discord PM) or
`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.
@@ -67,4 +69,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

View File

@@ -1,22 +1,31 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '1.3.61'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.3.61'
id 'org.jetbrains.kotlin.jvm' version '1.5.10'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.10'
}
group 'de.wulkanat'
version '1.4.2'
group 'org.wulkanat'
version '2.0.0'
repositories {
mavenCentral()
jcenter()
maven {
url 'https://m2.dv8tion.net/releases'
}
}
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"
implementation 'net.dv8tion:JDA:4.2.1_253'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1'
implementation 'com.github.redouane59.twitter:twittered:1.20'
implementation group: 'org.quartz-scheduler', name: 'quartz', version: '2.3.2'
testImplementation 'io.mockk:mockk:1.11.0'
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.10'
}
compileKotlin {
@@ -28,7 +37,7 @@ compileTestKotlin {
jar {
manifest {
attributes('Main-Class': 'de.wulkanat.MainKt')
attributes('Main-Class': 'org.wulkanat.MainKt')
}
}
@@ -39,7 +48,7 @@ task fatJar(type: Jar) {
})
with jar
manifest {
attributes 'Main-Class': 'de.wulkanat.MainKt',
attributes 'Main-Class': 'org.wulkanat.MainKt',
'Implementation-Version': version
}
}

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.8-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,27 +0,0 @@
import net.dv8tion.jda.internal.requests.Method;
import net.dv8tion.jda.internal.requests.Route;
import java.lang.reflect.Constructor;
public class Inaccessibles {
/**
* This is private by default
*
* @param method look
* @param route somewhere
* @return else
*/
public static Route getRoute(Method method, String route) {
try {
Constructor<?> constructor = Route.class.getDeclaredConstructor(Method.class, String.class);
constructor.setAccessible(true);
return (Route) constructor.newInstance(method, route);
} catch (Exception e) {
return null;
}
}
public static String toUnsignedString(long num) {
return Long.toUnsignedString(num);
}
}

View File

@@ -0,0 +1,15 @@
package org.hmcore;
public enum MessageType {
INVALID (-1),
BLOGPOST(0),
TWITTER(1),
JOB_LISTING(2),
WEBSITE_CHANGED(3);
MessageType(int i) {
}
int i;
}

View File

@@ -0,0 +1,40 @@
package org.hmcore;
import com.github.redouane59.twitter.TwitterClient;
import com.github.redouane59.twitter.dto.tweet.Tweet;
import com.github.redouane59.twitter.signature.TwitterCredentials;
import net.dv8tion.jda.api.MessageBuilder;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import java.util.Objects;
public class TwitterJob implements Job {
public static TwitterClient twitterClient = new TwitterClient(TwitterCredentials.builder()
.accessToken(Objects.requireNonNull(Admin.adFile.getTwitterApi()).getAccessToken())
.accessTokenSecret(Objects.requireNonNull(Admin.adFile.getTwitterApi()).getAccessTokenSecret())
.apiKey(Objects.requireNonNull(Admin.adFile.getTwitterApi()).getApiKey())
.apiSecretKey(Objects.requireNonNull(Admin.adFile.getTwitterApi()).getApiKeySecret())
.build());
public static String hytaleTwitterID = twitterClient.getUserFromUserName("Hytale").getId();
public static String lastTweetID = twitterClient.getUserTimeline(hytaleTwitterID, 20).get(0).getId();
@Override
public void execute(JobExecutionContext context) {
try {
Tweet tweet = twitterClient.getUserTimeline(hytaleTwitterID, 20).get(0);
String tweetID = tweet.getId();
if (!lastTweetID.equalsIgnoreCase(tweetID)) {
lastTweetID = tweetID;
Channels.INSTANCE.sentToAll(new MessageBuilder().append("https://twitter.com/Hytale/status/").append(tweetID).build(), MessageType.TWITTER);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -1,141 +0,0 @@
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 java.awt.Color
object Admin {
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
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
fun println(msg: String) {
sendDevMessage(
EmbedBuilder()
.setTitle(msg)
.setColor(Color.WHITE)
.build(),
msg
)
}
fun printlnBlocking(msg: String) {
senDevMessageBlocking(
EmbedBuilder()
.setTitle(msg)
.setColor(Color.WHITE)
.build(),
msg
)
}
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}"
)
}
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 fun senDevMessageBlocking(messageEmbed: MessageEmbed, fallback: String) {
admin = jda!!.retrieveUserById(userId).complete()
val devChannel = admin?.openPrivateChannel() ?: kotlin.run {
kotlin.io.println(fallback)
return
}
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

@@ -1,177 +0,0 @@
package de.wulkanat
import de.wulkanat.extensions.crosspost
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 Channels {
var jda: JDA? = null
/**
* List of (ServerID, ChannelID)
*/
var channels: MutableList<DiscordChannel> = refreshChannelsFromDisk()
var serviceChannels: MutableList<ServiceChannel> = refreshServiceChannelsFromDisk()
fun sentToAll(messageEmbed: MessageEmbed) {
if (jda == null)
return
for (channel_pair in channels) {
try {
val channel = jda!!.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 = jda!!.getTextChannelById(channelInfo.id)
channel?.sendMessage(serviceMessage)?.queue()
}
Admin.println("Service message distributed to ${serviceChannels.size} channels.")
Admin.sendDevMessage(serviceMessage, """
***************
SERVICE MESSAGE
$title
-------
$message
***************
""".trimIndent())
}
fun checkEveryonePermission() {
for (channel_pair in channels) {
val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue
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 (jda == null)
return listOf()
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)*"
}
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 (jda == null)
return listOf()
return serviceChannels.filter { server == null || (jda!!.getTextChannelById(it.id)?.guild?.idLong == server) }.map {
val channel = jda!!.getTextChannelById(it.id)
"**${channel?.guild?.name ?: it.id}** #${channel?.name ?: "(inactive)"}"
}
}
fun testServerId(id: Long): TextChannel? {
return jda?.getTextChannelById(id)
}
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

@@ -1,43 +0,0 @@
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,
var mentionedRole: String? = 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 = 12345,
val token: String = "12345",
val updateMs: Long = 30000,
val watchingMessage: String = "for new Blogposts",
val offlineMessage: String = "CONNECTION FAILED"
)
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

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

View File

@@ -1,40 +0,0 @@
package de.wulkanat
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.requests.GatewayIntent
import de.wulkanat.web.SiteWatcher
import kotlin.concurrent.timer
fun main() {
val builder = JDABuilder.createLight(
Admin.token,
GatewayIntent.GUILD_MESSAGES, GatewayIntent.DIRECT_MESSAGES)
.setActivity(Activity.watching(Admin.message))
.build()
builder.addEventListener(AdminCli())
builder.addEventListener(ErrorHandler())
builder.addEventListener(OwnerCli())
builder.awaitReady()
Channels.jda = builder
Admin.jda = builder
DiscordRpc.jda = builder
Admin.info()
Runtime.getRuntime().addShutdownHook(object : Thread() {
override fun run() {
println("Shutting down...")
println("Sending shutdown notice to Admin, waiting 5s...")
Admin.println("Shutting down")
sleep(5000)
}
})
timer("Updater", daemon = true, initialDelay = 0L, period = Admin.updateMs) {
if (SiteWatcher.hasNewBlogPost()) {
Channels.sentToAll(SiteWatcher.newestBlog!!.toMessageEmbed())
}
}
}

View File

@@ -1,28 +0,0 @@
package de.wulkanat.extensions
import Inaccessibles
import net.dv8tion.jda.api.entities.Message
import net.dv8tion.jda.api.entities.MessageChannel
import net.dv8tion.jda.api.requests.restaction.MessageAction
import net.dv8tion.jda.internal.requests.Method
import net.dv8tion.jda.internal.requests.Route
import net.dv8tion.jda.internal.requests.restaction.MessageActionImpl
import net.dv8tion.jda.internal.utils.Checks
fun MessageChannel.crosspostById(messageId: String): MessageAction {
Checks.isSnowflake(messageId, "Message ID")
val route = CROSSPOST_MESSAGE.compile(id, messageId)
return MessageActionImpl(jda, route, this).append("This is not of your interest.")
}
fun Message.crosspost(): MessageAction {
val messageId = Inaccessibles.toUnsignedString(idLong)
return channel.crosspostById(messageId)
}
val CROSSPOST_MESSAGE: Route = Inaccessibles.getRoute(
Method.POST,
"channels/{channel_id}/messages/{message_id}/crosspost"
)

View File

@@ -1,25 +0,0 @@
package de.wulkanat.model
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.entities.MessageEmbed
import de.wulkanat.extensions.hex2Rgb
data class BlogPostPreview(
val title: String,
val description: String,
val date: String,
val author: String,
val imgUrl: String,
val fullPostUrl: String
) {
fun toMessageEmbed(): MessageEmbed {
return EmbedBuilder()
.setTitle(this.title, this.fullPostUrl)
.setDescription(this.description)
.setAuthor(this.author)
.setThumbnail(this.imgUrl)
.setFooter(this.date, "https://www.hytale.com/static/images/logo-h.png")
.setColor(hex2Rgb("#337fb0"))
.build()
}
}

View File

@@ -1,24 +0,0 @@
package de.wulkanat.web
import de.wulkanat.model.BlogPostPreview
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
object BlogPostParser {
fun getFistBlog(doc: Document): BlogPostPreview {
val posts = doc.getElementsByClass("postWrapper")
return parseBlog(posts.first())
}
private fun parseBlog(elm: Element): BlogPostPreview {
return BlogPostPreview(
title = elm.getElementsByClass("post__details__heading").first().text(),
imgUrl = elm.getElementsByClass("post__image__frame").first().child(0).attr("src"),
fullPostUrl = elm.child(0).absUrl("href"),
date = elm.getElementsByClass("post__details__meta__date").first().text(),
author = elm.getElementsByClass("post__details__meta__author").first().text(),
description = elm.getElementsByClass("post__details__body").first().text()
)
}
}

View File

@@ -1,44 +0,0 @@
package de.wulkanat.web
import de.wulkanat.Admin
import de.wulkanat.DiscordRpc
import de.wulkanat.model.BlogPostPreview
import org.jsoup.Jsoup
import java.io.IOException
object SiteWatcher {
private const val BLOG_INDEX_URL = "https://www.hytale.com/news"
var newestBlog: BlogPostPreview? = null
private var siteOnline = false
fun hasNewBlogPost(): Boolean {
try {
val doc = Jsoup.connect(BLOG_INDEX_URL).get()
val newBlog = BlogPostParser.getFistBlog(doc)
if (newestBlog == newBlog) {
return false
}
if (newestBlog == null) {
newestBlog = newBlog
return false
} else {
newestBlog = newBlog
}
} 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
}
}

View File

@@ -0,0 +1,104 @@
@file:JvmName("Admin")
package org.hmcore
import org.hmcore.extensions.embed
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.User
import java.awt.Color
object Admin {
@JvmField
val adFile = Json.decodeFromString<AdminFile>(ADMIN_FILE.readText())
val userId: Long = adFile.adminId
val token: String = adFile.token
val updateMs: Long = adFile.updateMs
val message: String = adFile.watchingMessage
val offlineMessage: String = adFile.offlineMessage
var admin: User? = null
fun connectToUser() {
Main.jdas.find { jda ->
jda.retrieveUserById(userId).complete()?.also { admin = it } != null
} ?: return kotlin.io.println("Connection to org.hmcore.Admin failed!")
kotlin.io.println("Connected to ${admin!!.name}. No further errors will be printed here.")
}
fun println(msg: String) = sendDevMessage(
embed {
title = msg
color = Color.WHITE
}, msg
)
fun printlnBlocking(msg: String) = sendDevMessageBlocking(
embed {
title = msg
color = Color.WHITE
}, msg
)
fun error(msg: String, error: String, author: User? = null) = sendDevMessage(
embed {
title = msg
description = error
color = Color.RED
author {
name = author?.asTag
url = author?.avatarUrl
icon = author?.avatarUrl
}
}, "$msg\n\n${error}"
)
fun errorBlocking(msg: String, error: Exception) = sendDevMessageBlocking(
embed {
title = msg
description = error.message
color = Color.RED
}, "$msg\n\n${error.message}"
)
fun warning(msg: String) = sendDevMessage(
embed {
title = msg
color = Color.YELLOW
}, msg
)
fun info() {
sendDevMessage(
embed {
title = "Now watching for new Hytale Blogposts every ${updateMs / 1000}s"
description = """
${Channels.getServerNames().joinToString("\n")}
**_Service Channels_**
${Channels.getServiceChannelServers().joinToString("\n")}
""".trimIndent()
color = Color.GREEN
}, "Now watching for new Hytale BlogPosts"
)
}
fun silent(msg: String) {
kotlin.io.println(msg)
}
private fun sendDevMessageBlocking(messageEmbed: MessageEmbed, fallback: String) {
(admin?.openPrivateChannel() ?: return kotlin.io.println(fallback))
.complete().sendMessage(messageEmbed).complete()
}
fun sendDevMessage(messageEmbed: MessageEmbed, fallback: String) {
(admin?.openPrivateChannel() ?: return kotlin.io.println(fallback)).queue {
it.sendMessage(messageEmbed).queue()
}
}
}

View File

@@ -1,8 +1,8 @@
package de.wulkanat
package org.hmcore
import de.wulkanat.model.BlogPostPreview
import org.hmcore.web.fakeUpdateBlogPost
import org.hmcore.web.fakeUpdateJobListings
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
@@ -23,16 +23,19 @@ class AdminCli : ListenerAdapter() {
when (command[0].value) {
"stop" -> exitProcess(1)
"fakeUpdate" -> {
SiteWatcher.newestBlog = BlogPostPreview(
title = "FakePost",
imgUrl = "",
fullPostUrl = "",
author = "wulkanat",
date = "now",
description = "Lorem Ipsum"
)
if (command.size < 2) {
Admin.println("Specify type")
} else {
val amount = command.getOrNull(2)?.value?.trim('`')?.toIntOrNull() ?: 1
when (command[1].value.trim('`')) {
"blog" -> fakeUpdateBlogPost(amount)
"twitter" -> TwitterJob.lastTweetID = "poggers"
"jobs" -> fakeUpdateJobListings(amount)
else -> Admin.println("Must be blog|twitter|jobs")
}
Admin.println("Posting on next update cycle.")
Admin.println("Posting $amount on next update cycle.")
}
}
"info" -> {
Admin.info()
@@ -69,7 +72,7 @@ class AdminCli : ListenerAdapter() {
"""
**${prefix}stop**
Stop the bot
**${prefix}fakeUpdate**
**${prefix}fakeUpdate [blog|twitter|jobs] [amount]**
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

View File

@@ -0,0 +1,180 @@
@file:JvmName("Channels")
package org.hmcore
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.Permission
import net.dv8tion.jda.api.entities.Message
import org.hmcore.extensions.embed
import org.hmcore.extensions.toWebhook
import java.awt.Color
object Channels {
/**
* List of (ServerID, ChannelID)
*/
var channels: MutableList<DiscordChannel> = refreshChannelsFromDisk()
var serviceChannels: MutableList<ServiceChannel> = refreshServiceChannelsFromDisk()
fun sentToAll(messageEmbed: Message, msgType: MessageType) {
try {
messageEmbed.toWebhook().send(WEBHOOKS.blogPostsWebhookUrl)
} catch (e: Exception) {
e.printStackTrace()
Admin.sendDevMessage(embed {
title = "Error"
description = e.stackTraceToString()
color = Color.red
}, e.stackTrace.toString())
}
Main.jdas.forEach { jda ->
for (channel_pair in channels) {
try {
if(!channel_pair.type.equals(msgType)) continue
val channel = jda.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) {
Main.jdas.forEach {
val channel = it.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() {
Main.jdas.forEach {
for (channel_pair in channels) {
val channel = it.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() =
Json.decodeFromString<List<DiscordChannel>>(SERVERS_FILE.readText()).toMutableList()
fun refreshServiceChannelsFromDisk() =
Json.decodeFromString<List<ServiceChannel>>(SERVICE_CHANNELS_FILE.readText()).toMutableList()
fun getServerNames(server: Long? = null) = Main.jdas.flatMap { jda ->
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)*"
}
val role = when (it.mentionedRole) {
null -> ""
"everyone" -> " @everyone"
else -> " @${channel.guild.getRoleById(it.mentionedRole ?: "")?.name}"
}
val publish = if (it.autoPublish) " (publish)" else ""
val type = " " + it.type.toString()
"**${channel.guild.name}** #${channel.name}${role}${publish}${type}${
if (it.message == null) {
""
} else {
"\n*${it.message!!.message}*${if (it.message!!.pushAnnouncement) " (publish)" else ""}"
}
}"
}
}
fun getServiceChannelServers(server: Long? = null): List<String> {
return Main.jdas.flatMap { jda ->
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) =
Main.jdas.map { it.getTextChannelById(id) }.firstOrNull()
fun addChannel(id: Long, msgType: MessageType): DiscordChannel? {
if (channels.find { it.id == id && it.type == msgType } != null) {
return null
}
val out = DiscordChannel(id, msgType)
channels.add(out)
saveChannels()
return out
}
fun saveChannels() {
SERVERS_FILE.writeText(Json.encodeToString(channels))
SERVICE_CHANNELS_FILE.writeText(Json.encodeToString(serviceChannels))
}
}

View File

@@ -0,0 +1,66 @@
@file:JvmName("DataIO")
package org.hmcore
import org.hmcore.extensions.ensureExists
import kotlinx.serialization.Required
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
const val channelVersion = 2.0
@Serializable
data class DiscordChannel(
val id: Long,
var type: MessageType,
var mentionedRole: String? = null,
var autoPublish: Boolean = false,
var message: CustomMessage? = null,
var version: Double? = channelVersion
)
@Serializable
data class ServiceChannel(
val id: Long
)
@Serializable
data class CustomMessage(
var message: String,
var pushAnnouncement: Boolean = false
)
@Serializable
data class AdminFile(
@Required val adminId: Long = 12345,
@Required val token: String = "12345",
@Required val updateMs: Long = 30000,
@Required val shards: Int = 6,
@Required val watchingMessage: String = "for new Blogposts",
@Required val offlineMessage: String = "CONNECTION FAILED",
@Required var twitterApi: TwitterApi? = TwitterApi()
)
@Serializable
data class TwitterApi(
@Required val accessToken: String = "accessTokenHere",
@Required val accessTokenSecret: String = "accessTokenSecretHere",
@Required val apiKey: String = "apiKeyHere",
@Required val apiKeySecret: String = "Api Key secret here"
)
@Serializable
data class Webhooks(
@Required val blogPostsWebhookUrl: String = "https://...",
@Required val jobListingsWebhookUrl: String = "https://...",
)
val WEBHOOKS_FILE = File("webhooks.json").ensureExists(Json.encodeToString(Webhooks()))
val WEBHOOKS = Json.decodeFromString<Webhooks>(WEBHOOKS_FILE.readText())
val SERVERS_FILE = File("servers.json").ensureExists(Json.encodeToString(listOf<DiscordChannel>()))
val SERVICE_CHANNELS_FILE =
File("service_channels.json").ensureExists(Json.encodeToString(listOf<ServiceChannel>()))
val ADMIN_FILE = File("admin.json").ensureExists(Json.encodeToString(AdminFile()))

View File

@@ -0,0 +1,13 @@
package org.hmcore
object DiscordRpc {
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,4 +1,4 @@
package de.wulkanat
package org.hmcore
import net.dv8tion.jda.api.events.ExceptionEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter

View File

@@ -0,0 +1,124 @@
package org.hmcore
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.hmcore.web.getNewBlogPosts
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.JDABuilder
import net.dv8tion.jda.api.MessageBuilder
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.requests.GatewayIntent
import net.dv8tion.jda.api.utils.ChunkingFilter
import net.dv8tion.jda.api.utils.MemberCachePolicy
import net.dv8tion.jda.api.utils.cache.CacheFlag
import org.hmcore.web.getNewJobListings
import org.quartz.CronScheduleBuilder.cronSchedule
import org.quartz.JobBuilder.newJob
import org.quartz.JobDetail
import org.quartz.Trigger
import org.quartz.TriggerBuilder.newTrigger
import org.quartz.impl.StdSchedulerFactory
import java.io.File
import javax.security.auth.login.LoginException
import kotlin.concurrent.timer
object Main {
@JvmField
var jdas = mutableListOf<JDA>()
@JvmStatic
fun main(args: Array<String>) {
if(args.isEmpty()) startBot() else
when(args[0]) {
"serverDataConvert1" -> serverDataConvert1()
else -> startBot()
}
}
fun serverDataConvert1() {
var file = File("servers.json")
if(!file.exists()) return
var content = ""
file.bufferedReader().readLines().forEach {
content += it
.replace("""mentionedRole":""".toRegex(),"""type":"BLOGPOST","mentionedRole":""")
}
file.writeBytes(content.encodeToByteArray())
}
fun startBot() {
val builder = JDABuilder.createLight(
Admin.token,
GatewayIntent.GUILD_MESSAGES,
GatewayIntent.DIRECT_MESSAGES
).setActivity(Activity.watching(Admin.message))
configureMemoryUsage(builder)
for (i in 0 until Admin.adFile.shards) {
try {
jdas.add(builder.useSharding(i, Admin.adFile.shards).build().apply {
addEventListener(AdminCli())
addEventListener(ErrorHandler())
addEventListener(OwnerCli())
awaitReady()
})
} catch (loginException: LoginException) {
println("!!! Shard $i could not login !!!")
}
}
Admin.connectToUser()
Admin.info()
Runtime.getRuntime().addShutdownHook(object : Thread() {
override fun run() {
println("Shutting down...")
println("Sending shutdown notice to Admin, waiting 5s...")
Admin.println("Shutting down")
sleep(5000)
}
})
timer("UpdaterBlogpost", daemon = true, initialDelay = 0L, period = Admin.updateMs) {
getNewBlogPosts()?.forEach {
Channels.sentToAll(MessageBuilder().setEmbed(it.toMessageEmbed()).build(), MessageType.BLOGPOST)
}
}
timer("UpdaterJob", daemon = true, initialDelay = 0L, period = Admin.updateMs) {
getNewJobListings()?.forEach {
Channels.sentToAll(MessageBuilder().setEmbed(it.toMessageEmbed()).build(), MessageType.JOB_LISTING)
}
}
val scheduler = StdSchedulerFactory.getDefaultScheduler()
scheduler.start()
val job: JobDetail = newJob(TwitterJob::class.java)
.withIdentity("job1", "group1")
.build()
// Trigger the job to run now, and then repeat every 5 minutes
val trigger: Trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(cronSchedule("0 0/5 * 1/1 * ? *"))
.build()
scheduler.scheduleJob(job, trigger)
}
private fun configureMemoryUsage(builder: JDABuilder) {
builder.disableCache(CacheFlag.ACTIVITY)
builder.setMemberCachePolicy(MemberCachePolicy.VOICE.or(MemberCachePolicy.OWNER))
builder.setChunkingFilter(ChunkingFilter.NONE)
builder.disableIntents(GatewayIntent.GUILD_PRESENCES, GatewayIntent.GUILD_MESSAGE_TYPING)
// Consider guilds with more than 50 members as "large".
// Large guilds will only provide online members in their setup and thus reduce bandwidth if chunking is disabled.
builder.setLargeThreshold(50)
}
}

View File

@@ -1,4 +1,4 @@
package de.wulkanat
package org.hmcore
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.Permission
@@ -18,10 +18,38 @@ class OwnerCli : ListenerAdapter() {
val command = msg.removePrefix(prefix).split(Regex("\\s+"))
val channelId = event.message.channel.idLong
val msgType: MessageType
if(command.size < 2)
msgType = MessageType.INVALID
else msgType = when (command[1].lowercase()) {
"blogpost" -> MessageType.BLOGPOST
"twitter" -> MessageType.TWITTER
"job" -> MessageType.JOB_LISTING
"website" -> MessageType.WEBSITE_CHANGED
else -> MessageType.INVALID
}
when (command.first()) {
"categories" -> {
event.message.channel.sendMessage(EmbedBuilder()
.setTitle("Categories")
.setColor(Color.YELLOW)
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.setDescription("Valid Categories:\n" +
" Blogpost\n" +
" Twitter\n" +
" Job - (changes of Job listings)\n" +
" Website - (if the content of some website or subdomain thats owned by hypixel studios gets changed) - soon\n")
.build()).queue()
return
}
"add" -> {
val result = Channels.addChannel(channelId, null)
if (msgType == MessageType.INVALID) {
event.message.channel.sendMessage("Please choose a valid category. List valid categories with ${prefix}categories").queue()
return
}
val result = Channels.addChannel(channelId, msgType)
if (result == null) {
event.message.channel.sendMessage("Already added.").queue()
} else {
@@ -30,7 +58,10 @@ class OwnerCli : ListenerAdapter() {
}
}
"remove" -> {
val result = Channels.channels.removeAll { it.id == channelId }
if (msgType == MessageType.INVALID) {
event.message.channel.sendMessage("Please choose a valid category. List valid categories with ${prefix}categories").queue()
}
val result = Channels.channels.removeAll { it.id == channelId && (it.type == msgType || it.type == MessageType.INVALID) }
Channels.saveChannels()
if (result) {
event.message.channel.sendMessage("Removed.").queue()
@@ -39,31 +70,37 @@ class OwnerCli : ListenerAdapter() {
}
}
"publish" -> {
val result = Channels.channels.find { it.id == channelId }
if (msgType == MessageType.INVALID) {
event.message.channel.sendMessage("Please choose a valid category. List valid categories with ${prefix}categories").queue()
}
val result = Channels.channels.find { it.id == channelId && it.type == msgType}
if (result != null) {
if (command.size > 1 && listOf("on", "off").contains(command[1])) {
result.autoPublish = command[1] == "on"
if (command.size > 2 && listOf("on", "off").contains(command[2])) {
result.autoPublish = command[2] == "on"
Channels.saveChannels()
event.message.channel.sendMessage("Auto publish is now ${command[1]}").queue()
} else {
event.message.channel.sendMessage("Usage: `${prefix}publish [on|off]`")
event.message.channel.sendMessage("Usage: `${prefix}publish [type] [on|off]`")
}
} else {
event.message.channel.sendMessage("Channel not registered.").queue()
}
}
"ping" -> {
val result = Channels.channels.find { it.id == channelId }
if (msgType == MessageType.INVALID) {
event.message.channel.sendMessage("Please choose a valid category. List valid categories with ${prefix}categories").queue()
}
val result = Channels.channels.find { it.id == channelId && it.type == msgType}
if (result != null) {
if (command.size > 1) {
val roles = event.message.guild.getRolesByName(command[1], false)
if (command.size > 2) {
val roles = event.message.guild.getRolesByName(command[2], false)
result.mentionedRole = when {
command[1] == "everyone" -> {
command[2] == "everyone" -> {
event.message.channel.sendMessage("Now pinging everyone.").queue()
"everyone"
}
command[1] == "none" -> {
command[2] == "none" -> {
event.message.channel.sendMessage("Now pinging none.").queue()
null
}
@@ -78,29 +115,35 @@ class OwnerCli : ListenerAdapter() {
}
Channels.saveChannels()
} else {
event.message.channel.sendMessage("Usage: `${prefix}ping [everyone|none|roleName]`")
event.message.channel.sendMessage("Usage: `${prefix}ping [type] [everyone|none|roleName]`")
}
} else {
event.message.channel.sendMessage("Channel is not registered.").queue()
}
}
"setMessage" -> {
val result = Channels.channels.find { it.id == channelId }
if (msgType == MessageType.INVALID) {
event.message.channel.sendMessage("Please choose a valid category. List valid categories with ${prefix}categories").queue()
}
val result = Channels.channels.find { it.id == channelId && it.type == msgType }
if (result != null) {
if (command.size > 1) {
val message = event.message.contentRaw.removePrefix("${prefix}setMessage").trim()
if (command.size > 2) {
val message = command.subList(2, command.size).toString().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]`")
event.message.channel.sendMessage("Usage: `${prefix}setMessage [type] [message]`")
}
} else {
event.message.channel.sendMessage("Channel is not registered.").queue()
}
}
"resetMessage" -> {
val result = Channels.channels.find { it.id == channelId }
if (msgType == MessageType.INVALID) {
event.message.channel.sendMessage("Please choose a valid category. List valid categories with ${prefix}categories").queue()
}
val result = Channels.channels.find { it.id == channelId && it.type == msgType }
if (result != null) {
result.message = null
Channels.saveChannels()
@@ -132,16 +175,19 @@ class OwnerCli : ListenerAdapter() {
}
"publishMessage" -> {
val result = Channels.channels.find { it.id == channelId }
if (msgType == MessageType.INVALID) {
event.message.channel.sendMessage("Please choose a valid category. List valid categories with ${prefix}categories").queue()
}
val result = Channels.channels.find { it.id == channelId && it.type == msgType }
if (result != null) {
if (result.message != null) {
if (command.size > 1 && listOf("on", "off").contains(command[1])) {
result.message?.pushAnnouncement = command[1] == "on"
if (command.size > 2 && listOf("on", "off").contains(command[2])) {
result.message?.pushAnnouncement = command[2] == "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]`")
event.message.channel.sendMessage("Usage: `${prefix}publishMessage [type] [on|off]`")
}
} else {
event.message.channel.sendMessage("Channel has no custom message.").queue()
@@ -185,22 +231,24 @@ class OwnerCli : ListenerAdapter() {
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.setDescription(
"""
**${prefix}add**
**${prefix}add [type]**
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**
**${prefix}remove [type]**
Remove this channel to the notified list
**${prefix}publish [on|off]**
**${prefix}publish [type] [on|off]**
[Community|Partner|Verified only] Auto publish the message if in an announcement channel
**${prefix}ping [none|everyone|roleName]**
**${prefix}ping [type] [none|everyone|roleName]**
What role to ping
**${prefix}setMessage [message]**
**${prefix}setMessage [type] [message]**
Set a custom message to show
**${prefix}resetMessage**
**${prefix}resetMessag [type]e**
Reset the message
**${prefix}info**
Show an overview about all channels registered on this server
**${prefix}categories**
Show a list of categories available for alert types
**${prefix}report**
Report an issue to the Bot Admin (this will share your user name so they can contact you)
**${prefix}help**

View File

@@ -1,4 +1,4 @@
package de.wulkanat.extensions
package org.hmcore.extensions
import java.awt.Color
@@ -8,4 +8,7 @@ fun hex2Rgb(colorStr: String): Color {
Integer.valueOf(colorStr.substring(3, 5), 16),
Integer.valueOf(colorStr.substring(5, 7), 16)
)
}
}
fun Color.toRgb() =
(((red shl 8) + green) shl 8) + blue

View File

@@ -0,0 +1,76 @@
package org.hmcore.extensions
import net.dv8tion.jda.api.EmbedBuilder
import java.awt.Color
class EmbedBuilderBuilder {
val _embed = EmbedBuilder()
var title: String?
set(value) {
_embed.setTitle(value)
}
get() = null
var description: String?
set(value) {
_embed.setDescription(value)
}
get() = null
var color: Color
set(value) {
_embed.setColor(value)
}
get() = Color.BLACK
var thumbnail: String?
set(value) {
_embed.setThumbnail(value)
}
get() = null
fun field(builder: FieldBuilderBuilder.() -> Unit) =
FieldBuilderBuilder().apply { builder() }.let {
_embed.addField(it.name, it.value, it.inline)
}
fun author(builder: AuthorBuilderBuilder.() -> Unit) =
AuthorBuilderBuilder().apply { builder() }.let {
_embed.setAuthor(it.name, it.url, it.icon)
}
fun title(builder: TitleBuilderBuilder.() -> Unit) =
TitleBuilderBuilder().apply { builder() }.let {
_embed.setTitle(it.value, it.url)
}
fun footer(builder: FooterBuilderBuilder.() -> Unit) =
FooterBuilderBuilder().apply { builder() }.let {
_embed.setFooter(it.value, it.iconUrl)
}
}
class FieldBuilderBuilder {
var name: String? = null
var value: String? = null
var inline = false
}
class TitleBuilderBuilder {
var value: String? = null
var url: String? = null
}
class AuthorBuilderBuilder {
var name: String? = null
var url: String? = null
var icon: String? = null
}
class FooterBuilderBuilder {
var value: String? = null
var iconUrl: String? = null
}
fun embed(builder: EmbedBuilderBuilder.() -> Unit) =
EmbedBuilderBuilder().apply { builder() }._embed.build()

View File

@@ -1,4 +1,4 @@
package de.wulkanat.extensions
package org.hmcore.extensions
import java.io.File

View File

@@ -0,0 +1,11 @@
package org.hmcore.extensions
import org.jsoup.nodes.Element
import org.jsoup.select.Elements
operator fun Element.get(className: String): Elements =
this.getElementsByClass(className)
val Elements.text get() = text().trim()
val Element.absUrl get(): String = child(0).absUrl("href")
val Element.imgSrc get(): String = child(0).attr("src")

View File

@@ -0,0 +1,44 @@
package org.hmcore.extensions
import net.dv8tion.jda.api.entities.Message
import org.hmcore.WEBHOOKS
import org.hmcore.webhook.*
fun Message.toWebhook(): DiscordWebhook {
val webhook = DiscordWebhook()
webhook.content = contentRaw
webhook.tts = false
embeds.forEach { embed ->
webhook.embeds.add(DiscordWebhookEmbed().apply {
title = embed.title
description = embed.description
color = embed.color?.toRgb()
image = Image(embed.image?.url)
url = embed.url
thumbnail = Thumbnail(embed.thumbnail?.url)
author = Author(
embed.author?.name,
embed.author?.url,
embed.author?.iconUrl
)
footer = Footer(
embed.footer?.text,
embed.footer?.iconUrl,
)
for (field in embed.fields) {
fields.add(
Field(
field.name,
field.value,
field.isInline,
)
)
}
})
}
return webhook
}

View File

@@ -0,0 +1,33 @@
package org.hmcore.model
import org.hmcore.extensions.hex2Rgb
import kotlinx.serialization.Serializable
import org.hmcore.extensions.embed
@Serializable
data class BlogPostPreview(
val title: String,
val description: String,
val date: String,
val author: String,
val imgUrl: String,
val fullPostUrl: String
) {
fun toMessageEmbed() = embed {
title {
value = this@BlogPostPreview.title
url = fullPostUrl
}
description = this@BlogPostPreview.description
color = hex2Rgb("#337FB0")
thumbnail = imgUrl
author {
name = author
}
footer {
value = date
iconUrl = "https://www.hytale.com/static/images/logo-h.png"
}
}
}

View File

@@ -0,0 +1,26 @@
package org.hmcore.model
import org.hmcore.extensions.hex2Rgb
import kotlinx.serialization.Serializable
import org.hmcore.extensions.embed
@Serializable
data class JobListingPreview(
val title: String,
val department: String,
val location: String,
val fullListingUrl: String
) {
fun toMessageEmbed() = embed {
title {
value = this@JobListingPreview.title
url = fullListingUrl
}
description = department
color = hex2Rgb("#337fb0")
author {
name = location
}
}
}

View File

@@ -0,0 +1,40 @@
package org.hmcore.web
import org.hmcore.extensions.absUrl
import org.hmcore.extensions.get
import org.hmcore.extensions.imgSrc
import org.hmcore.extensions.text
import org.hmcore.model.BlogPostPreview
import org.hmcore.model.JobListingPreview
private const val BLOG_POST_STATE_FILE_NAME = "blog_state.json"
fun fakeUpdateBlogPost(amount: Int = 1) = removeFromSiteSave<BlogPostPreview>(BLOG_POST_STATE_FILE_NAME, amount)
fun getNewBlogPosts() = updateSite("https://hytale.com/news", BLOG_POST_STATE_FILE_NAME) { doc ->
doc["postWrapper"].map {
BlogPostPreview(
title = it["post__details__heading"].text,
imgUrl = it["post__image__frame"].first().imgSrc,
fullPostUrl = it.absUrl,
date = it["post__details__meta__date"].text,
author = it["post__details__meta__author"].text,
description = it["post__details__body"].text,
)
}
}
private const val JOB_LISTING_STATE_FILE_NAME = "jobs_state.json"
fun fakeUpdateJobListings(amount: Int = 1) = removeFromSiteSave<JobListingPreview>(JOB_LISTING_STATE_FILE_NAME, amount)
fun getNewJobListings() = updateSite("https://hypixelstudios.com/jobs/", JOB_LISTING_STATE_FILE_NAME) { doc ->
doc["current-jobs__departments"].flatMap { jobDepartment ->
val jobDepartmentName = jobDepartment["current-jobs__department-name"].text
jobDepartment["current-jobs__job"].map { job ->
JobListingPreview(
title = job["current-jobs__job-title"].text,
department = jobDepartmentName,
location = job["current-jobs__job-location"].text,
fullListingUrl = job.absUrl
)
}
}
}

View File

@@ -0,0 +1,42 @@
package org.hmcore.web
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.File
import java.io.IOException
/**
* Removes the first element of a saved JSON list file
*/
inline fun <reified T> removeFromSiteSave(fileName: String, amount: Int = 1) =
File(fileName).takeIf { it.exists() }?.let {
it.writeText(
if (amount >= 0) Json.encodeToString(
Json.decodeFromString<List<T>>(it.readText()).subList(0, amount)
)
else "[]"
)
}
inline fun <reified T> updateSite(url: String, fileName: String, parser: (Document) -> List<T>) = try {
val currentStateFile = File(fileName)
val retrievedElements = parser(Jsoup.connect(url).get())
var currentElements = if (currentStateFile.exists())
Json.decodeFromString(currentStateFile.readText()) else retrievedElements
val newElements = retrievedElements - currentElements
currentElements = retrievedElements
currentStateFile.writeText(Json.encodeToString(currentElements))
newElements
} catch (e: IOException) {
// TODO: put this somewhere else
// Admin.error("""Fetching "$url" failed!""", e.message ?: e.localizedMessage)
// DiscordRpc.updatePresence(canUpdate.also { canUpdate = false })
null
}

View File

@@ -0,0 +1,92 @@
package org.hmcore.webhook
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.IOException
import java.io.OutputStream
import java.net.URL
import javax.net.ssl.HttpsURLConnection
@Serializable
data class Footer(
val text: String? = null,
@SerialName("icon_url")
val iconUrl: String? = null,
)
@Serializable
data class Thumbnail(
val url: String? = null,
)
@Serializable
data class Image(
val url: String? = null,
)
@Serializable
data class Author(
val name: String? = null,
val url: String? = null,
@SerialName("icon_url")
val iconUrl: String? = null,
)
@Serializable
data class Field(
val name: String? = null,
val value: String? = null,
val inline: Boolean? = null,
)
@Serializable
data class DiscordWebhookEmbed(
var title: String? = null,
var description: String? = null,
var url: String? = null,
var color: Int? = null,
var footer: Footer? = null,
var thumbnail: Thumbnail? = null,
var image: Image? = null,
var author: Author? = null,
var fields: MutableList<Field> = mutableListOf(),
)
@Serializable
data class DiscordWebhook(
var content: String? = null,
var username: String? = null,
var avatarUrl: String? = null,
var tts: Boolean = false,
var embeds: MutableList<DiscordWebhookEmbed> = mutableListOf(),
) {
fun send(url: String): Boolean {
var connection: HttpsURLConnection? = null
var stream: OutputStream? = null
return try {
connection = URL(url).openConnection() as HttpsURLConnection
connection.addRequestProperty("Content-Type", "application/json")
connection.addRequestProperty("User-Agent", "Kotlin-DiscordWebhook")
connection.doOutput = true
connection.requestMethod = "POST"
stream = connection.outputStream
stream.write(Json.encodeToString(this).toByteArray())
stream.flush()
true
} catch (e: IOException) {
e.printStackTrace()
false
} finally {
stream?.close()
connection?.inputStream?.close()
connection?.disconnect()
}
}
}

View File

@@ -0,0 +1,25 @@
package org.hmcore.extensions
import org.junit.Assert.assertEquals
import org.junit.Test
import java.awt.Color
class ColorTest {
@Test
fun `color should parse from hex correctly`() {
assertEquals(hex2Rgb("#FFFFFF"), Color.WHITE)
assertEquals(hex2Rgb("#000000"), Color.BLACK)
assertEquals(hex2Rgb("#FF0000"), Color.RED)
assertEquals(hex2Rgb("#00FF00"), Color.GREEN)
assertEquals(hex2Rgb("#0000FF"), Color.BLUE)
}
@Test
fun `color should convert to RGB correctly`() {
assertEquals(16777215, Color.WHITE.toRgb())
assertEquals(0, Color.BLACK.toRgb())
assertEquals(16711680, Color.RED.toRgb())
assertEquals(65280, Color.GREEN.toRgb())
assertEquals(255, Color.BLUE.toRgb())
}
}

View File

@@ -0,0 +1,90 @@
package org.hmcore.extensions
import java.awt.Color
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class EmbedTest {
@Test
fun `Embed Title DSL should work`() {
val dslEmbed = embed {
title {
value = "Title"
url = "https://a.b.c"
}
}
assertEquals("Title", dslEmbed.title)
assertEquals("https://a.b.c", dslEmbed.url)
}
@Test
fun `Embed Thumbnail should work`() {
val dslEmbed = embed {
thumbnail = "https://a.b.c"
}
assertNotNull(dslEmbed.thumbnail)
assertEquals("https://a.b.c", dslEmbed.thumbnail!!.url)
}
@Test
fun `Embed Footer DSL should work`() {
val dslEmbed = embed {
footer {
value = "Value"
iconUrl = "https://a.b.c"
}
}
assertNotNull(dslEmbed.footer)
assertEquals("Value", dslEmbed.footer!!.text)
assertEquals("https://a.b.c", dslEmbed.footer!!.iconUrl)
}
@Test
fun `Embed DSL should produce same result as native`() {
val dslEmbed = embed {
title = "A Title"
description = "A Description"
color = Color.YELLOW
author {
name = "An author"
icon = "https://d.e.f"
url = "https://a.b.c"
}
field {
inline = true
name = "Field1"
value = "Body1"
}
field {
inline = false
name = "Field2"
value = "Body2"
}
}
assertEquals("A Title", dslEmbed.title)
assertEquals("A Description", dslEmbed.description)
assertEquals(Color.YELLOW, dslEmbed.color)
assertNotNull(dslEmbed.author)
assertEquals("An author", dslEmbed.author!!.name)
assertEquals("https://d.e.f", dslEmbed.author!!.iconUrl)
assertEquals("https://a.b.c", dslEmbed.author!!.url)
assertEquals(2, dslEmbed.fields.size)
assertEquals("Field1", dslEmbed.fields[0].name)
assertEquals("Body1", dslEmbed.fields[0].value)
assertEquals(true, dslEmbed.fields[0].isInline)
assertEquals("Field2", dslEmbed.fields[1].name)
assertEquals("Body2", dslEmbed.fields[1].value)
assertEquals(false, dslEmbed.fields[1].isInline)
}
}

View File

@@ -0,0 +1,73 @@
package org.hmcore.extensions
import io.mockk.*
import java.io.File
import kotlin.test.Test
import kotlin.test.assertEquals
class FileTest {
@Test
fun `Ensure exists should create a file with default content if it does not exist`() {
val file = mockk<File>()
mockkStatic("kotlin.io.FilesKt__FileReadWriteKt")
every { file.exists() } returns false
every { file.createNewFile() } returns true
every { file.writeText(any()) } just Runs
file.ensureExists("Default Text")
verifySequence {
file.exists()
file.createNewFile()
file.writeText("Default Text")
}
}
@Test
fun `Ensure exists should create a file if supplied with null but not write text`() {
val file = mockk<File>()
mockkStatic("kotlin.io.FilesKt__FileReadWriteKt")
every { file.exists() } returns false
every { file.createNewFile() } returns true
every { file.writeText(any()) } just Runs
file.ensureExists(null)
verifySequence {
file.exists()
file.createNewFile()
}
}
@Test
fun `Ensure exists should not touch the file if it exists`() {
val file = mockk<File>()
mockkStatic("kotlin.io.FilesKt__FileReadWriteKt")
every { file.exists() } returns true
every { file.createNewFile() } returns true
every { file.writeText(any()) } just Runs
file.ensureExists(null)
verifySequence {
file.exists()
}
}
@Test
fun `Ensure exists should return itself`() {
val file = mockk<File>()
mockkStatic("kotlin.io.FilesKt__FileReadWriteKt")
every { file.exists() } returns true
every { file.createNewFile() } returns true
every { file.writeText(any()) } just Runs
val f2 = file.ensureExists(null)
assertEquals(file, f2)
}
}

View File

@@ -0,0 +1,34 @@
package org.hmcore.model
import org.hmcore.extensions.hex2Rgb
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class BlogPostPreviewTest {
@Test
fun `Blog post should be correctly parsed to Embed`() {
val embed = BlogPostPreview(
title = "Title",
description = "Description",
date = "01.01.2000",
author = "Nobody",
imgUrl = "https://a.b.c",
fullPostUrl = "https://d.e.f"
).toMessageEmbed()
assertEquals("Title", embed.title)
assertEquals("https://d.e.f", embed.url)
assertEquals("Description", embed.description)
assertEquals(hex2Rgb("#337FB0"), embed.color)
assertNotNull(embed.thumbnail)
assertEquals("https://a.b.c", embed.thumbnail!!.url)
assertNotNull(embed.footer)
assertEquals("01.01.2000", embed.footer!!.text)
assertEquals("https://www.hytale.com/static/images/logo-h.png", embed.footer!!.iconUrl)
assertNotNull(embed.author)
assertEquals(embed.author!!.name, "Nobody")
}
}

View File

@@ -0,0 +1,26 @@
package org.hmcore.model
import org.hmcore.extensions.hex2Rgb
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class JobListingPreviewTest {
@Test
fun `Job listings should be correctly parsed to Embed`() {
val embed = JobListingPreview(
title = "Title",
department = "Department",
location = "Null Island",
fullListingUrl = "https://d.e.f"
).toMessageEmbed()
assertEquals("Title", embed.title)
assertEquals("https://d.e.f", embed.url)
assertEquals("Department", embed.description)
assertEquals(hex2Rgb("#337FB0"), embed.color)
assertNotNull(embed.author)
assertEquals(embed.author!!.name, "Null Island")
}
}

View File

@@ -0,0 +1,17 @@
package org.hmcore.serialization
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.hmcore.MessageType
import org.junit.Test
import kotlin.test.assertNotNull
class EnumTest {
@Test
fun `Enum serialization`() {
println(Json.encodeToString(MessageType.BLOGPOST))
assertNotNull(MessageType.INVALID)
}
}

View File

@@ -0,0 +1,30 @@
package org.hmcore.webhook
import org.junit.Test
import kotlin.test.assertEquals
class DiscordWebhookEmbed {
/*@Test TODO: Test against JSON Schema or something
fun `Webhook class should comply with Discord specification`() {
val exampleWebhook = """
""".trimIndent()
}*/
@Test
fun `Webhook should not throw and return false if supplied invalid URL`() {
assertEquals(false, DiscordWebhook().send("not a valid url"))
}
@Test
fun `Webhook should return false if connection throws`() {
}
@Test
fun `Webhook should send correctly`() {
DiscordWebhook(
content = "Test"
)
}
}