commit a43a0daa7134682da94f4f01435eccb1fdd0aafe Author: yl60lepu Date: Thu Mar 25 00:18:50 2021 +0100 Initial Commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..224195b --- /dev/null +++ b/.gitignore @@ -0,0 +1,200 @@ +# ---> Java +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +.idea + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# ---> Eclipse +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +.project + +# ---> Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# ---> JetBrains +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ +.classpath + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +# ---> Gradle +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +updateYoshiBot.sh + +# Resources +rsc/audio/* +!rsc/audio/.gitkeep +rsc/PrivateJdaBuilderString.txt +rsc/RedditCredentials.properties \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..736ad71 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# YoshiBot # + +Ein in Java geschriebener Discordbot, der lustige Sachen kann. + +Das Git-Projekt befindet sich unter http://yannicpunktdee.de:3000/yannic/YoshiBot.git. + +## Einrichtung ## + +Der Bot ist hauptsächlich in Java geschrieben mit Gradle als Build-Tool. +Zusätzlich wird Python3 mit dem Paket gTTS verwendet für alles was mit Text-To-Speech zu tun hat. +Für die Annotationen wie ```@Getter``` oder ```@SneakyThrows``` wird das Lombok-Plugin für Intellij benötigt. +Mit Gradle sind die Bibliotheken JDA für die Discord-Schnittstelle (https://github.com/DV8FromTheWorld/JDA) und +LavaPlayer (https://github.com/sedmelluq/lavaplayer) für Audioangelegenheiten einzubinden. Dies ist aber bereits im +Gradle build script (```app/build.gradle```) enthalten. + +Als Entwicklungsumgebung eignet sich z.B. IntelliJ IDEA. + +Zum Einrichten einfach das Projekt mit Git clonen und in Intellij öffnen. + +Damit sich der Bot online schalten kann wird noch die Datei ```rsc/PrivateJdaBuilderString.txt``` benötigt, die den +Geheimschlüssel der Discord-Anwendung enthält. Diesen bekommt man mitgeteilt, sobald man eine Discord-Anwendung erstellt +über https://discord.com/developers/applications und der Bot auf den Server eingeladen wurde. + +Weiterhin muss die Guild-ID der Server-Guild in der ```Config.properties``` angepasst werden. + +Für Pfadangaben in der ```Config.properties``` achte darauf, dass diese Ordner auch existieren und dass Verzeichnisse auch unter +Windows mit ```/``` und nicht mit ```\ ``` geschrieben werden und darauf enden. + +Nun nur noch die Main-Methode starten oder wie folgt builden und ausführen und der Bot läuft. + +**Warnung** Der Bot benutzt das temp-Verzeichnis (Ordner ```yoshibot```) des Rechners, auf dem er ausgeführt wird und legt dort evtl viele Dateien +ab, die er selbst nicht löscht. Also entweder muss das Verzeichnis von Zeit zu Zeit geleert oder der Rechner neu gestartet werden. + +## Build ## + +Zum Exportieren der Applikation führe den Befehl ```gradlew clean build``` auf Windows oder für Linux ```gradle clean build``` +im Root Verzeichnis aus. Die fertige Jar liegt dann in ```app/build/libs```. Füge dieser noch im selben Verzeichnis den +rsc-Ordner mitsamt Inhalt hinzu. Starte die Applikation mit ```java-jar app.jar``` am besten im Hintergrund oder per ```screen``` +unter Linux, damit der Bot permanent läuft. diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..615b995 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,55 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Java application project to get you started. + * For more details take a look at the 'Building Java & JVM projects' chapter in the Gradle + * User Manual available at https://docs.gradle.org/6.8.3/userguide/building_java_projects.html + */ + +plugins { + // Apply the application plugin to add support for building a CLI application in Java. + id 'application' +} + +repositories { + // Use JCenter for resolving dependencies. + jcenter() +} + +dependencies { + // Use JUnit test framework. + testImplementation 'junit:junit:4.13' + + // This dependency is used by the application. + implementation 'com.google.guava:guava:29.0-jre' + + implementation group: 'org.json', name: 'json', version: '20210307' + + implementation 'net.dv8tion:JDA:4.2.0_247' + + implementation 'com.sedmelluq:lavaplayer:1.3.73' + + implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.0' + implementation 'org.apache.commons:commons-text:1.9' + + implementation "net.dean.jraw:JRAW:1.1.0" + + compileOnly 'org.projectlombok:lombok:1.18.16' + annotationProcessor 'org.projectlombok:lombok:1.18.16' +} + +mainClassName = 'de.yannicpunktdee.yoshibot.main.Main' + +application { + // Define the main class for the application. + mainClass = "$mainClassName" +} + +jar { + manifest { + attributes "Main-Class": "$mainClassName" + } + from { + configurations.runtimeClasspath.findAll({ !it.path.endsWith(".pom") }).collect { it.isDirectory() ? it : zipTree(it) } + } +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioLoadResultHandlerImpl.java b/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioLoadResultHandlerImpl.java new file mode 100644 index 0000000..d61fb25 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioLoadResultHandlerImpl.java @@ -0,0 +1,33 @@ +package de.yannicpunktdee.yoshibot.audio; + +import com.sedmelluq.discord.lavaplayer.player.AudioLoadResultHandler; +import com.sedmelluq.discord.lavaplayer.tools.FriendlyException; +import com.sedmelluq.discord.lavaplayer.track.AudioPlaylist; +import com.sedmelluq.discord.lavaplayer.track.AudioTrack; + +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import de.yannicpunktdee.yoshibot.utils.Logger; + +public class AudioLoadResultHandlerImpl implements AudioLoadResultHandler { + + @Override + public void trackLoaded(AudioTrack track) { + YoshiBot.getInstance().audioPlayer.playTrack(track); + } + + @Override + public void playlistLoaded(AudioPlaylist playlist) { + Logger.logWarning("Aktuell kann noch keine Playlist abgespielt werden."); + } + + @Override + public void noMatches() { + Logger.logError("Nothing found"); + } + + @Override + public void loadFailed(FriendlyException exception) { + Logger.logError("Loading failed"); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioPlayerListener.java b/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioPlayerListener.java new file mode 100644 index 0000000..67df1f8 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioPlayerListener.java @@ -0,0 +1,31 @@ +package de.yannicpunktdee.yoshibot.audio; + +import com.sedmelluq.discord.lavaplayer.player.event.AudioEvent; +import com.sedmelluq.discord.lavaplayer.player.event.AudioEventListener; + +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import lombok.Getter; +import net.dv8tion.jda.api.managers.AudioManager; + +public class AudioPlayerListener implements AudioEventListener { + + private final AudioManager audioManager; + + @Getter + private static boolean isPlayingTrack = false; + + + public AudioPlayerListener(AudioManager audioManager) { + this.audioManager = audioManager; + } + + @Override + public void onEvent(AudioEvent event) { + if(event.player.getPlayingTrack() == null) { + event.player.stopTrack(); + isPlayingTrack = false; + YoshiBot.getInstance().joinVoiceChannelWithMostMembers(); + } else isPlayingTrack = true; + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioSendHandlerImpl.java b/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioSendHandlerImpl.java new file mode 100644 index 0000000..649f9b8 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/audio/AudioSendHandlerImpl.java @@ -0,0 +1,39 @@ +package de.yannicpunktdee.yoshibot.audio; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.track.playback.AudioFrame; +import net.dv8tion.jda.api.audio.AudioSendHandler; + +import java.nio.ByteBuffer; + +public class AudioSendHandlerImpl implements AudioSendHandler { + + private final AudioPlayer audioPlayer; + private AudioFrame lastFrame; + + + public AudioSendHandlerImpl(AudioPlayer audioPlayer) { + this.audioPlayer = audioPlayer; + } + + // + @Override + public boolean canProvide() { + lastFrame = audioPlayer.provide(); + return lastFrame != null; + } + + @Override + public ByteBuffer provide20MsAudio() { + return ByteBuffer.wrap(lastFrame.getData()); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isOpus() { + return true; + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommand.java new file mode 100644 index 0000000..5c71097 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommand.java @@ -0,0 +1,177 @@ +package de.yannicpunktdee.yoshibot.command; + +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import de.yannicpunktdee.yoshibot.utils.Logger; +import de.yannicpunktdee.yoshibot.utils.Resources; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Message.Attachment; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.VoiceChannel; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +/** + * Abstrakte Superklasse für alle Kommandos. + * + * @author Yannic Link + */ +public abstract class YoshiCommand { + + protected final String[] requiredArguments = {}; + + /** + * Der Kontext mit dem das Kommando aufgerufen wurde. + */ + protected YoshiCommandContext context; + + public static String resourceToDelete = null; + + + /** + * Erzeugt ein neues Kommando, führt es aber noch nicht aus. Es wird ermittelt, ob die Argumentenkombination valide + * ist und das isOk-Flag gesetzt. Im Fehlerfall wird eine Fehleremeldung spezifiziert. + * + * @param context Der Kontext mit dem das Kommando aufgerufen wurde. + */ + public YoshiCommand(YoshiCommandContext context) { + this.context = context; + } + + /** + * Führt das Kommando aus. + * + * @return True, wenn Ausführung erfolgreich. False, wenn Ausführung fehlgeschlagen. Fehlermeldung wird in + * errorMessage spezifiziert. + */ + public boolean execute() { + if (!context.containsArguments(requiredArguments)) { + sendErrorMessage("Fehlende Argumente"); + return false; + } + return true; + } + + protected final void sendMessage(String message) { + EmbedBuilder eb = new EmbedBuilder(); + eb.setColor(Color.pink); + eb.setDescription(message); + context.getEvent().getTextChannel().sendMessage(eb.build()).queue(); + } + + protected final void sendFile(File file, String description) { + EmbedBuilder eb = new EmbedBuilder(); + eb.setColor(Color.pink); + if (description != null) eb.setDescription(description); + context.getEvent().getTextChannel().sendFile(file).queue(); + } + + protected final void sendInfoMessage(String message) { + EmbedBuilder eb = new EmbedBuilder(); + eb.setTitle("INFO"); + eb.setColor(Color.blue); + eb.setDescription(message); + context.getEvent().getTextChannel().sendMessage(eb.build()).queue(); + } + + protected final void sendErrorMessage(String message) { + EmbedBuilder eb = new EmbedBuilder(); + eb.setTitle("ERROR"); + eb.setColor(Color.red); + eb.setDescription(message); + context.getEvent().getTextChannel().sendMessage(eb.build()).queue(); + } + + protected final void sendCustomMessage(MessageEmbed messageEmbed) { + context.getEvent().getTextChannel().sendMessage(messageEmbed).queue(); + } + + + protected File downloadAttachmentToFile(String directoryPath, String name) { + if (directoryPath == null) directoryPath = Resources.getEnsuredTempPath(); + if (name == null) name = UUID.randomUUID().toString(); + + if (!(new File(directoryPath)).isDirectory()) { + Logger.logError("Das Download-Verzeichnis wurde nicht gefunden."); + sendErrorMessage("Der Anhang konnte nicht gedownloaded werden."); + return null; + } + + List attachments = context.getEvent().getMessage().getAttachments(); + if (attachments.size() == 0) { + return null; + } + Attachment attachment = attachments.get(0); + + File file = new File(directoryPath + name + "." + attachment.getFileExtension()); + CompletableFuture future = attachment.downloadToFile(file); + future.exceptionally(e -> { + sendErrorMessage("Ein Anhang konnte nicht gedownloaded werden."); + return null; + }); + try { + future.get(); + } catch (InterruptedException | ExecutionException e) { + sendErrorMessage("Ein Anhang konnte nicht gedownloaded werden."); + return null; + } + if (!file.exists()) { + sendErrorMessage("Ein Anhang konnte nicht gedownloaded werden."); + return null; + } + + if (file.getAbsolutePath().endsWith(".mp3")) { + String newFilePath = file.getAbsolutePath().substring(0, file.getAbsolutePath().length() - 3) + "opus"; + Runtime rt = Runtime.getRuntime(); + try { + String command = "/usr/bin/ffmpeg -y -i " + file.getAbsolutePath() + " " + newFilePath; + Process pr = rt.exec(command); + String err = new BufferedReader(new InputStreamReader(pr.getErrorStream())).lines() + .collect(Collectors.joining()); + String out = new BufferedReader(new InputStreamReader(pr.getInputStream())).lines().collect( + Collectors.joining()); + int exit = pr.waitFor(); + if (!file.delete()) { + throw new IOException("Delete ging nich"); + } + return new File(newFilePath); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + return file; + } + + protected VoiceChannel getVoiceChannelByParam() { + VoiceChannel vc; + if (context.containsArguments(new String[]{"channel"})) { + if (context.getArgument("channel") == null) return null; + List channels = YoshiBot.getInstance().jda + .getVoiceChannelsByName(context.getArgument("channel"), true); + if (!(channels.size() > 0)) { + sendErrorMessage("Der Kanalname konnte nicht gefunden werden."); + return null; + } + vc = channels.get(0); + } else { + try { + vc = context.getEvent().getMember().getVoiceState().getChannel(); + if (vc == null) vc = YoshiBot.getInstance().getGuild().getAudioManager().getConnectedChannel(); + } catch (Exception e) { + sendErrorMessage( + "Es konnte kein Voicekanal gefunden werden in dem die Audio-Datei abgespielt werden kann."); + return null; + } + } + return vc; + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommandContext.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommandContext.java new file mode 100644 index 0000000..b8c5eb1 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommandContext.java @@ -0,0 +1,305 @@ +package de.yannicpunktdee.yoshibot.command; + +import java.util.*; + +import de.yannicpunktdee.yoshibot.command.YoshiCommandDistributor.Action; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent;; + +/** + * Parst einen Eingabestring, entscheidet ob er ein Kommando ist und zerlegt ihn in seine Bestandteile. Kommandos + * besitzen einen Status, der Auskunft gibt, ob Fehler beim Parsen des Eingabestrings aufgetreten sind oder der String + * gar kein Kommando war. Außerdem wird bei erfolgreich geparsten Kommandos eine Aktion festgelegt, die das Kommando + * ausführen soll, sowie die Argumente mit welcher das Kommando ausgeführt wird. Außerdem enthalten ist eine Referenz + * auf Ursprungs-User und -TextChannel. + * + * @author Yannic Link + */ +public class YoshiCommandContext { + + /** + * Repräsentiert einen Status, den eine YoshiCommand-Instanz hat, nachdem der Eingabestring eingelesen und geparst + * wurde. Sofern der Status nicht OK ist, ist diese Instanz entweder kein Kommando oder besitzt eine fehlerhafte + * Syntax. + */ + public enum State { + /** + * Das Kommando wurde erfolgreich geparst und besitzt keine Syntaxfehler. + */ + OK, + /** + * Der Eingabestring war kein Kommando für den Yoshi-Bot. + */ + NO_COMMAND, + /** + * Im Kommando wurde keine Aktion spezifiziert. + */ + NO_ACTION, + /** + * Die angegebene Aktion existiert nicht. + */ + UNKNOWN_ACTION, + /** + * Das Kommando hatt einen Syntaxfehler. + */ + BAD_SYNTAX + } + + /** + * Hilfskonstrukt. Beschreibt den Zustand vom Parser. + */ + private enum ReadingState { + /** + * Der Parser ist dabei festzustellen, ob es sich um ein Kommando handelt. + */ + VERIFYING, + /** + * Zwischenzustand zwischen VERIFYING und READING_ACTION. + */ + AFTER_VERIFY, + /** + * Der Parser liest die Action ein. + */ + READING_ACTION, + /** + * Zustand nach READING_ACTION, READING_VALUE oder READING_STRING). + */ + INTERMEDIATE, + /** + * Der Parser ist dabei einen Argumentenkey einzulesen. + */ + READING_KEY, + /** + * Der Parser hat gerade einen Argumentenkey eingelesen. + */ + AFTER_KEY, + /** + * Der Parser liest gerade einen Argumentenwert ein. + */ + READING_VALUE, + /** + * Der Parser liest gerade einen zusammenhängenden Argumentenwert ein. + */ + READING_STRING + } + + ; + + + /** + * Das Präfix, mit dem Yoshi-Bot-Kommandos beginnen. + */ + public static final String PREFIX = "::yoshi"; + + /** + * Der (Fehler-)Status, den der Parser nach Einlesen des Eingabestrings angenommen hat. + */ + private State state; + /** + * Die im Eingabesting spezifizierte Aktion. + */ + private Action action; + /** + * Eine Map, die die Key-Werte der Argumente (ohne Bindestrich) auf dessen Werte abbildet. + */ + private Map arguments; + + protected List argumentList; + + private MessageReceivedEvent event; + + + /** + * Erzeugt aus einem unbearbeiteten Eingabestring ein Kommando. Nach dem parsen enthält die state-Variable den + * Endzustand des Parsers. + * + * @param argumentsString Ein unbearbeiteter Eingabestring + */ + public YoshiCommandContext(String argumentsString, MessageReceivedEvent event) { + this.event = event; + + argumentsString = argumentsString.trim(); + + argumentList = new ArrayList<>(); + argumentList.addAll(Arrays.asList(argumentsString.split(" "))); + argumentList.remove(0); + + arguments = new HashMap<>(); + + ReadingState readingState = ReadingState.VERIFYING; + String currentKey = null; + int startPos = 0; + int length = argumentsString.length(); + + for (int position = 0; position < length; position++) { + char currentChar = argumentsString.charAt(position); + + switch (readingState) { + case VERIFYING: + if (!Character.isWhitespace(currentChar)) continue; + if (!argumentsString.substring(0, position).equals(PREFIX)) { + state = State.NO_COMMAND; + return; + } + readingState = ReadingState.AFTER_VERIFY; + continue; + case AFTER_VERIFY: + if (Character.isWhitespace(currentChar)) continue; + if (currentChar == '-') { + state = State.NO_ACTION; + return; + } + startPos = position; + readingState = ReadingState.READING_ACTION; + continue; + case READING_ACTION: + if (!Character.isWhitespace(currentChar)) continue; + try { + action = Action.valueOf(argumentsString.substring(startPos, position).toUpperCase()); + readingState = ReadingState.INTERMEDIATE; + } catch (IllegalArgumentException e) { + state = State.UNKNOWN_ACTION; + return; + } + continue; + case INTERMEDIATE: + if (Character.isWhitespace(currentChar)) continue; + if (currentChar != '-' || currentChar == '"') { + state = State.BAD_SYNTAX; + return; + } + startPos = position + 1; + readingState = ReadingState.READING_KEY; + continue; + case READING_KEY: + if (!Character.isWhitespace(currentChar)) continue; + currentKey = argumentsString.substring(startPos, position); + readingState = ReadingState.AFTER_KEY; + continue; + case AFTER_KEY: + if (Character.isWhitespace(currentChar)) continue; + if (currentChar == '-') { + arguments.put(currentKey, null); + startPos = position + 1; + readingState = ReadingState.READING_KEY; + } else if (currentChar == '"') { + startPos = position + 1; + readingState = ReadingState.READING_STRING; + } else { + startPos = position; + readingState = ReadingState.READING_VALUE; + } + continue; + case READING_VALUE: + if (!Character.isWhitespace(currentChar)) continue; + arguments.put(currentKey, argumentsString.substring(startPos, position)); + readingState = ReadingState.INTERMEDIATE; + continue; + case READING_STRING: + if (currentChar != '"') continue; + if (argumentsString.charAt(position - 1) == '\\') continue; + arguments.put(currentKey, argumentsString.substring(startPos, position)); + readingState = ReadingState.INTERMEDIATE; + continue; + } + } + + switch (readingState) { + case INTERMEDIATE: + state = State.OK; + return; + case VERIFYING: + if (argumentsString.equals(PREFIX)) { + action = Action.HELP; + state = State.OK; + } else { + state = State.NO_COMMAND; + } + return; + case READING_ACTION: + try { + action = Action.valueOf(argumentsString.substring(startPos).toUpperCase()); + readingState = ReadingState.INTERMEDIATE; + } catch (IllegalArgumentException e) { + state = State.UNKNOWN_ACTION; + return; + } + state = State.OK; + return; + case READING_KEY: + arguments.put(argumentsString.substring(startPos), null); + state = State.OK; + return; + case READING_VALUE: + arguments.put(currentKey, argumentsString.substring(startPos)); + state = State.OK; + return; + default: + break; + } + } + + /** + * Prüft, ob der Eingabestring ein valides Kommando war. + */ + public boolean isValid() { + return state.equals(State.OK); + } + + /** + * Gibt den (Fehler-)Status des Parsers zurück. + */ + public State getState() { + return state; + } + + /** + * Gibt die im Kommando spezifizierte Aktion zurück. null, wenn status fehlerhaft oder kein Kommando. + */ + public Action getAction() { + return action; + } + + /** + * Prüft, ob das Kommando mit Argumenten aufgerufen wurde. + */ + public boolean hasArguments() { + return !arguments.isEmpty(); + } + + + public boolean containsArgument(String arg){ + return this.containsArguments(new String[]{arg}); + } + /** + * Prüft, ob alle Key-Werte in der Argumentenliste vorhanden sind. + * + * @param args Liste von den auf Existenz zu überprüfenden Argumenten. + */ + public boolean containsArguments(String[] args) { + for (String arg : args) { + if (!arguments.containsKey(arg)) { + return false; + } + } + return true; + } + + /** + * Gibt den Wert eines Arguments zurück. + * + * @param arg Name des Arguments. + */ + public String getArgument(String arg) { + if (!arguments.containsKey(arg)) return null; + return arguments.get(arg); + } + + public MessageReceivedEvent getEvent() { + return event; + } + + public List getArguments() { + return this.argumentList; + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommandDistributor.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommandDistributor.java new file mode 100644 index 0000000..1c70863 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/YoshiCommandDistributor.java @@ -0,0 +1,111 @@ +package de.yannicpunktdee.yoshibot.command; + +import de.yannicpunktdee.yoshibot.command.commands.*; + +/** + * Unterscheidet nach der spezifizierten Action welche YoshiCommand-Kindklasse zum Ausführen des Kommandos verwendet + * wird. + * + * @author Yannic Link + */ +public class YoshiCommandDistributor { + + /** + * Führt das jeweils zuständige Kommando aus. + * + * @param context + */ + public static void distribute(YoshiCommandContext context) { + switch (context.getState()) { + case NO_ACTION: + context.getEvent().getTextChannel() + .sendMessage("Im letzten Befehl wurde keine Aktion spezifiziert. Führe \"" + + YoshiCommandContext.PREFIX + " help\" für Hilfe aus.").queue(); + return; + case UNKNOWN_ACTION: + context.getEvent().getTextChannel() + .sendMessage("Im letzten Befehl wurde eine unbekannte Aktion angegeben. Führe \"" + + YoshiCommandContext.PREFIX + " help\" für Hilfe aus.").queue(); + return; + case BAD_SYNTAX: + context.getEvent().getTextChannel().sendMessage("Der letzte Befehl hatte ein falsches Format. Führe \"" + + YoshiCommandContext.PREFIX + " help\" für Hilfe aus.") + .queue(); + return; + default: + break; + } + + YoshiCommand command = null; + switch (context.getAction()) { + case HELP: + command = new HelpCommand(context); + break; + case JOKE: + command = new JokeCommand(context); + break; + case SAY: + command = new SayCommand(context); + break; + case PLAY: + command = new PlayCommand(context); + break; + case SAUCE: + command = new SauceCommand(context); + break; + case PAT: + command = new PatCommand(context); + break; + case BONK: + command = new BonkCommand(context); + break; + case WIKIPEDIA: + command = new WikipediaCommand(context); + break; + default: + context.getEvent().getTextChannel().sendMessage("Dieses Kommando existiert noch nicht.").queue(); + break; + } + + if (command != null) command.execute(); + } + + /** + * Enth�lt alle m�glichen Aktionen, die der Yoshi-Bot ausf�hren kann. + * + * @author Yannic Link + */ + public enum Action { + /** + * Sende eine Hilfe-Nachricht, in der die Benutzung des Yoshi-Bots dokumentiert ist. + */ + HELP, + /** + * Erzählt einen Jokus. + */ + JOKE, + /** + * Gib die Nachricht -message aus. �ber die Option -out [text|voice] wird angegeben, ob die Nachricht per + * Textnachricht oder als Text-To-Speech ausgegeben wird. Mit -channel l�sst sich der Ausgabechannel bestimmen. + * Standardm��ig wird die Ausgabe in den Textchannel zur�ckgesendet, aus dem das Kommando kam. + */ + SAY, + /** + * Gibt eine vorhandene Ressource -name aus. (Vorhandene Ressourcen lassen sich mit der Aktion LIST anzeigen). + * �ber den Parameter -type [link|audio|video] l�sst sich der Typ der Ressource spezifizieren. Ein Link wird + * �ber in den per -channel spezifizierten (default=Ursprungskanal) Textkanal geschickt. Eine Audiodatei wird + * �ber den per -channel spezifizierten (default=Aktueller Kanal) Voice-Channel ausgegeben. Ein Video wird �ber + * den per -channel spezifizierten (default=Aktueller Kanal) Voice-Channel abgespielt. + */ + PLAY, + /** + * L�scht die Ressource, die �ber -name spezifiziert wurde. Mit -type wird der Ressourcentyp festgelegt. + */ + SAUCE, + PAT, + BONK, + WIKIPEDIA + } + + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/BonkCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/BonkCommand.java new file mode 100644 index 0000000..56c563b --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/BonkCommand.java @@ -0,0 +1,59 @@ +package de.yannicpunktdee.yoshibot.command.commands; + +import de.yannicpunktdee.yoshibot.command.YoshiCommand; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.utils.GifSequenceWriter; +import de.yannicpunktdee.yoshibot.utils.Resources; + +import javax.imageio.ImageIO; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class BonkCommand extends YoshiCommand { + + public BonkCommand(YoshiCommandContext context) { + super(context); + } + + @Override + public boolean execute() { + if(!super.execute()) return false; + + File outFile = new File(Resources.getEnsuredTempPath() + UUID.randomUUID().toString() + ".gif"); + + try { + BufferedImage inPicture = ImageIO.read(downloadAttachmentToFile(null, null)); + BufferedImage bonk1Picture = ImageIO.read(new File(Resources.getBonkPngPath() + "bonk1.png")); + BufferedImage bonk2Picture = ImageIO.read(new File(Resources.getBonkPngPath() + "bonk2.png")); + BufferedImage frame1 = getOutFrame(bonk1Picture, inPicture, 615, 155, 185, 345); + ImageOutputStream output = new FileImageOutputStream(outFile); + GifSequenceWriter writer = new GifSequenceWriter(output, frame1.getType(), 100, true); + writer.writeToSequence(frame1); + writer.writeToSequence(getOutFrame(bonk2Picture, inPicture, 455, 155, 345, 345)); + writer.close(); + output.close(); + } catch (IOException e) { + sendErrorMessage("GIF konnte nicht erstellt werden."); + return false; + } + + sendFile(outFile, null); + + return true; + } + + private BufferedImage getOutFrame(BufferedImage bonkDogGraphics, BufferedImage personGraphics, int x, int y, int width, int height){ + BufferedImage outFrame = new BufferedImage(800, 500, BufferedImage.TYPE_INT_RGB); + Graphics2D g = outFrame.createGraphics(); + g.setColor(Color.black); + g.drawImage(personGraphics, x, y, width, height, null); + g.drawImage(bonkDogGraphics, 0, 0, 680, 412, null); + return outFrame; + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/HelpCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/HelpCommand.java new file mode 100644 index 0000000..283db4e --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/HelpCommand.java @@ -0,0 +1,50 @@ +package de.yannicpunktdee.yoshibot.command.commands; + +import de.yannicpunktdee.yoshibot.command.YoshiCommand; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import net.dv8tion.jda.api.EmbedBuilder; + +public class HelpCommand extends YoshiCommand { + + public HelpCommand(YoshiCommandContext context) { + super(context); + } + + @Override + public boolean execute() { + if (!super.execute()) return false; + + EmbedBuilder eb = new EmbedBuilder(); + eb.setTitle("So kannst du den Yohsi-Bot benutzen:"); + eb.setDescription("W\u00e4hle einen Befehl aus der folgenden Liste und ersetze die ``hervorgehobenen`` Ausdr\u00fccke " + + "durch die jeweiligen Werte. Enth\u00e4lt ein Wert Leerzeichen muss er in doppelten Anf\u00fchrungszeichen stehen. " + + "Die Befehle funktionieren nur im bot-muell Textchannel und der sauce-Befehl nur im schrein-auf-den-bot.\n[Link zum Projekt](http://yannicpunktdee.de:3000/yannic/YoshiBot.git)"); + eb.addField("::yoshi help", "Zeigt genau diesen Hilfetext an.", false); + eb.addField("::yoshi joke", "Schreibt einen random Jokus in den bot-muell Textchannel.", false); + eb.addField("::yoshi joke -channel ``kanal``", "Schreibt einen random Jokus in den Textchannel kanal.", false); + eb.addField("::yoshi play -add -name ``name``", "F\u00fcgt die im **ANHANG** enthaltene Audiodatei (im .opus-" + + "Format der Audiosammlung hinzu und gibt ihr den Namen ``name``. Dabei m\u00fcssen der Befehl und der Anhang " + + "nat\u00fcrlich der selben Nachricht angeh\u00f6ren.", false); + eb.addField("::yoshi play -list", "Listet alle derzeit verf\u00fcgbaren Sounds auf.", false); + eb.addField("::yoshi play -name ``name`` -channel ``kanal``", "Spielt den Sound mit dem Namen ``name`` " + + "in dem Voicechannel ``kanal``. Die Kanalangabe kann man auch weglassen, wenn man selbst oder der Bot " + + "sich gerade in einem Voicechannel befindet.", false); + eb.addField("::yoshi say -text \"``text``\" -channel ``kanal``", "Liest den Text ``text`` mittels " + + "Google \u00dcbersetzer in dem Voicechannel ``kanal`` vor. Die Kanalangabe kann man auch weglassen, " + + "wenn man selbst oder der Bot sich gerade in einem Voicechannel befindet.", false); + eb.addField("::yoshi sauce -tags \"``tags``\"", "Schickt ein zuf\u00e4lliges knuspriges Bild von rule34.xxx " + + "in den schrein-auf-den-bot gefiltert durch die ``tags`` (durch Leerzeichen getrennt). Ps.: Vergiss die " + + "doppelten Anf\u00fchrungszeichen nicht.", false); + eb.addField("::yoshi pat", "Macht aus dem im **ANHANG** enthaltenen Bild ein Pat-GIF und schickt es " + + "in den bot-muell Textchannel. Dabei m\u00fcssen der Befehl und der Anhang nat\u00fcrlich der selben " + + "Nachricht angeh\u00f6ren.", false); + eb.addField("::yoshi bonk", "Analog zu pat, nur mit Bonk.", false); + eb.addField("::yoshi wikipedia -name ``name`` -channel ``kanal``", "Liest die ersten S\u00e4tze des " + + "Wikipedia-Artikels zu ``name`` im Sprachkanal ``kanal`` vor und schickt den Text in den bot-muell Textchannel." + + "Die Kanalangabe kann man auch weglassen, wenn man selbst oder der Bot sich gerade in einem Voicechannel befindet.", false); + sendCustomMessage(eb.build()); + + return true; + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/JokeCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/JokeCommand.java new file mode 100644 index 0000000..5c6e73c --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/JokeCommand.java @@ -0,0 +1,120 @@ +package de.yannicpunktdee.yoshibot.command.commands; + +import de.yannicpunktdee.yoshibot.command.YoshiCommand; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import net.dv8tion.jda.api.entities.TextChannel; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; +import java.util.Random; + +import static de.yannicpunktdee.yoshibot.utils.RestHelper.getFromURL; + +/** + * Schickt einen zufälligen Jokus aus einer zufällig ausgewählten Quelle in den Textchannel. + * + * @author Yannic Link + */ +public class JokeCommand extends YoshiCommand { + + /** + * Erstellt einen neuen JokeCommand. + */ + public JokeCommand(YoshiCommandContext context) { + super(context); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized boolean execute() { + String message = "Jokus"; + + Random random = YoshiBot.getInstance().getRandom(); + int number = random.nextInt(3); + switch (number) { + case 0: + message = jokeApi(); + break; + case 1: + message = officialJokeApi(); + break; + case 2: + message = chuckNorris(); + break; + default: + message = "Jokus"; + break; + } + + if (context.containsArguments(new String[]{"channel"})) { + String arg = context.getArgument("channel"); + if (arg == null) { + sendErrorMessage("Es wurde kein channel angegeben."); + return false; + } + List channels = YoshiBot.getInstance().jda + .getTextChannelsByName(context.getArgument("channel"), true); + if (channels.isEmpty()) { + sendErrorMessage("Der Kanalname konnte nicht gefunden werden."); + return false; + } + channels.get(0).sendMessage(message).queue(); + } else { + sendMessage(message); + } + return true; + } + + private String chuckNorris() { + String url = "http://api.icndb.com/jokes/random"; + + JSONObject json = null; + + try { + String raw = getFromURL(url); + json = new JSONObject(raw); + return json.getJSONObject("value").getString("joke"); + } catch (JSONException e) { + return "Konnte keinen Jokus von \"" + url + "\" laden."; + } + } + + private String officialJokeApi() { + String url = "https://official-joke-api.appspot.com/jokes/random"; + + JSONObject json = null; + + try { + String raw = getFromURL(url); + json = new JSONObject(raw); + String result = json.getString("setup"); + result += " - "; + result += json.getString("punchline"); + return result; + } catch (JSONException e) { + return "Konnte keinen Jokus von \"" + url + "\" laden."; + } + } + + private String jokeApi() { + String url = "https://v2.jokeapi.dev/joke/any"; + + JSONObject json = null; + + try { + String raw = getFromURL(url); + json = new JSONObject(raw); + String result = json.getString("setup"); + result += " - "; + result += json.getString("delivery"); + return result; + } catch (JSONException e) { + return "Konnte keinen Jokus von \"" + url + "\" laden."; + } + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/PatCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/PatCommand.java new file mode 100644 index 0000000..d4ebefd --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/PatCommand.java @@ -0,0 +1,62 @@ +package de.yannicpunktdee.yoshibot.command.commands; + +import de.yannicpunktdee.yoshibot.command.YoshiCommand; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.utils.GifSequenceWriter; +import de.yannicpunktdee.yoshibot.utils.Resources; + +import javax.imageio.ImageIO; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class PatCommand extends YoshiCommand { + + public PatCommand(YoshiCommandContext context) { + super(context); + } + + @Override + public boolean execute() { + if (!super.execute()) return false; + + File outFile = new File(Resources.getEnsuredTempPath() + UUID.randomUUID().toString() + ".gif"); + + try { + BufferedImage inPicture= ImageIO.read(downloadAttachmentToFile(null, null)); + BufferedImage pat1Picture= ImageIO.read(new File(Resources.getPatPngPath() + "pat1.png")); + BufferedImage pat2Picture= ImageIO.read(new File(Resources.getPatPngPath() + "pat2.png")); + BufferedImage pat3Picture= ImageIO.read(new File(Resources.getPatPngPath() + "pat3.png")); + BufferedImage frame1 = getOutFrame(pat1Picture, inPicture, 100, 100, 400, 400); + ImageOutputStream output = new FileImageOutputStream(outFile); + GifSequenceWriter writer = new GifSequenceWriter(output, frame1.getType(), 100, true); + writer.writeToSequence(frame1); + writer.writeToSequence(getOutFrame(pat2Picture, inPicture, 100, 70, 400, 430)); + writer.writeToSequence(getOutFrame(pat3Picture, inPicture, 100, 50, 400, 450)); + writer.close(); + output.close(); + } catch (IOException e) { + sendErrorMessage("Gif konnte nicht erstellt werden,"); + e.printStackTrace(); + return false; + } + + sendFile(outFile, null); + + return true; + } + + private BufferedImage getOutFrame(BufferedImage patHandGraphics, BufferedImage personGraphics, int x, int y, int width, int height){ + BufferedImage outFrame = new BufferedImage(500, 500, BufferedImage.TYPE_INT_RGB); + Graphics2D g = outFrame.createGraphics(); + g.setColor(Color.black); + g.drawImage(personGraphics, x, y, width, height, null); + g.drawImage(patHandGraphics, 0, 0, 500, 400, null); + return outFrame; + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/PlayCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/PlayCommand.java new file mode 100644 index 0000000..8736994 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/PlayCommand.java @@ -0,0 +1,129 @@ +package de.yannicpunktdee.yoshibot.command.commands; + +import de.yannicpunktdee.yoshibot.command.YoshiCommand; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import de.yannicpunktdee.yoshibot.utils.Resources; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.VoiceChannel; + +import org.apache.commons.text.similarity.JaccardDistance; + +import java.awt.Color; +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +public class PlayCommand extends YoshiCommand { + + protected String[] requiredArguments = {"name"}; + + private static final Map cooldownChecker = new HashMap<>(); + + private static final int SECOND_DELAY = 5; + + + public PlayCommand(YoshiCommandContext context) { + super(context); + } + + + @Override + public boolean execute() { + if (!super.execute()) return false; + + for (User user : cooldownChecker.keySet()) { + Long timestamp = cooldownChecker.get(user); + if (System.currentTimeMillis() - timestamp > SECOND_DELAY * 1000) { + cooldownChecker.remove(user); + } + } + + if (context.containsArguments(new String[]{"add", "name"})) { + return addSound(context.getArgument("name")); + } else if (context.containsArgument("list")) { + return listSounds(); + } else if (context.containsArgument("name")) { + return playSound(context.getArgument("name")); + } else { + context.getEvent().getMessage().getTextChannel().sendMessage("Blyat, keine Ahnung was du willst. Gib mal " + + "Parameter").queue(); + } + + return true; + } + + private boolean addSound(String filename) { + File download = downloadAttachmentToFile(Resources.getAudioPath(), filename); + if (download.isFile()) { + sendInfoMessage("Audio erfolgreich hinzugefügt."); + return true; + } else { + sendErrorMessage("Audio konnte nicht hinzugefügt werden."); + return false; + } + } + + private boolean listSounds() { + EmbedBuilder eb = new EmbedBuilder(); + eb.setTitle("Es sind folgende Audios verf\u00fcgbar:"); + eb.setColor(Color.cyan); + eb.setDescription(String.join("\n", getAllFiles())); + sendCustomMessage(eb.build()); + return true; + } + + private boolean playSound(String requestedFile) { + if (requestedFile == null) { + sendErrorMessage(String.format("Konnte keine Audiodatei namens '%s.opus' finden!", + context.getArgument("name"))); + return false; + } + requestedFile = getBestMatch(requestedFile, getAllFiles()); + File file = new File(Resources.getPathToAudioFile(requestedFile)); + if (!file.isFile()) { + sendErrorMessage(String.format("Konnte keine Audiodatei namens '%s.opus' finden!", + context.getArgument("name"))); + return false; + } + VoiceChannel vc = getVoiceChannelByParam(); + if (vc == null) { + sendErrorMessage("Konnte keinen Audiochannel auswählen."); + return false; + } + User author = context.getEvent().getAuthor(); + if (YoshiBot.getInstance().jda.getVoiceChannels().parallelStream() + .flatMap(vcs -> vcs.getMembers().parallelStream()).map( + Member::getUser).noneMatch(user -> user == author)) { + if (PlayCommand.cooldownChecker.containsKey(author)) { + sendErrorMessage( + "Иди нахуй (geh auf Schwanz), du musst " + SECOND_DELAY + "s warten, wenn du nicht in" + + " einem Channel bist"); + return false; + } + PlayCommand.cooldownChecker.put(author, System.currentTimeMillis()); + } + context.getEvent().getMessage().getTextChannel() + .sendMessage("Danke, " + context.getEvent().getMessage().getAuthor().getName() + ". Spiele '" + + requestedFile + "' in '" + vc.getName() + "' ab").queue(); + YoshiBot.getInstance().playSound(file, vc); + return true; + } + + private String getBestMatch(String word, List choices) { + Optional match = choices.parallelStream().filter(word::equals).findAny(); + return match.orElse(choices.parallelStream().min( + Comparator.comparingDouble(file -> new JaccardDistance().apply(word, file))).orElse(null)); + } + + private List getAllFiles() { + File audioDirectory = new File(Resources.getAudioPath()); + return Arrays.stream(Objects.requireNonNull(audioDirectory.listFiles())) + .map(File::getName) + .filter(name -> name.endsWith(".opus")) + .map(name -> name.substring(0, name.lastIndexOf("."))) + .sorted(String::compareToIgnoreCase).collect(Collectors.toList()); + } +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/SayCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/SayCommand.java new file mode 100644 index 0000000..cdf918f --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/SayCommand.java @@ -0,0 +1,27 @@ +package de.yannicpunktdee.yoshibot.command.commands; + +import de.yannicpunktdee.yoshibot.command.YoshiCommand; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import net.dv8tion.jda.api.entities.VoiceChannel; + +public class SayCommand extends YoshiCommand { + + + protected final String[] requiredArguments = {"text", "channel"}; + + + public SayCommand(YoshiCommandContext context) { + super(context); + } + + @Override + public boolean execute() { + if (!super.execute()) return false; + context.getEvent().getMessage().getTextChannel().sendMessage( + "Danke, " + context.getEvent().getMessage().getAuthor().getName() + ". Ich werde nun " + + "abspielen:\n" + context.getArgument("text")).queue(); + return YoshiBot.getInstance().sayTTS(context.getArgument("text"), getVoiceChannelByParam()); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/WikipediaCommand.java b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/WikipediaCommand.java new file mode 100644 index 0000000..fcaa078 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/command/commands/WikipediaCommand.java @@ -0,0 +1,47 @@ +package de.yannicpunktdee.yoshibot.command.commands; + +import de.yannicpunktdee.yoshibot.command.YoshiCommand; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import de.yannicpunktdee.yoshibot.utils.Logger; +import de.yannicpunktdee.yoshibot.utils.RestHelper; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.List; + +public class WikipediaCommand extends YoshiCommand { + /** + * Erzeugt ein neues Kommando, führt es aber noch nicht aus. Es wird ermittelt, ob die Argumentenkombination valide + * ist und das isOk-Flag gesetzt. Im Fehlerfall wird eine Fehleremeldung spezifiziert. + * + * @param context Der Kontext mit dem das Kommando aufgerufen wurde. + */ + public WikipediaCommand(YoshiCommandContext context) { + super(context); + } + + @Override + public boolean execute() { + if (!super.execute()) return false; + + String url = "https://de.wikipedia.org/w/api.php?format=json&action=query&prop=extracts&exintro&explaintext" + + "&redirects&titles=" + context.getArgument("name"); + + JSONObject articleBase = new JSONObject(RestHelper.getFromURL(url)); + JSONObject pages = articleBase.getJSONObject("query").getJSONObject("pages"); + if (pages.has("-1")) { + sendErrorMessage("Kein Artikel namens " + context.getArgument("name") + " gefunden!"); + Logger.logWarning("Konnte Artikel " + context.getArgument("name") + " nicht finden!"); + } + assert pages.keySet().stream().findFirst().isPresent(); + JSONObject page = pages.getJSONObject(pages.keySet().stream().findFirst().get()); + + String text = page.getString("extract"); + sendMessage(text); + List parts = Arrays.asList(text.split(" ")); + text = String.join(" ", parts.subList(0, Math.min(parts.size(), 50))); + + return YoshiBot.getInstance().sayTTS(text, getVoiceChannelByParam()); + } +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/listeners/CommandLine.java b/app/src/main/java/de/yannicpunktdee/yoshibot/listeners/CommandLine.java new file mode 100644 index 0000000..9bee151 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/listeners/CommandLine.java @@ -0,0 +1,44 @@ +package de.yannicpunktdee.yoshibot.listeners; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import de.yannicpunktdee.yoshibot.main.YoshiBot; + +public class CommandLine extends Thread implements Runnable { + + private BufferedReader reader; + + /** + * Nicht manuell aufrufen. Wird einmalig in startYoshiBot in einem neuen Thread aufgerufen und reagiert + * auf administrative Konsoleneingaben außerhalb von Discord. + */ + @Override public void run() { + String line = ""; + reader = new BufferedReader(new InputStreamReader(System.in)); + try { + System.out.print("> "); + while((line = reader.readLine()) != null) { + line = line.trim(); + if(line.equalsIgnoreCase("exit")) { + YoshiBot.getInstance().stop(); + return; + } + System.out.print("> "); + } + } catch(IOException e) { + System.err.println("Es ist eine IOException aufgetreten."); + } + } + + public void stopCommandLine() { + try { + reader.close(); + interrupt(); + } catch (IOException e) { + System.err.println("Fehler beim Schließen des Readers."); + } + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/listeners/DiscordEventListener.java b/app/src/main/java/de/yannicpunktdee/yoshibot/listeners/DiscordEventListener.java new file mode 100644 index 0000000..9f0af7f --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/listeners/DiscordEventListener.java @@ -0,0 +1,96 @@ +package de.yannicpunktdee.yoshibot.listeners; + +import de.yannicpunktdee.yoshibot.audio.AudioPlayerListener; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import de.yannicpunktdee.yoshibot.utils.Resources; +import net.dv8tion.jda.api.entities.ChannelType; +import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.events.guild.voice.GuildVoiceJoinEvent; +import net.dv8tion.jda.api.events.guild.voice.GuildVoiceLeaveEvent; +import net.dv8tion.jda.api.events.guild.voice.GuildVoiceMoveEvent; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +/** + * Lauscht auf eingehende Nachrichten und leitet diese an die YoshiBot.executeCommand-Methode weiter, falls es sich um + * ein Kommando handelt. + * + * @author Yannic Link + */ +public class DiscordEventListener extends ListenerAdapter { + + /** + * {@inheritDoc} + */ + @Override + public void onMessageReceived(MessageReceivedEvent event) { + if (!event.isFromType(ChannelType.TEXT)) return; + + if (event.getAuthor().isBot()) return; + + boolean inErlaubtemKanal = Arrays.stream(Resources.getRestrict_commands_to_channel()) + .anyMatch(channel -> channel.equalsIgnoreCase(event.getTextChannel().getName())); + + if (Resources.getRestrict_commands_to_channel() != null && !inErlaubtemKanal) + return; + + String raw = event.getMessage().getContentRaw().trim(); + + if (!raw.startsWith(YoshiCommandContext.PREFIX)) return; + + YoshiCommandContext context = new YoshiCommandContext(raw, event); + if (!context.getState().equals(YoshiCommandContext.State.NO_COMMAND)) YoshiBot.executeCommand(context); + } + + @Override + public void onGuildVoiceJoin(@NotNull GuildVoiceJoinEvent event) { + super.onGuildVoiceJoin(event); + if (event.getMember().getUser().isBot()) return; + + if (Resources.isGreetings_and_byebyes_on()) { + String nameToPlay = event.getMember().getNickname(); + if (nameToPlay == null) nameToPlay = event.getMember().getUser().getName(); + + YoshiBot.getInstance().sayTTS(Resources.getRandomGreeting(nameToPlay), event.getChannelJoined()); + } + if (!AudioPlayerListener.isPlayingTrack()) YoshiBot.getInstance().joinVoiceChannelWithMostMembers(); + + } + + @Override + public void onGuildVoiceMove(@NotNull GuildVoiceMoveEvent event) { + super.onGuildVoiceMove(event); + if (event.getMember().getUser().isBot()) return; + + VoiceChannel afkChannel = YoshiBot.getInstance().getGuild().getAfkChannel(); + String nameToPlay = event.getMember().getNickname(); + nameToPlay = nameToPlay == null ? event.getMember().getUser().getName() : nameToPlay; + if (event.getChannelJoined() == afkChannel) { + YoshiBot.getInstance().sayTTS(Resources.getRandomAfk(nameToPlay, false), event.getChannelLeft()); + } else if (event.getChannelLeft() == afkChannel) { + YoshiBot.getInstance().sayTTS(Resources.getRandomAfk(nameToPlay, true), event.getChannelJoined()); + } + + if (!AudioPlayerListener.isPlayingTrack()) YoshiBot.getInstance().joinVoiceChannelWithMostMembers(); + } + + @Override + public void onGuildVoiceLeave(@NotNull GuildVoiceLeaveEvent event) { + super.onGuildVoiceLeave(event); + if (event.getMember().getUser().isBot() || event.getChannelLeft().getMembers().size() == 0) return; + + if (Resources.isGreetings_and_byebyes_on()) { + + String nameToPlay = event.getMember().getNickname(); + nameToPlay = nameToPlay == null ? event.getMember().getUser().getName() : nameToPlay; + + YoshiBot.getInstance().sayTTS(Resources.getRandomByebye(nameToPlay), event.getChannelLeft()); + } + if (!AudioPlayerListener.isPlayingTrack()) YoshiBot.getInstance().joinVoiceChannelWithMostMembers(); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/main/Main.java b/app/src/main/java/de/yannicpunktdee/yoshibot/main/Main.java new file mode 100644 index 0000000..971c54e --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/main/Main.java @@ -0,0 +1,36 @@ +package de.yannicpunktdee.yoshibot.main; + +import de.yannicpunktdee.yoshibot.utils.Logger; + +import javax.security.auth.login.LoginException; +import java.net.URISyntaxException; + +/** + * Main-Klasse und Startpunkt für die Bot-Applikation. + * + * @author Yannic Link + */ +public class Main { + + /** + * Eintrittspunkt für die Applikation. Erzeugen eines neuen Yoshi-Bots und Starten. + * + * @param args Aufrufargumente. Werden später zum Konfigurieren genutzt. + * + * @throws URISyntaxException + */ + public static void main(String[] args) throws URISyntaxException { + YoshiBot yoshiBot = YoshiBot.getInstance(); + + if (!yoshiBot.init((args.length > 0) ? args[0] : null)) { + Logger.logError("Es ist ein Fehler beim Initialisieren der Ressourcen aufgetreten."); + return; + } + + Logger.logInfo("Ressourcen erfolgreich initialisiert. Starte Yoshi Bot ..."); + + + yoshiBot.start(); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/main/YoshiBot.java b/app/src/main/java/de/yannicpunktdee/yoshibot/main/YoshiBot.java new file mode 100644 index 0000000..7dca5a7 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/main/YoshiBot.java @@ -0,0 +1,244 @@ +package de.yannicpunktdee.yoshibot.main; + +import com.sedmelluq.discord.lavaplayer.player.AudioPlayer; +import com.sedmelluq.discord.lavaplayer.player.AudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.player.DefaultAudioPlayerManager; +import com.sedmelluq.discord.lavaplayer.source.AudioSourceManagers; +import com.sedmelluq.discord.lavaplayer.source.local.LocalAudioSourceManager; +import de.yannicpunktdee.yoshibot.audio.AudioLoadResultHandlerImpl; +import de.yannicpunktdee.yoshibot.audio.AudioPlayerListener; +import de.yannicpunktdee.yoshibot.audio.AudioSendHandlerImpl; +import de.yannicpunktdee.yoshibot.command.YoshiCommandContext; +import de.yannicpunktdee.yoshibot.command.YoshiCommandDistributor; +import de.yannicpunktdee.yoshibot.listeners.CommandLine; +import de.yannicpunktdee.yoshibot.listeners.DiscordEventListener; +import de.yannicpunktdee.yoshibot.utils.*; +import lombok.Getter; +import lombok.SneakyThrows; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.OnlineStatus; +import net.dv8tion.jda.api.entities.Activity; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.PermissionOverride; +import net.dv8tion.jda.api.entities.VoiceChannel; +import net.dv8tion.jda.api.exceptions.InsufficientPermissionException; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Repräsentiert einen Yoshi-Bot. Der Bot initialisiert alle Ressourcen und schaltet sich in der startYoshiBot-Methode + * online und beginnt dann zu lauschen. Parallel lauscht ein Thread auf Konsoleneingaben für administrative Zwecke, die + * nicht über den Chat erledigt werden sollten. + * + * @author Yannic Link + */ +public final class YoshiBot { + + private CommandLine commandLineThread; + + /** + * Erlaubt es einige Einstellungen vor und nach der Erzeugung eines Bots vorzunehmen. + */ + public JDABuilder jdaBuilder; + /** + * Instanz vom aktuell laufenden Bot. + */ + public JDA jda; + /** + * LavaPlayer AudioPlayerManager. + */ + public AudioPlayerManager audioPlayerManager; + + @Getter + private Guild guild; + + public AudioPlayer audioPlayer; + + private static YoshiBot instance = null; + + @Getter + private final Random random = new Random(); + + private final Set allProvides = new HashSet<>(); + + /** + * Initialisiert alle dynamisch hinzugefügten und statischen Ressourcen. Startet aber nicht den Bot selbst. + */ + public boolean init(String configPath) { + return Resources.init(configPath); + } + + /** + * Startet den Bot und schaltet ihn online. Beginnt auf Konsoleneingaben für administrative Zwecke zu lauschen. + **/ + @SneakyThrows + public void start() { + System.out.println("Starte YoshiBot."); + + jdaBuilder = JDABuilder.createDefault(Resources.getJda_builder_string()); + jdaBuilder.setAutoReconnect(true); + + jdaBuilder.addEventListeners(new DiscordEventListener()); + + audioPlayerManager = new DefaultAudioPlayerManager(); + audioPlayerManager.registerSourceManager(new LocalAudioSourceManager()); + AudioSourceManagers.registerRemoteSources(audioPlayerManager); + + jda = jdaBuilder.build(); + try { + jda.awaitReady(); + } catch (InterruptedException e) { + Logger.logError("Konnte nicht auf jda warten. Thread unterbrochen."); + return; + } + + jda.awaitReady(); + guild = jda.getGuildById(Resources.getGuild_id()); + + audioPlayer = audioPlayerManager.createPlayer(); + audioPlayer.addListener(new AudioPlayerListener(guild.getAudioManager())); + guild.getAudioManager().setSendingHandler(new AudioSendHandlerImpl(audioPlayer)); + + jdaBuilder.setStatus(OnlineStatus.ONLINE); + + Logger.logInfo("YoshiBot online."); + + commandLineThread = new CommandLine(); + commandLineThread.start(); + + StatusProviderFactory.createStatusProviders(Resources.getMcserver_config_file(), allProvides); + + //SauceProvider.init(300); + + + Executors.newScheduledThreadPool(1).scheduleAtFixedRate(YoshiBot::setRandomActivity, 0, 10, TimeUnit.HOURS); + + Runtime.getRuntime().addShutdownHook(new Thread(() -> YoshiBot.getInstance().stop())); + + joinVoiceChannelWithMostMembers(); + } + + public synchronized void stop() { + allProvides.forEach(Provider::onStop); + commandLineThread.stopCommandLine(); + System.out.println("Beende YoshiBot ..."); + jdaBuilder.setStatus(OnlineStatus.OFFLINE); + jda.shutdown(); + System.out.println("YoshiBot offline."); + } + + /** + * Leitet den Context an den CommandDistributor weiter. + * + * @param command Der Kontext für das Kommando. + */ + public static void executeCommand(YoshiCommandContext command) { + YoshiCommandDistributor.distribute(command); + } + + public static YoshiBot getInstance() { + if (YoshiBot.instance == null) { + YoshiBot.instance = new YoshiBot(); + } + return YoshiBot.instance; + } + + @SneakyThrows + private static void setRandomActivity() { + YoshiBot yoshiBot = YoshiBot.getInstance(); + yoshiBot.jda.awaitReady(); + List text = Files.readAllLines(new File(Resources.getActivitiesPath()).toPath()); + String activity = text.get(yoshiBot.random.nextInt(text.size())); + yoshiBot.jda.getPresence().setActivity(Activity.playing(activity)); + Logger.logInfo("Setze Aktivität auf " + activity); + } + + public synchronized void joinVoiceChannel(VoiceChannel vc) { + if (vc == null) { + guild.getAudioManager().closeAudioConnection(); + return; + } + if (guild.getAudioManager().getConnectedChannel() != null && + vc.getIdLong() == guild.getAudioManager().getConnectedChannel().getIdLong()) return; + try { + guild.getAudioManager().openAudioConnection(vc); + } catch (InsufficientPermissionException e) { + Logger.logWarning("Durfte dem VoiceChannel " + vc.getName() + " nicht beitreten."); + } + } + + public VoiceChannel joinVoiceChannelWithMostMembers() { + Map vcAmount = + guild.getVoiceChannels().stream() + .filter(channel -> channel != guild.getAfkChannel()) + .collect(Collectors.toMap(vc -> vc, + vc -> vc.getMembers().stream() + .filter(m -> !m.getUser().isBot()) + .count())); + + VoiceChannel channel = + vcAmount.entrySet().stream() + .filter(vc -> vc.getValue() > 0) + .max(Comparator.comparingLong(Map.Entry::getValue)) + .map(Map.Entry::getKey) + .orElse(null); + if (channel != null) { + PermissionOverride override = channel.getPermissionOverride( + Objects.requireNonNull(guild.getMember(jda.getSelfUser()))); + } + joinVoiceChannel(channel); + return channel; + } + + public boolean playSound(File file, VoiceChannel vc) { + if (!file.isFile()) return false; + + joinVoiceChannel(vc); + + audioPlayerManager.loadItem(file.getAbsolutePath(), new AudioLoadResultHandlerImpl()); + + return true; + } + + public boolean sayTTS(String text, VoiceChannel vc) { + String path = Resources.getEnsuredTempPath() + UUID.randomUUID() + ".opus"; + + try { + ProcessBuilder pb = new ProcessBuilder( + "python3", + Resources.getTtsPath(), + "--text", + text, + "--lang", + "de", + "--out", + path); + + Process p = pb.start(); + BufferedReader errorReader = new BufferedReader(new InputStreamReader(p.getErrorStream())); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = errorReader.readLine()) != null) { + builder.append(line).append("\n"); + } + if (builder.toString().length() > 0) { + Logger.logError(builder.toString()); + } + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return playSound(new File(path), vc); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/utils/GifSequenceWriter.java b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/GifSequenceWriter.java new file mode 100644 index 0000000..6aaa5ab --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/GifSequenceWriter.java @@ -0,0 +1,153 @@ +package de.yannicpunktdee.yoshibot.utils; + +// +// GifSequenceWriter.java +// +// Created by Elliot Kroo on 2009-04-25. +// +// This work is licensed under the Creative Commons Attribution 3.0 Unported +// License. To view a copy of this license, visit +// http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative +// Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. + + +import javax.imageio.*; +import javax.imageio.metadata.*; +import javax.imageio.stream.*; +import java.awt.image.*; +import java.io.*; +import java.util.Iterator; + +public class GifSequenceWriter { + protected ImageWriter gifWriter; + protected ImageWriteParam imageWriteParam; + protected IIOMetadata imageMetaData; + + /** + * Creates a new GifSequenceWriter + * + * @param outputStream the ImageOutputStream to be written to + * @param imageType one of the imageTypes specified in BufferedImage + * @param timeBetweenFramesMS the time between frames in miliseconds + * @param loopContinuously wether the gif should loop repeatedly + * @throws IIOException if no gif ImageWriters are found + * @author Elliot Kroo (elliot[at]kroo[dot]net) + */ + public GifSequenceWriter( + ImageOutputStream outputStream, + int imageType, + int timeBetweenFramesMS, + boolean loopContinuously) throws IIOException, IOException { + // my method to create a writer + gifWriter = getWriter(); + imageWriteParam = gifWriter.getDefaultWriteParam(); + ImageTypeSpecifier imageTypeSpecifier = + ImageTypeSpecifier.createFromBufferedImageType(imageType); + + imageMetaData = + gifWriter.getDefaultImageMetadata(imageTypeSpecifier, + imageWriteParam); + + String metaFormatName = imageMetaData.getNativeMetadataFormatName(); + + IIOMetadataNode root = (IIOMetadataNode) + imageMetaData.getAsTree(metaFormatName); + + IIOMetadataNode graphicsControlExtensionNode = getNode( + root, + "GraphicControlExtension"); + + graphicsControlExtensionNode.setAttribute("disposalMethod", "none"); + graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE"); + graphicsControlExtensionNode.setAttribute( + "transparentColorFlag", + "FALSE"); + graphicsControlExtensionNode.setAttribute( + "delayTime", + Integer.toString(timeBetweenFramesMS / 10)); + graphicsControlExtensionNode.setAttribute( + "transparentColorIndex", + "0"); + + IIOMetadataNode commentsNode = getNode(root, "CommentExtensions"); + commentsNode.setAttribute("CommentExtension", "Created by MAH"); + + IIOMetadataNode appEntensionsNode = getNode( + root, + "ApplicationExtensions"); + + IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension"); + + child.setAttribute("applicationID", "NETSCAPE"); + child.setAttribute("authenticationCode", "2.0"); + + int loop = loopContinuously ? 0 : 1; + + child.setUserObject(new byte[]{0x1, (byte) (loop & 0xFF), (byte) + ((loop >> 8) & 0xFF)}); + appEntensionsNode.appendChild(child); + + imageMetaData.setFromTree(metaFormatName, root); + + gifWriter.setOutput(outputStream); + + gifWriter.prepareWriteSequence(null); + } + + public void writeToSequence(RenderedImage img) throws IOException { + gifWriter.writeToSequence( + new IIOImage( + img, + null, + imageMetaData), + imageWriteParam); + } + + /** + * Close this GifSequenceWriter object. This does not close the underlying + * stream, just finishes off the GIF. + */ + public void close() throws IOException { + gifWriter.endWriteSequence(); + } + + /** + * Returns the first available GIF ImageWriter using + * ImageIO.getImageWritersBySuffix("gif"). + * + * @return a GIF ImageWriter object + * @throws IIOException if no GIF image writers are returned + */ + private static ImageWriter getWriter() throws IIOException { + Iterator iter = ImageIO.getImageWritersBySuffix("gif"); + if (!iter.hasNext()) { + throw new IIOException("No GIF Image Writers Exist"); + } else { + return iter.next(); + } + } + + /** + * Returns an existing child node, or creates and returns a new child node (if + * the requested node does not exist). + * + * @param rootNode the IIOMetadataNode to search for the child node. + * @param nodeName the name of the child node. + * @return the child node, if found or a new node created with the given name. + */ + private static IIOMetadataNode getNode( + IIOMetadataNode rootNode, + String nodeName) { + int nNodes = rootNode.getLength(); + for (int i = 0; i < nNodes; i++) { + if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) + == 0) { + return ((IIOMetadataNode) rootNode.item(i)); + } + } + IIOMetadataNode node = new IIOMetadataNode(nodeName); + rootNode.appendChild(node); + return (node); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Logger.java b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Logger.java new file mode 100644 index 0000000..a0d03d1 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Logger.java @@ -0,0 +1,43 @@ +package de.yannicpunktdee.yoshibot.utils; + +public final class Logger { + + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_GREEN = "\u001B[32m"; + private static final String ANSI_YELLOW = "\u001B[33m"; + private static final String ANSI_BLUE = "\u001B[34m"; + + + public static void logDebug(String message){ + System.out.printf("%s[%tT: Yoshi::DEBUG] %s%s%n", + ANSI_GREEN, + System.currentTimeMillis(), + message, + ANSI_RESET); + } + + public static void logInfo(String message){ + System.out.printf("%s[%tT: Yoshi::INFO] %s%s%n", + ANSI_BLUE, + System.currentTimeMillis(), + message, + ANSI_RESET); + } + + public static void logWarning(String message){ + System.out.printf("%s[%tT: Yoshi::WARNING] %s%s%n", + ANSI_YELLOW, + System.currentTimeMillis(), + message, + ANSI_RESET); + } + + public static void logError(String message){ + System.err.printf("%s[%tT: Yoshi::ERROR] %s%s%n", + ANSI_YELLOW, + System.currentTimeMillis(), + message, + ANSI_RESET); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Provider.java b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Provider.java new file mode 100644 index 0000000..0414754 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Provider.java @@ -0,0 +1,7 @@ +package de.yannicpunktdee.yoshibot.utils; + +public interface Provider { + + void onStop(); + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Resources.java b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Resources.java new file mode 100644 index 0000000..14c36a8 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/Resources.java @@ -0,0 +1,317 @@ +package de.yannicpunktdee.yoshibot.utils; + +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import lombok.Getter; +import lombok.SneakyThrows; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; +import java.util.function.Function; +import java.util.stream.StreamSupport; + +public final class Resources { + + @Getter + private static boolean greetings_and_byebyes_on; + + @Getter + private static int statusUpdate; + + @Getter + private static long guild_id; + + @Getter + private static String resourcePath, configPath, audioPath, tempPath, activitiesPath, greetingsPath, byebyesPath, + sauceConfigPath, ttsPath, patPngPath, imagePath, bonkPngPath, jda_builder_string, status_channel, + path_to_mcstatus, comebacksPath, departsPath, mcserver_config_file; + + private static List greetings, byebyes, departs, comebacks; + + @Getter + private static String[] restrict_commands_to_channel, filteredTags; + + @Getter + private static final Map> feedDetails = new HashMap<>(); + + + private static Properties propertiesFile; + + + public static boolean init(String resourcePathArg) { + boolean isOk = initResources(resourcePathArg); + if (isOk) isOk = initConfig(); + if (isOk) isOk = initAudio(); + if (isOk) isOk = initTemp(); + if (isOk) isOk = initActivities(); + if (isOk) isOk = initGreetingsAndByebyes(); + if (isOk) isOk = initSauceConfig(); + if (isOk) isOk = initTTS(); + if (isOk) isOk = initJdaBuilderString(); + if (isOk) isOk = initGuildId(); + if (isOk) isOk = initChannelRestrict(); + if (isOk) isOk = initTagFilter(); + if (isOk) isOk = initPatPngPath(); + if (isOk) isOk = initImages(); + if (isOk) isOk = initBonkPngPath(); + if (isOk) isOk = initStatusMessage(); + + if (isOk) Logger.logInfo("Die Konfigurationen wurden erfolgreich geladen."); + else Logger.logError("Die Konfiguration konnte nicht geladen werden"); + + return isOk; + } + + private static boolean initResources(String resourcePathArg) { + Logger.logInfo("Versuche Resource-Verzeichnis zu finden."); + + resourcePath = (new File((resourcePathArg == null) ? "rsc" : resourcePathArg)).getAbsolutePath() + .replace('\\', '/') + "/"; + + return verifyExists(resourcePath, File::isDirectory) != null; + } + + @SneakyThrows + private static boolean initConfig() { + configPath = verifyExists(resourcePath + "Config.properties", File::isFile); + + if (configPath == null) { + return false; + } + + propertiesFile = new Properties(); + propertiesFile.load(new FileInputStream(configPath)); + + return true; + } + + private static boolean initAudio() { + return (audioPath = verifyExists(resourcePath + "audio/", File::isDirectory)) != null; + } + + public static String getPathToAudioFile(String name) { + String filePathWithoutExtension = audioPath + name; + if (new File(filePathWithoutExtension + ".opus").isFile()) { + return filePathWithoutExtension + ".opus"; + } else if (new File(filePathWithoutExtension + ".mp3").isFile()) { + return filePathWithoutExtension + ".mp3"; + } else { + return ""; + } + } + + private static boolean initTemp() { + Logger.logInfo("Versuche Temp-Verzeichnis zu finden."); + + String theoreticalTempPath = System.getProperty("java.io.tmpdir").replace('\\', '/') + "/yoshibot/"; + + tempPath = verifyExists(theoreticalTempPath, File::isDirectory); + if (tempPath != null) { + return true; + } + File tempDir = new File(theoreticalTempPath); + + if (tempDir.mkdir()) { + return verifyExists(tempDir.getAbsolutePath(), File::isDirectory) != null; + } else { + Logger.logError("Temp-Verzeichnis konnte nicht erstellt werden."); + return false; + } + } + + public static String getEnsuredTempPath() { + if (tempPath == null) { + initTemp(); + } + File tempDir = new File(tempPath); + if (!tempDir.isDirectory()) + if (!tempDir.mkdir()) throw new Error("Could not make Temp directory"); + return tempPath; + } + + private static boolean initActivities() { + activitiesPath = verifyExists(resourcePath + "activities.txt", File::isFile); + return activitiesPath != null; + } + + private static boolean initSauceConfig() { + sauceConfigPath = verifyExists(resourcePath + "sauceConfig.json", File::isFile); + return sauceConfigPath != null; + } + + private static boolean initTTS() { + ttsPath = verifyExists(resourcePath + "tts.py", File::isFile); + return ttsPath != null; + } + + @SneakyThrows + private static boolean initJdaBuilderString() { + if (verifyExists(resourcePath + "PrivateJdaBuilderString.txt", File::isFile) == null) { + return false; + } + jda_builder_string = Files.readAllLines(new File(resourcePath + "PrivateJdaBuilderString.txt").toPath()).get(0); + Logger.logInfo("jda_builder_string erfolgreich geladen"); + return true; + } + + private static boolean initPatPngPath() { + patPngPath = verifyExists(resourcePath + "pats/", File::isDirectory); + return patPngPath != null; + } + + private static boolean initBonkPngPath() { + bonkPngPath = verifyExists(resourcePath + "bonks/", File::isDirectory); + return bonkPngPath != null; + } + + private static boolean initGuildId() { + if (!propertiesFile.containsKey("guild_id")) { + Logger.logError("Die Config.properties benötigt das Attribut guild_id."); + return false; + } + String raw = propertiesFile.getProperty("guild_id"); + try { + guild_id = Long.parseLong(raw); + } catch (NumberFormatException e) { + Logger.logError("Die angegebene guild_id ist keine Ganzzahl"); + return false; + } + Logger.logInfo("guild_id erfolgreich geladen"); + return true; + } + + @SneakyThrows + private static boolean initGreetingsAndByebyes() { + greetingsPath = verifyExists(resourcePath + "greetings.txt", File::isFile); + if (greetingsPath == null) { + return false; + } + byebyesPath = verifyExists(resourcePath + "byebyes.txt", File::isFile); + comebacksPath = verifyExists(resourcePath + "comebacks.txt", File::isFile); + departsPath = verifyExists(resourcePath + "departs.txt", File::isFile); + + if (propertiesFile.containsKey("greetings_and_byebyes_on")) { + greetings_and_byebyes_on = Boolean.parseBoolean(propertiesFile.getProperty("greetings_and_byebyes_on")); + if (!greetings_and_byebyes_on) return true; + } else greetings_and_byebyes_on = true; + + greetings = Files.readAllLines(Paths.get(greetingsPath)); + byebyes = Files.readAllLines(Paths.get(byebyesPath)); + comebacks = Files.readAllLines(Paths.get(comebacksPath)); + departs = Files.readAllLines(Paths.get(departsPath)); + return true; + } + + public static String getRandomGreeting(String name) { + return greetings_and_byebyes_on ? String.format(getRandomFrom(greetings), name) : null; + } + + public static String getRandomByebye(String name) { + return greetings_and_byebyes_on ? String.format(getRandomFrom(byebyes), name) : null; + } + + public static String getRandomAfk(String name, boolean didComeBack) { + return greetings_and_byebyes_on ? String.format(getRandomFrom(didComeBack ? comebacks : departs), name) : null; + } + + private static String getRandomFrom(List pool) { + return pool.get(YoshiBot.getInstance().getRandom().nextInt(pool.size())); + } + + private static boolean initChannelRestrict() { + if (propertiesFile.containsKey("restrict_commands_to_channel")) + restrict_commands_to_channel = propertiesFile.getProperty("restrict_commands_to_channel").split("\\s+"); + return true; + } + + private static boolean initTagFilter() { + try { + JSONObject configBase = new JSONObject( + String.join("\n", + Files.readAllLines(new File(sauceConfigPath).toPath()))); + JSONArray filter = configBase.getJSONArray("tags_general_filter"); + filteredTags = + StreamSupport.stream(filter.spliterator(), false).map(i -> (String) i).toArray(String[]::new); + JSONArray feeds = configBase.getJSONArray("feeds"); + for (Object feedConfigObj : feeds) { + JSONObject feedConfig = (JSONObject) feedConfigObj; + List tags = new ArrayList<>(); + for (Object tagObj : feedConfig.getJSONArray("tags")) { + if (tagObj instanceof String) { + tags.add((String) tagObj); + } else if (tagObj instanceof JSONArray) { + StringBuilder sb = new StringBuilder().append("(%20"); + sb.append(((JSONArray) tagObj).join("%20~%20")); + tags.add(sb.append("%20)").toString().replace("\"", "")); + } + } + feedDetails.put(feedConfig.getString("channel"), tags); + } + } catch (IOException e) { + return false; + } + + Logger.logInfo("tags_general_filter erfolgreich geladen"); + return true; + } + + private static boolean initImages() { + imagePath = verifyExists(resourcePath + "image/", File::isDirectory); + if (imagePath != null) { + return true; + } + if (new File(resourcePath + "image/").mkdir()) { + imagePath = verifyExists(resourcePath + "image/", File::isDirectory); + Logger.logInfo("Bildordner erzeugt"); + return true; + } else { + Logger.logError("Konnte Bildordner nicht erzeugen!"); + return false; + } + } + + private static String verifyExists(String filename, Function checkIsValidFile) { + String[] split = filename.split("/"); + Logger.logDebug(String.format("Versuche %s zu finden.", split[split.length - 1])); + if (checkIsValidFile.apply(new File(filename))) { + return filename; + } else { + Logger.logError(String.format("%s konnte nicht gefunden werden", filename)); + return null; + } + } + + private static boolean initStatusMessage() { + if (propertiesFile.containsKey("path_to_mcstatus") && + propertiesFile.containsKey("status_channel") && + propertiesFile.containsKey("path_to_status_json")) { + status_channel = propertiesFile.getProperty("status_channel"); + path_to_mcstatus = propertiesFile.getProperty("path_to_mcstatus"); + mcserver_config_file = propertiesFile.getProperty("path_to_status_json"); + try { + statusUpdate = Integer.parseInt(propertiesFile.getProperty("status_update")); + } catch (NumberFormatException e) { + return false; + } + return true; + } else return false; + } + + public static String getProperty(String key) { + return propertiesFile.getProperty(key); + } + + @SneakyThrows + public synchronized static void setProperty(String key, String value) { + propertiesFile.setProperty(key, value); + + propertiesFile.store(new FileOutputStream(configPath), ""); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/utils/RestHelper.java b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/RestHelper.java new file mode 100644 index 0000000..87a8a65 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/RestHelper.java @@ -0,0 +1,40 @@ +package de.yannicpunktdee.yoshibot.utils; + +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringEscapeUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +public final class RestHelper { + + @SneakyThrows + public static String getFromURL(String url) { + StringBuilder response = new StringBuilder(""); + + HttpURLConnection con = null; + + try { + con = (HttpURLConnection) (new URL(url)).openConnection(); + con.setRequestMethod("GET"); + BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream())); + + String line; + while ((line = br.readLine()) != null) { + response.append(StringEscapeUtils.unescapeHtml4(line)); + } + + br.close(); + } catch (IOException e) { + return ""; + } finally { + if (con != null) con.disconnect(); + } + + return response.toString(); + } + +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/utils/StatusProvider.java b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/StatusProvider.java new file mode 100644 index 0000000..cec5fa1 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/StatusProvider.java @@ -0,0 +1,136 @@ +package de.yannicpunktdee.yoshibot.utils; + +import lombok.Getter; +import net.dv8tion.jda.api.EmbedBuilder; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class StatusProvider implements Provider { + + private static final ScheduledExecutorService statusScheduler = Executors.newScheduledThreadPool(1); + + private int lastPlayersOnline = -1; + + @Getter + private final String ip; + + private final String desc; + + private LocalDateTime timestampLastPlayerOnline; + + private final ProcessBuilder mcstatus = new ProcessBuilder(); + + + public StatusProvider(String desc, int secondsPerTime, String ip, LocalDateTime timestamp) { + this.timestampLastPlayerOnline = timestamp; + this.desc = desc; + this.ip = ip; + this.mcstatus.command(Resources.getPath_to_mcstatus(), ip, "json"); + + statusScheduler.scheduleAtFixedRate(this::updateStatusMessage, 0, secondsPerTime, + TimeUnit.SECONDS); + } + + @SuppressWarnings("unchecked") + public void updateStatusMessage() { + EmbedBuilder eb = new EmbedBuilder(); + eb.setTitle(desc); + eb.addField("IP", ip, false); + + try { + Map serverInfo = getPlayersOnline(); + if ((boolean) serverInfo.get("online")) { + if ((boolean) serverInfo.get("Server starting")) { + eb.addField("Server still Starting", "True", false); + } else { + int newPlayersOnline = (int) serverInfo.get("playerCount"); + eb.addField("Version", (String) serverInfo.get("version"), true); + eb.addField("MOTD", (String) serverInfo.get("motd"), true); + eb.addField("Spieler online", newPlayersOnline + " / " + serverInfo.get("playerMax"), false); + if (newPlayersOnline == 0) { + if (lastPlayersOnline > newPlayersOnline) { + timestampLastPlayerOnline = LocalDateTime.now(); + StatusProviderFactory.updateTimestamp(this, timestampLastPlayerOnline); + } + eb.addField("Zuletzt gesehen", + StatusProviderFactory.TIME_FORMATTER.format(timestampLastPlayerOnline), false); + } else { + eb.addField("Spieler:", String.join(", ", (List) serverInfo.get("playerNames")), false); + + } + lastPlayersOnline = newPlayersOnline; + } + } else { + eb.addField("Offline", "", false); + } + + StatusProviderFactory.updateStatus(this, eb.build()); + } catch (IOException e) { + Logger.logError(e.toString()); + } + } + + private Map getPlayersOnline() throws IOException { + + Process process = mcstatus.start(); + + try { + process.waitFor(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (process.exitValue() != 0) { + Logger.logError("MCStatus on IP " + ip + " exited with errorcode " + process.exitValue()); + } + + String output = new BufferedReader(new InputStreamReader(process.getInputStream())).lines() + .collect(Collectors.joining()); + + Map result = new HashMap<>(); + + if (output.startsWith("The server did not respond to the query protocol.")) { + result.put("online", false); + return result; + } + JSONObject obj = new JSONObject(output); + + result.put("online", obj.getBoolean("online")); + + if (!obj.getBoolean("online")) { + return result; + } + for (String key : new String[]{"player_count", "player_max", "players", "motd", "version"}) { + if (!obj.has(key)) { + result.put("Server starting", true); + return result; + } + } + result.put("Server starting", false); + + result.put("playerCount", obj.getInt("player_count")); + result.put("playerMax", obj.getInt("player_max")); + result.put("playerNames", StreamSupport.stream(obj.getJSONArray("players").spliterator(), false) + .map(jsonobj -> ((JSONObject) jsonobj).getString("name")).sorted().collect(Collectors.toList())); + result.put("motd", obj.getString("motd")); + result.put("version", obj.getString("version")); + + return result; + } + + @Override + public void onStop() { + StatusProviderFactory.updateTimestamp(this, timestampLastPlayerOnline); + } +} diff --git a/app/src/main/java/de/yannicpunktdee/yoshibot/utils/StatusProviderFactory.java b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/StatusProviderFactory.java new file mode 100644 index 0000000..87269f3 --- /dev/null +++ b/app/src/main/java/de/yannicpunktdee/yoshibot/utils/StatusProviderFactory.java @@ -0,0 +1,98 @@ +package de.yannicpunktdee.yoshibot.utils; + +import de.yannicpunktdee.yoshibot.main.YoshiBot; +import lombok.NonNull; +import lombok.SneakyThrows; +import net.dv8tion.jda.api.MessageBuilder; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.MessageEmbed; +import net.dv8tion.jda.api.entities.TextChannel; +import net.dv8tion.jda.api.exceptions.ErrorResponseException; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +public abstract class StatusProviderFactory { + + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss E, dd.MM.yyyy", + Locale.GERMANY); + private static final Map providers = new HashMap<>(); + private static final Map providerJSON = new HashMap<>(); + private static JSONObject rootJSON; + private static File configFile; + + @NonNull + private static final TextChannel statusChannel = + Objects.requireNonNull(YoshiBot.getInstance().getGuild().getTextChannelById(Resources.getStatus_channel())); + + @SneakyThrows + public static void createStatusProviders(String configFilePath, Set allProviders) { + configFile = new File(configFilePath); + rootJSON = new JSONObject(String.join("\n", Files.readAllLines(configFile.toPath()))); + final JSONArray servers = rootJSON.getJSONArray("servers"); + for (int i = 0; i < servers.length(); i++) { + final JSONObject server = servers.getJSONObject(i); + final String ip = server.getString("ip"); + final String name = server.getString("name"); + + if (!server.has("timestamp")) { + server.put("timestamp", TIME_FORMATTER.format(LocalDateTime.now())); + } + final LocalDateTime timestamp = LocalDateTime.parse(server.getString("timestamp"), TIME_FORMATTER); + + if (!server.has("message")) { + server.put("message", -1); + } + final long msg = ensureMessageId(server.getLong("message")); + server.put("message", msg); + + StatusProvider provider = new StatusProvider(name, Resources.getStatusUpdate(), ip, timestamp); + providers.put(provider, msg); + providerJSON.put(provider, servers.getJSONObject(i)); + } + + allProviders.addAll(providers.keySet()); + writeJSON(); + } + + public static void updateStatus(StatusProvider provider, MessageEmbed msg) { + statusChannel.editMessageById(providers.get(provider), msg).queue(); + } + + public static void updateTimestamp(StatusProvider provider, LocalDateTime timestamp) { + providerJSON.get(provider).put("timestamp", TIME_FORMATTER.format(timestamp)); + writeJSON(); + } + + @SneakyThrows + private static void writeJSON() { + BufferedWriter bw = new BufferedWriter(new FileWriter(configFile)); + bw.write(rootJSON.toString(2)); + bw.flush(); + } + + private static long createStatusMessage() { + Logger.logInfo("Creating new Status Message!"); + return StatusProviderFactory.statusChannel.sendMessage( + new MessageBuilder() + .append("ServerInformation") + .build()) + .complete().getIdLong(); + } + + private static long ensureMessageId(long messageId) { + try { + Message msg = StatusProviderFactory.statusChannel.retrieveMessageById(messageId).complete(); + return msg.getIdLong(); + } catch (ErrorResponseException e) { + return createStatusMessage(); + } + } +} diff --git a/app/src/main/resources/.gitkeep b/app/src/main/resources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..442d913 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/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 +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +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" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +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 + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# 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 + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + 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" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +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" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@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 +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +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" "-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 execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/rsc/.gitignore b/rsc/.gitignore new file mode 100644 index 0000000..9d0a8e7 --- /dev/null +++ b/rsc/.gitignore @@ -0,0 +1,2 @@ +255*.txt +mcstatus.sh diff --git a/rsc/Config.properties b/rsc/Config.properties new file mode 100644 index 0000000..33f533a --- /dev/null +++ b/rsc/Config.properties @@ -0,0 +1,14 @@ +#Sun Mar 13 19:43:48 CET 2022 +status_message_vanilla=890007862456238120 +status_channel=889880296168755231 +restrict_commands_to_channel=bot-muell schrein-auf-den-bot +status_message_eighteen_vanilla=952638126327726150 +status_message_selina=952638127451820042 +mc_server=85.214.148.23 +greetings_and_byebyes_on=true +guild_id=801554100814741524 +status_message_modded=952638124197040139 +status_message_kreativ=952638123324616735 +status_update=30 +path_to_mcstatus=/home/paul/.local/bin/mcstatus +path_to_status_json=/home/paul/gits/YoshiBot/rsc/mcservers.json diff --git a/rsc/Ordnerstruktur.txt b/rsc/Ordnerstruktur.txt new file mode 100644 index 0000000..3800272 --- /dev/null +++ b/rsc/Ordnerstruktur.txt @@ -0,0 +1,11 @@ +rsc + |-- .gitkeep + |-- Ordnerstruktur.txt + |-- Config.properties + |-- tts.py + |-- audio + |-- temp + |-- temp_1.opus + |-- temp_2.opus + |-- audio_1.opus + |-- audio_2.opus \ No newline at end of file diff --git a/rsc/activities.txt b/rsc/activities.txt new file mode 100644 index 0000000..9291b0d --- /dev/null +++ b/rsc/activities.txt @@ -0,0 +1,7 @@ +Haare waschen +Kleine Menschen (Honey) verprügeln +Im Kosovo Hexen verbennen +Magnesiumcarbonat schnupfen +Offene Wunden mit Sekundenkleber verschließen +Sich 'nen saftigen Knackarsch reinzimmern +Spielt Rasputin bei Just Dance diff --git a/rsc/bonks/bonk1.png b/rsc/bonks/bonk1.png new file mode 100644 index 0000000..fd099e1 Binary files /dev/null and b/rsc/bonks/bonk1.png differ diff --git a/rsc/bonks/bonk2.png b/rsc/bonks/bonk2.png new file mode 100644 index 0000000..0d1c650 Binary files /dev/null and b/rsc/bonks/bonk2.png differ diff --git a/rsc/byebyes.txt b/rsc/byebyes.txt new file mode 100644 index 0000000..c55a430 --- /dev/null +++ b/rsc/byebyes.txt @@ -0,0 +1,27 @@ +Verpiss dich %s. +Bye bye %s. +Tschö %s. +Geh kacken %s. +%s verlässt uns unu. +%s ist kurz Halle Peißen. +%s geht Haare waschen. +%s ist kurz den Ofen schrubben. +%s ist ein Verräter. +Algengrütze, %s ist weg. +Walfischdreck, %s ist weg. +Ach verdammt, das Killerkaninchen hat %s erwischt. +%s geht den Agaven-Dickbaum suchen +%s geht Müll mit Kevin aus Oldenburg sammeln. +%s macht nun schmutzige Sachen auf einem anderen Discord. +%s geht nun mit Seliner telefonieren. +%s ist ein Wichser. +Man darf nun über %s lästern. +%s wurde fachgerecht entsorgt. +%s geht jetzt seine Nachbarn teebeuteln. +Oh nein wir haben %s verloren. +Unga bunga, wo %s? +%s hasst jeden hier. +%s hats erwischt. +%s ist aus zu großer Höhe gefallen. +%s hat versucht in Lava zu schwimmen. +%s ist an Pauls Kartoffelsalat gestorben.. diff --git a/rsc/comebacks.txt b/rsc/comebacks.txt new file mode 100644 index 0000000..3cfc3a6 --- /dev/null +++ b/rsc/comebacks.txt @@ -0,0 +1,3 @@ +%s hat jetzt fertig. +%s ist wieder aufnahmebereit. +%s hat sich wieder erholt. \ No newline at end of file diff --git a/rsc/departs.txt b/rsc/departs.txt new file mode 100644 index 0000000..20a41d3 --- /dev/null +++ b/rsc/departs.txt @@ -0,0 +1,4 @@ +%s ist dann mal abwesend. +%s hat jetzt erstmal nix mehr zu sagen. +%s ist verschwunden. +%s kommt bald wieder.... vielleicht. \ No newline at end of file diff --git a/rsc/greetings.txt b/rsc/greetings.txt new file mode 100644 index 0000000..6f994f5 --- /dev/null +++ b/rsc/greetings.txt @@ -0,0 +1,21 @@ +Sei gegrüßt Genosse %s. +Uwu %s ist uns beigetreten. +Gelobt sei %s. +Hurra hurra %s ist da. +%s ist gekommen um uns zu erleuchten. +%s was ist deine Weisheit? +Es erscheine: %s! +Ja ach scheiß doch die Wand an, %s ist da! +%s ist vom Rauchen zurück. +%s beehrt uns juhu tralala. +%s ist gekommen um zu kommen. +%s ich wähle dich! +Zuerst war das nichts, dann %s. +Ist es ein Flugzeug? Ist es ein Vogel? Nein es ist %s. +Hajoa schleck ma ja %s ist da. +%s betritt das Hornyjail. +Moin %s. +%s kommt für billige Unterhaltung. +%s lässt sich für 2 Euro in die Eier treten. +%s ist ein Mann / Männin von Ähre. +%s hat sich entschieden bliat zu kosten. diff --git a/rsc/mcservers.json b/rsc/mcservers.json new file mode 100644 index 0000000..09e491d --- /dev/null +++ b/rsc/mcservers.json @@ -0,0 +1,20 @@ +{"servers": [ + { + "ip": "85.214.148.23:25568", + "name": "MCMuffing™®㋏ Inc.", + "message": 960983446350594049, + "timestamp": "13:43:35 Fr., 15.04.2022" + }, + { + "ip": "85.214.148.23:25566", + "name": "Enigmatica 6: Expert 1.0.0", + "message": 960983448410013737, + "timestamp": "21:25:08 Di., 05.04.2022" + }, + { + "ip": "85.214.148.23:25570", + "name": "Medieval Minecraft 1.16.5 v52", + "message": 960983449961922600, + "timestamp": "11:30:09 So., 10.04.2022" + } +]} \ No newline at end of file diff --git a/rsc/pats/pat1.png b/rsc/pats/pat1.png new file mode 100644 index 0000000..c283cfd Binary files /dev/null and b/rsc/pats/pat1.png differ diff --git a/rsc/pats/pat2.png b/rsc/pats/pat2.png new file mode 100644 index 0000000..66494f0 Binary files /dev/null and b/rsc/pats/pat2.png differ diff --git a/rsc/pats/pat3.png b/rsc/pats/pat3.png new file mode 100644 index 0000000..150223e Binary files /dev/null and b/rsc/pats/pat3.png differ diff --git a/rsc/tts.py b/rsc/tts.py new file mode 100644 index 0000000..2d4a8d4 --- /dev/null +++ b/rsc/tts.py @@ -0,0 +1,18 @@ +import sys +from gtts import gTTS +import os +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("--text") +parser.add_argument("--lang") +parser.add_argument("--out") +args = parser.parse_args() + +mytext = args.text +language = args.lang +output = args.out + +myobj = gTTS(text=mytext, lang=language, slow=False) + +myobj.save(output) \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..edd903e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/6.8.3/userguide/multi_project_builds.html + */ + +rootProject.name = 'YoshiBot' +include('app')