40 Commits
v1.1 ... master

Author SHA1 Message Date
Wieland Schöbl
4e2186026c Revert "hotfix 1"
This reverts commit 1f3ebc42
2021-06-02 23:21:50 +02:00
Wieland Schöbl
7ff789afd3 Revert "temporarily removed inactive servers message"
This reverts commit 3d73d50a47.

# Conflicts:
#	src/main/kotlin/org/hmcore/Channels.kt
2021-06-02 23:20:52 +02:00
Wieland Schöbl
c9acfdc79e Revert "hotfix 1"
This reverts commit 1f3ebc42
2021-06-02 23:19:46 +02:00
UnrealValentin
3d73d50a47 temporarily removed inactive servers message 2021-06-02 23:04:29 +02:00
UnrealValentin
e6eaabaa1a Merge branch 'master' of https://github.com/HMCore/BlogShot 2021-06-02 22:58:59 +02:00
UnrealValentin
1f3ebc42e6 hotfix 1 2021-06-02 22:58:54 +02:00
Wieland Schöbl
43ae59b3e5 Fix main class 2021-06-02 22:12:05 +02:00
UnrealValentin
5511d1c5ac Merge remote-tracking branch 'origin/master'
# Conflicts:
#	.idea/workspace.xml
2021-06-02 21:59:29 +02:00
UnrealValentin
cd5321af75 added shadow jar 2021-06-02 21:59:10 +02:00
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
Wieland Schöbl
8b98d4ba3c fix connection failed message not disappearing 2020-09-25 16:04:31 +02:00
Wieland Schöbl
0877883e3c fix crash 2020-08-31 20:03:05 +02:00
Wieland Schöbl
a78c2343da Add service announcement channel 2020-08-19 14:25:54 +02:00
Wieland Schöbl
490a5dcd41 Add ability to customize message 2020-08-18 10:35:13 +02:00
Wieland Schöbl
5969a2f221 Add self-configuration feature 2020-08-18 00:39:15 +02:00
53 changed files with 2283 additions and 815 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

8
.gitignore vendored
View File

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

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
HytaleUpdateBot

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

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

View File

@@ -2,6 +2,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>

2
.idea/misc.xml generated
View File

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

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>

517
.idea/workspace.xml generated
View File

@@ -1,10 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="[1.1]">
<list default="true" id="1aabf22b-2f57-46ac-9973-367d8668ffd3" name="Default Changelist" comment="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" />
</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" />
@@ -28,11 +31,6 @@
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
<item name="Source Sets" type="e897c970:GradleViewContributor$SourceSetsNode" />
</path>
<path>
<item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
<item name="HytaleUpdateBot" type="f1a62948:ProjectNode" />
@@ -74,6 +72,16 @@
<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>
@@ -86,127 +94,223 @@
<list>
<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="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" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showExcludedFiles" value="true" />
<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$/src" />
<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="Gradle.HytaleUpdateBot [fatJar]" />
<item itemvalue="Kotlin.MainKt" />
<item itemvalue="Gradle.HytaleUpdateBot [build]" />
<item itemvalue="Gradle.HytaleUpdateBot [clean]" />
<item itemvalue="JAR Application.HytaleUpdateBot-all-1.0-SNAPSHOT.jar" />
<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>
@@ -217,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>
@@ -232,104 +337,146 @@
<option name="project" value="LOCAL" />
<updated>1597438052596</updated>
</task>
<option name="localTasksCounter" value="3" />
<task id="LOCAL-00003" summary="[1.1]">
<created>1597438317540</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1597438317540</updated>
</task>
<task id="LOCAL-00004" summary="Add service announcement channel">
<created>1597839954908</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1597839954909</updated>
</task>
<task id="LOCAL-00005" summary="prepare twitter integration">
<created>1601042375685</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1601042375685</updated>
</task>
<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]" />
<option name="LAST_COMMIT_MESSAGE" value="[1.1]" />
</component>
<component name="WindowStateProjectService">
<state x="552" y="179" key="#Project_Structure" timestamp="1597434105164">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="552" y="179" key="#Project_Structure/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597434105164" />
<state x="-1050" y="581" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1597352463714">
<screen x="-1050" y="105" width="1050" height="1640" />
</state>
<state x="-1050" y="581" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597352463714" />
<state x="633" y="446" key="#com.intellij.refactoring.move.MoveHandler.SelectRefactoringDialog" timestamp="1597362173063">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="633" y="446" key="#com.intellij.refactoring.move.MoveHandler.SelectRefactoringDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597362173063" />
<state x="690" y="268" key="#com.intellij.refactoring.safeDelete.UnsafeUsagesDialog" timestamp="1597428556346">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="690" y="268" key="#com.intellij.refactoring.safeDelete.UnsafeUsagesDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597428556346" />
<state x="739" y="173" width="484" height="693" key="#org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog" timestamp="1597362199927">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="739" y="173" width="484" height="693" key="#org.jetbrains.kotlin.idea.refactoring.move.moveDeclarations.ui.MoveKotlinTopLevelDeclarationsDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597362199927" />
<state x="128" y="270" width="490" height="591" key="#xdebugger.evaluate" timestamp="1597332665464">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="128" y="270" width="490" height="591" key="#xdebugger.evaluate/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597332665464" />
<state x="569" y="115" key="CommitChangelistDialog2" timestamp="1597438290366">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="569" y="115" key="CommitChangelistDialog2/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438290366" />
<state width="1876" height="161" key="GridCell.Tab.0.bottom" timestamp="1597438298864">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1876" height="161" key="GridCell.Tab.0.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1876" height="161" key="GridCell.Tab.0.center" timestamp="1597438298864">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329411" />
<state width="1876" height="161" key="GridCell.Tab.0.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1876" height="161" key="GridCell.Tab.0.left" timestamp="1597438298864">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329411" />
<state width="1876" height="161" key="GridCell.Tab.0.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1876" height="161" key="GridCell.Tab.0.right" timestamp="1597438298864">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.0.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329411" />
<state width="1876" height="161" key="GridCell.Tab.0.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438298864" />
<state width="1006" height="588" key="GridCell.Tab.1.bottom" timestamp="1597366506508">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1006" height="588" key="GridCell.Tab.1.bottom/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506508" />
<state width="1006" height="588" key="GridCell.Tab.1.center" timestamp="1597366506506">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1006" height="588" key="GridCell.Tab.1.center/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506506" />
<state width="1006" height="588" key="GridCell.Tab.1.left" timestamp="1597366506505">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1006" height="588" key="GridCell.Tab.1.left/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506505" />
<state width="1006" height="588" key="GridCell.Tab.1.right" timestamp="1597366506507">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state width="1006" height="588" key="GridCell.Tab.1.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597351329412" />
<state width="1006" height="588" key="GridCell.Tab.1.right/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597366506507" />
<state x="672" y="237" key="MultipleFileMergeDialog" timestamp="1597438068748">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="672" y="237" key="MultipleFileMergeDialog/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438068748" />
<state x="94" y="257" key="SettingsEditor" timestamp="1597361509050">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="-1040" y="568" key="SettingsEditor/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@-1050.105.1050.1640" timestamp="1597353858648" />
<state x="94" y="257" key="SettingsEditor/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597361509050" />
<state x="552" y="254" key="Vcs.Push.Dialog.v2" timestamp="1597438121430">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="552" y="254" key="Vcs.Push.Dialog.v2/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597438121430" />
<state x="616" y="240" key="run.anything.popup" timestamp="1597325088886">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="616" y="240" key="run.anything.popup/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597325088886" />
<state x="623" y="225" width="672" height="678" key="search.everywhere.popup" timestamp="1597363843473">
<screen x="0" y="0" width="1920" height="1040" />
</state>
<state x="623" y="225" width="672" height="678" key="search.everywhere.popup/0.0.1920.1040/1920.-213.2560.1400/-1050.105.1050.1640@0.0.1920.1040" timestamp="1597363843473" />
<MESSAGE value="Add service announcement channel" />
<MESSAGE value="fix crash on missing permission&#10;add removeInactive command" />
<MESSAGE value="prepare twitter integration" />
<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">
<watches-manager>
@@ -338,4 +485,10 @@
</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>

122
README.md
View File

@@ -1,82 +1,72 @@
# BlogShot
A bot that automatically polls the newest blogpost from [Hytale News Tab](https://www.hytale.com/news) and posts a message into servers if there is a new one.
## Setup
Okay, this isn't really meant for you to setup, if you want it though it first is easier to just dm me on Twitter [@tale_talk](https://twitter.com/tale_talk) so I can add you to the server list.
If you *really* want to set it up yourself, fine.
* first go to the release tab, download the jar, and put it in a folder
* Add two files in the root of the repo, an `admin.json` and a `servers.json`.
Add your Discord ID (not name), Bot token, and update frequency to the `admin.json`:
```json
{
"adminId": 12345678910,
"token": "AOGH@(AKnjsfjiJijaig3ijgG92jaij",
"updateMs":30000
}
```
* add your servers to `servers.json`
```json
[
{
"id": 15050067772322222,
"mentionedRole": "everyone",
"autoPublish":true
},
{
"id": 74050067772325222,
"mentionedRole": null,
"autoPublish":false
},
{
"id": 74050067772325222,
"mentionedRole": "74036067771625222",
"autoPublish":false
}
]
```
* add a `test.json` with the same schema as the `server.json`. When
you enable test mode, the servers from there will be used instead allowing
you to test if it works.
## Add to your server
Click [this](https://discord.com/api/oauth2/authorize?client_id=743447329901641799&permissions=150528&scope=bot) link to invite
the bot to your server. Please note that only people with *Administrator* permission will be able to
configure it.
You can type `%!info` to get an overview over all available commands.
## Commands
| **Command** | **Arguments** | **Info** |
|------------------|--------------------------------------------------------|---------------------------------------------------------------------------------------------------|
| %!add | | Add current channel to the notified list |
| %!remove | | Remove current channel to the notified list |
| %!publish | on &#124; off | [Community&#124;Partner&#124;Verified only] Auto publish the message if in an announcement channel |
| %!ping | none &#124; everyone &#124; roleName | What role to ping |
| %!setMessage | message | Set a custom message when a blogpost arrives |
| %!resetMessage | | Reset the custom message to none |
| %!serviceChannel | add &#124; remove | Add/remove channel from service notification list |
| %!publishMessage | on &#124; off | [Community&#124;Partner&#124;Verified only] Auto publish the custom message if in an announcement channel |
| %!info | | Show an overview about all channels registered on this server |
| %!report | Your message | Report an issue to the Bot Admin (this will share your user name so they can contact you) |
| %!help | | Show a help dialog with all these commands |
## Self Hosting
Okay, this isn't really meant for you to setup, but if you *really* want to set it up yourself, fine.
Go to the release tab, download the jar, and put it in a folder.
Start the server with `java -jar [server-file-name]` If you put in everything correctly,
the bot should message you on Discord.
*Note:* You need to invite the bot into a server before it can message you.
Run it once (it should crash or print an error), so `admin.json`, `servers.json` and `service_channels.json`
are being created.
Add your Discord ID `adminId` (not name), Bot token `token`, and update frequency `updateMs` to the `admin.json`,
optionally you can add your own messages for when the bot is looking and when it can't reach Hytale Servers.
If you verified that everything works correctly, you can start the server in the background, on Linux that is
`nohup java -Xmx1024m -jar [server-file-name]`. To stop it you can either type `!stop` in the Admin Console (Discord PM) or
if the bot is unresponsive the the PID of it through `ps -ef` and `kill [pid]`
I'm not 100% certain how much RAM the bot needs, default is typically `-Xmx256m`, and that lead to some issues, `-Xmx512m` is probably plenty, because my server has
tons of unused ram I set it to `-Xmx2048m`, just try and look what works for you.
## Compiling yourself
I developed it under Windows, and had some trouble compiling it on Linux. You mileage may vary.
## Admin commands
Start the server with `java -jar [server-file-name]` If you put in everything correctly, the bot should message you on Discord.
### Adding Servers
Please edit the JSON file.
You can force an update by calling
```
%!refreshList
```
### Testing
Switching between test and production files
```
%!testMode
%!fakeUpdate
```
```
%!productionMode
```
**WARNING**: Initiating a fake update is not being cancelled by switching
to production.
### Stop the server from within Discord
```
%!stop
```
### Show servers, channels and roles
```
%!info
```
| **Command** | **Arguments** | **Info** |
|------------------|-------|---------------------|
| !info | | Show all registered channels and servers. |
| !stop | | Stop the server (useful when running in `nohup`) |
| !serviceMessage | message | Send a service message to all registered channels |
| !fakeUpdate | | Cause a fake update (**WARNING**: This will show on **ALL** registered servers) |
| !refreshList | | Refresh servers and service channels from disk (if you manually edit the JSON files) |
| !removeInactive | | Remove inactive channels |
| !help | | Show a help dialog with all these commands |
These commands will work in every channel, but will be ignored if they don't come from you, however the bot will always respond in a private message.
It will also print errors directly in a Discord private message.
These commands will only work by private messaging the bot (and will be ignored if they don't
come from the admin registered in the `admin.json`.
## TODO
Mainly reaction roles for convenience, self setup on invite to server, Twitter integration.
Mainly reaction roles for convenience, Twitter integration to either be even faster or to brag how much faster
we were over the official Hytale Twitter.
## Other
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,33 @@
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'
id 'com.github.johnrengelman.shadow' version '7.0.0'
}
group 'de.wulkanat'
version '1.1'
group 'org.wulkanat'
version '2.0.1'
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,19 +39,7 @@ compileTestKotlin {
jar {
manifest {
attributes('Main-Class': 'de.wulkanat.MainKt')
}
}
task fatJar(type: Jar) {
baseName = project.name + '-all'
from((configurations.compile.findAll { !it.path.endsWith(".pom") }).collect {
it.isDirectory() ? it : zipTree(it)
})
with jar
manifest {
attributes 'Main-Class': 'de.wulkanat.MainKt',
'Implementation-Version': version
attributes('Main-Class': 'org.hmcore.Main')
}
}

Binary file not shown.

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

53
gradlew vendored
View File

@@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
@@ -66,6 +82,7 @@ esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
@@ -109,10 +126,11 @@ if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
@@ -138,19 +156,19 @@ if $cygwin ; then
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
i=`expr $i + 1`
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
@@ -159,14 +177,9 @@ save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

43
gradlew.bat vendored
View File

@@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +54,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,28 +64,14 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

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,142 +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
var testModeEnabled: Boolean = false
set(value) {
if (field == value)
return
field = value
if (value) {
jda?.presence?.setPresence(Activity.of(Activity.ActivityType.DEFAULT, "Testing mode, hold on..."), true)
} else {
jda?.presence?.setPresence(Activity.watching("for new Blogposts"), false)
}
Channels.channels = Channels.refreshFromDisk()
Admin.info()
}
init {
val admin = Json(JsonConfiguration.Stable).parse(AdminFile.serializer(), ADMIN_FILE.readText())
userId = admin.adminId
token = admin.token
updateMs = admin.updateMs
}
var jda: JDA? = null
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.")
}
}
private 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) {
sendDevMessage(
EmbedBuilder()
.setTitle(msg)
.setDescription(error)
.setColor(Color.RED)
.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"))
.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()
}
private 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,106 +0,0 @@
package de.wulkanat
import de.wulkanat.extensions.crosspost
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration
import kotlinx.serialization.list
import net.dv8tion.jda.api.JDA
import net.dv8tion.jda.api.Permission
import net.dv8tion.jda.api.entities.MessageEmbed
import net.dv8tion.jda.api.entities.TextChannel
object Channels {
var jda: JDA? = null
val json = Json(JsonConfiguration.Stable)
/**
* List of (ServerID, ChannelID)
*/
var channels: MutableList<DiscordChannel> = refreshFromDisk()
fun sentToAll(messageEmbed: MessageEmbed) {
if (jda == null)
return
for (channel_pair in channels) {
val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue
if (channel_pair.mentionedRole != null) {
val message = if (channel_pair.mentionedRole == "everyone") {
"New Blogpost @everyone"
} else {
"New Blogpost <@&${channel_pair.mentionedRole}>"
}
channel.sendMessage(message).queue()
}
channel.sendMessage(messageEmbed).queue {
if (channel_pair.autoPublish) {
it.crosspost().queue()
}
}
}
}
fun checkEveryonePermission() {
for (channel_pair in channels) {
val channel = jda!!.getTextChannelById(channel_pair.id) ?: continue
if (channel_pair.mentionedRole == "everyone" &&
channel.guild.selfMember.hasPermission(Permission.MESSAGE_MENTION_EVERYONE)
) {
Admin.warning("Cannot mention everyone on ${channel.guild.name}")
} else if (channel.guild.selfMember.hasPermission(Permission.MESSAGE_WRITE)) {
Admin.warning("Cannot send any messages on ${channel.guild.name}")
}
}
}
fun refreshFromDisk(): MutableList<DiscordChannel> {
return json.parse(
DiscordChannel.serializer().list, (if (Admin.testModeEnabled) {
TEST_FILE
} else {
SERVERS_FILE
}).readText()
).toMutableList()
}
fun getServerNames(): List<String> {
if (jda == null)
return listOf()
return channels.map {
val channel = jda!!.getTextChannelById(it.id)
if (channel == null) {
Admin.warning("Channel ${it.id} is no longer active!")
return@map "**${it.id}** *(inactive)*"
}
val role = when (it.mentionedRole) {
null -> ""
"everyone" -> " @everyone"
else -> " @${channel.guild.getRoleById(it.mentionedRole)?.name}"
}
"**${channel.guild.name}**\n#${channel.name}${role}"
}
}
fun testServerId(id: Long): TextChannel? {
return jda?.getTextChannelById(id)
}
fun addChannel(id: Long, role: String?) {
channels.add(DiscordChannel(id, role))
saveChannels()
}
private fun saveChannels() {
SERVERS_FILE.writeText(
json.stringify(
DiscordChannel.serializer().list,
channels
)
)
}
}

View File

@@ -1,50 +0,0 @@
package de.wulkanat
import de.wulkanat.model.BlogPostPreview
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import de.wulkanat.web.SiteWatcher
import net.dv8tion.jda.api.events.ExceptionEvent
import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent
import kotlin.system.exitProcess
class Cli : ListenerAdapter() {
override fun onPrivateMessageReceived(event: PrivateMessageReceivedEvent) {
val msg = event.message.contentRaw
if (event.author.idLong != Admin.userId ||
!msg.startsWith("!")
) {
return
}
val command = msg.removePrefix("!").split(Regex("\\s+"))
when (command[0]) {
"stop" -> exitProcess(1)
"fakeUpdate" -> {
SiteWatcher.newestBlog = BlogPostPreview(
title = "FakePost",
imgUrl = "",
fullPostUrl = "",
author = "wulkanat",
date = "now",
description = "Lorem Ipsum"
)
Admin.println("Posting on next update cycle.")
}
"info" -> {
Admin.info()
}
"refreshList" -> {
Channels.channels = Channels.refreshFromDisk()
Admin.info()
}
"testMode" -> {
Admin.testModeEnabled = true
}
"productionMode" -> {
Admin.testModeEnabled = false
}
}
}
}

View File

@@ -1,22 +0,0 @@
package de.wulkanat
import kotlinx.serialization.Serializable
import java.io.File
@Serializable
data class DiscordChannel(
val id: Long,
val mentionedRole: String? = null,
val autoPublish: Boolean = false
)
@Serializable
data class AdminFile(
val adminId: Long,
val token: String,
val updateMs: Long
)
val SERVERS_FILE = File("servers.json")
val TEST_FILE = File("test.json")
val ADMIN_FILE = File("admin.json")

View File

@@ -1,38 +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("for new Blogposts"))
.build()
builder.addEventListener(Cli())
builder.addEventListener(ErrorHandler())
builder.awaitReady()
Channels.jda = builder
Admin.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,37 +0,0 @@
package de.wulkanat.web
import de.wulkanat.Admin
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
fun hasNewBlogPost(): Boolean {
Admin.silent("Updating...")
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)
return false
}
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

@@ -0,0 +1,94 @@
package org.hmcore
import org.hmcore.web.fakeUpdateBlogPost
import org.hmcore.web.fakeUpdateJobListings
import net.dv8tion.jda.api.hooks.ListenerAdapter
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.events.message.priv.PrivateMessageReceivedEvent
import java.awt.Color
import kotlin.system.exitProcess
class AdminCli : ListenerAdapter() {
val prefix = "!"
override fun onPrivateMessageReceived(event: PrivateMessageReceivedEvent) {
val msg = event.message.contentRaw
if (event.author.idLong != Admin.userId ||
!msg.startsWith(prefix)
) {
return
}
val command = Regex("[^\\s`]+|`[^`]*`").findAll(msg.removePrefix("!")).toList()
when (command[0].value) {
"stop" -> exitProcess(1)
"fakeUpdate" -> {
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 $amount on next update cycle.")
}
}
"info" -> {
Admin.info()
}
"serviceMessage" -> {
if (command.size != 3) {
Admin.println("Enclose message and title in backticks (`)")
} else {
Channels.sendServiceMessage(command[1].value.trim('`'), command[2].value.trim('`'))
}
}
"refreshList" -> {
Channels.channels = Channels.refreshChannelsFromDisk()
Channels.serviceChannels = Channels.refreshServiceChannelsFromDisk()
Admin.info()
}
"removeInactive" -> {
Channels.channels.removeAll { channel ->
Channels.testServerId(channel.id) ?: run {
Admin.println("Removed ${channel.id}")
null
} == null
}
Admin.info()
Channels.saveChannels()
}
"help" -> {
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Help")
.setColor(Color.YELLOW)
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.setDescription(
"""
**${prefix}stop**
Stop the bot
**${prefix}fakeUpdate [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
**${prefix}serviceMessage [title] [message]**
Show a service message (update info etc) to all registered service channels
**${prefix}refreshList**
Refresh server list from disk
**${prefix}removeInactive**
Remove inactive channels
**${prefix}help**
Show this message
""".trimIndent()
)
.build()
).queue()
}
}
}
}

View File

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

@@ -0,0 +1,263 @@
package org.hmcore
import net.dv8tion.jda.api.EmbedBuilder
import net.dv8tion.jda.api.Permission
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import java.awt.Color
class OwnerCli : ListenerAdapter() {
private val prefix = "%!"
override fun onMessageReceived(event: MessageReceivedEvent) {
val msg = event.message.contentRaw
// Only accept admin requests
if (event.message.member?.hasPermission(Permission.ADMINISTRATOR) != true || !msg.startsWith(prefix)) {
return
}
val command = msg.removePrefix(prefix).split(Regex("\\s+"))
val channelId = event.message.channel.idLong
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" -> {
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 {
event.message.channel.sendMessage("Added.").queue()
Admin.info()
}
}
"remove" -> {
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()
} else {
event.message.channel.sendMessage("This channel is not registered.").queue()
}
}
"publish" -> {
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 > 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 [type] [on|off]`")
}
} else {
event.message.channel.sendMessage("Channel not registered.").queue()
}
}
"ping" -> {
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 > 2) {
val roles = event.message.guild.getRolesByName(command[2], false)
result.mentionedRole = when {
command[2] == "everyone" -> {
event.message.channel.sendMessage("Now pinging everyone.").queue()
"everyone"
}
command[2] == "none" -> {
event.message.channel.sendMessage("Now pinging none.").queue()
null
}
roles.firstOrNull() != null -> {
event.message.channel.sendMessage("Now pinging ${roles.first().name}").queue()
roles.first().id
}
else -> {
event.message.channel.sendMessage("Unknown role.").queue()
result.mentionedRole
}
}
Channels.saveChannels()
} else {
event.message.channel.sendMessage("Usage: `${prefix}ping [type] [everyone|none|roleName]`")
}
} else {
event.message.channel.sendMessage("Channel is not registered.").queue()
}
}
"setMessage" -> {
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 > 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 [type] [message]`")
}
} else {
event.message.channel.sendMessage("Channel is not registered.").queue()
}
}
"resetMessage" -> {
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()
event.message.channel.sendMessage("Reset to no message.").queue()
} else {
event.message.channel.sendMessage("Channel is not registered.").queue()
}
}
"serviceChannel" -> {
if (command.size > 1 && listOf("add", "remove").contains(command[1])) {
if (command[1] == "add") {
if (Channels.serviceChannels.find { it.id == channelId } != null) {
event.message.channel.sendMessage("Already a service channel.").queue()
} else {
Channels.serviceChannels.add(ServiceChannel(channelId))
Channels.saveChannels()
event.message.channel.sendMessage("Added as service channel.").queue()
}
} else {
event.message.channel.sendMessage(
if (Channels.serviceChannels.removeAll { it.id == channelId }) "Channel removed."
else "Not a service channel."
).queue()
}
Channels.saveChannels()
} else {
event.message.channel.sendMessage("Usage: `${prefix}serviceChannel [add|remove]`")
}
}
"publishMessage" -> {
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 > 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 [type] [on|off]`")
}
} else {
event.message.channel.sendMessage("Channel has no custom message.").queue()
}
} else {
event.message.channel.sendMessage("Channel not registered.").queue()
}
}
"info" -> {
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Server overview")
.setColor(Color.GREEN)
.setDescription("""
${Channels.getServerNames(event.message.guild.idLong).joinToString("\n")}
**_Service Channels_**
${Channels.getServiceChannelServers(event.message.guild.idLong).joinToString("\n")}
""".trimIndent())
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.build()
).queue()
}
"report" -> {
val errorReport = event.message.contentRaw.removePrefix("${prefix}report")
Admin.error(event.message.guild.name, errorReport, event.author)
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Error Report Received")
.setColor(Color.RED)
.setDescription(errorReport)
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.build()
).queue()
}
"help" -> {
event.message.channel.sendMessage(
EmbedBuilder()
.setTitle("Help")
.setColor(Color.YELLOW)
.setAuthor(Admin.admin?.name, Admin.admin?.avatarUrl, Admin.admin?.avatarUrl)
.setDescription(
"""
**${prefix}add [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 [type]**
Remove this channel to the notified list
**${prefix}publish [type] [on|off]**
[Community|Partner|Verified only] Auto publish the message if in an announcement channel
**${prefix}ping [type] [none|everyone|roleName]**
What role to ping
**${prefix}setMessage [type] [message]**
Set a custom message to show
**${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**
Show this message
""".trimIndent()
)
.build()
).queue()
}
}
}
}

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

@@ -0,0 +1,11 @@
package org.hmcore.extensions
import java.io.File
fun File.ensureExists(defaultText: String? = null): File {
if (!this.exists()) {
this.createNewFile()
this.writeText(defaultText ?: return this)
}
return this
}

View File

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