| @ -0,0 +1,145 @@ | |||||
| # Created by https://www.toptal.com/developers/gitignore/api/intellij+all,kotlin,gradle | |||||
| # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,kotlin,gradle | |||||
| ### Intellij+all ### | |||||
| # 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 | |||||
| # AWS User-specific | |||||
| .idea/**/aws.xml | |||||
| # 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/ | |||||
| # mpeltonen/sbt-idea plugin | |||||
| .idea_modules/ | |||||
| # JIRA plugin | |||||
| atlassian-ide-plugin.xml | |||||
| # Cursive Clojure plugin | |||||
| .idea/replstate.xml | |||||
| # SonarLint plugin | |||||
| .idea/sonarlint/ | |||||
| # 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 | |||||
| ### Intellij+all Patch ### | |||||
| # Ignore everything but code style settings and run configurations | |||||
| # that are supposed to be shared within teams. | |||||
| .idea/* | |||||
| !.idea/codeStyles | |||||
| !.idea/runConfigurations | |||||
| ### Kotlin ### | |||||
| # Compiled class file | |||||
| *.class | |||||
| # Log file | |||||
| *.log | |||||
| # BlueJ files | |||||
| *.ctxt | |||||
| # 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* | |||||
| replay_pid* | |||||
| ### 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 | |||||
| # Avoid ignore Gradle wrappper properties | |||||
| !gradle-wrapper.properties | |||||
| # Cache of project | |||||
| .gradletasknamecache | |||||
| # Eclipse Gradle plugin generated files | |||||
| # Eclipse Core | |||||
| .project | |||||
| # JDT-specific (Eclipse Java Development Tools) | |||||
| .classpath | |||||
| ### Gradle Patch ### | |||||
| # Java heap dump | |||||
| *.hprof | |||||
| # End of https://www.toptal.com/developers/gitignore/api/intellij+all,kotlin,gradle | |||||
| @ -0,0 +1,76 @@ | |||||
| import org.jetbrains.kotlin.gradle.tasks.KotlinCompile | |||||
| plugins { | |||||
| idea | |||||
| application | |||||
| kotlin("jvm") version "1.7.20" | |||||
| id("com.github.johnrengelman.shadow") version "7.1.2" | |||||
| id("com.github.gmazzo.buildconfig") version "3.1.0" | |||||
| } | |||||
| val commit = runCommand(arrayListOf("git", "rev-parse", "HEAD")) | |||||
| val changes = runCommand(arrayListOf("git", "diff", "--shortstat")) | |||||
| group = "xyz.brettb.discord.ieeevents" | |||||
| version = "${rootProject.findProperty("major")}.${rootProject.findProperty("minor")}.${rootProject.findProperty("patch")}" | |||||
| application { | |||||
| mainClass.set("xyz.brettb.discord.ieeevents.IEEEventsKt") | |||||
| } | |||||
| buildConfig { | |||||
| className("IEEEventsBotInfo") | |||||
| buildConfigField("String", "VERSION", "\"${version}\"") | |||||
| buildConfigField("String", "COMMIT", "\"$commit\"") | |||||
| buildConfigField("String", "LOCAL_CHANGES", "\"$changes\"") | |||||
| buildConfigField("long", "BUILD_TIME", "${System.currentTimeMillis()}L") | |||||
| } | |||||
| repositories { | |||||
| mavenCentral() | |||||
| maven { | |||||
| name = "brettb-repo" | |||||
| url = uri("https://repo.brettb.xyz") | |||||
| } | |||||
| maven("https://m2.dv8tion.net/releases") { | |||||
| name = "m2-dv8tion" | |||||
| } | |||||
| } | |||||
| dependencies { | |||||
| listOf("stdlib-jdk8", "reflect").forEach { implementation(kotlin(it)) } | |||||
| implementation("net.dv8tion:JDA:4.4.0_350") | |||||
| implementation("tech.junodevs.discord:kriess:0.14.0") | |||||
| implementation("ch.qos.logback:logback-classic:1.4.4") | |||||
| implementation("org.mnode.ical4j:ical4j:3.2.6") | |||||
| implementation("com.squareup.okhttp3:okhttp:4.10.0") | |||||
| // Utilities | |||||
| implementation("org.yaml:snakeyaml:1.31") | |||||
| implementation(kotlin("stdlib-jdk8")) | |||||
| } | |||||
| tasks.test { | |||||
| useJUnitPlatform() | |||||
| } | |||||
| tasks.withType<KotlinCompile> { | |||||
| kotlinOptions.jvmTarget = "1.8" | |||||
| } | |||||
| fun runCommand(commands: List<String>): String { | |||||
| val stdout = org.apache.commons.io.output.ByteArrayOutputStream() | |||||
| exec { | |||||
| commandLine = commands | |||||
| standardOutput = stdout | |||||
| } | |||||
| return stdout.toString("utf-8").trim() | |||||
| } | |||||
| @ -0,0 +1,5 @@ | |||||
| major=0 | |||||
| minor=1 | |||||
| patch=0 | |||||
| kotlin.code.style=official | |||||
| @ -0,0 +1,5 @@ | |||||
| distributionBase=GRADLE_USER_HOME | |||||
| distributionPath=wrapper/dists | |||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip | |||||
| zipStoreBase=GRADLE_USER_HOME | |||||
| zipStorePath=wrapper/dists | |||||
| @ -0,0 +1,234 @@ | |||||
| #!/bin/sh | |||||
| # | |||||
| # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. | |||||
| # | |||||
| # Important for running: | |||||
| # | |||||
| # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is | |||||
| # noncompliant, but you have some other compliant shell such as ksh or | |||||
| # bash, then to run this script, type that shell name before the whole | |||||
| # command line, like: | |||||
| # | |||||
| # ksh Gradle | |||||
| # | |||||
| # Busybox and similar reduced shells will NOT work, because this script | |||||
| # requires all of these POSIX shell features: | |||||
| # * functions; | |||||
| # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», | |||||
| # «${var#prefix}», «${var%suffix}», and «$( cmd )»; | |||||
| # * compound commands having a testable exit status, especially «case»; | |||||
| # * various built-in commands including «command», «set», and «ulimit». | |||||
| # | |||||
| # Important for patching: | |||||
| # | |||||
| # (2) This script targets any POSIX shell, so it avoids extensions provided | |||||
| # by Bash, Ksh, etc; in particular arrays are avoided. | |||||
| # | |||||
| # The "traditional" practice of packing multiple parameters into a | |||||
| # space-separated string is a well documented source of bugs and security | |||||
| # problems, so this is (mostly) avoided, by progressively accumulating | |||||
| # options in "$@", and eventually passing that to Java. | |||||
| # | |||||
| # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, | |||||
| # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; | |||||
| # see the in-line comments for details. | |||||
| # | |||||
| # There are tweaks for specific operating systems such as AIX, CygWin, | |||||
| # Darwin, MinGW, and NonStop. | |||||
| # | |||||
| # (3) This script is generated from the Groovy template | |||||
| # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt | |||||
| # within the Gradle project. | |||||
| # | |||||
| # You can find Gradle at https://github.com/gradle/gradle/. | |||||
| # | |||||
| ############################################################################## | |||||
| # Attempt to set APP_HOME | |||||
| # Resolve links: $0 may be a link | |||||
| app_path=$0 | |||||
| # Need this for daisy-chained symlinks. | |||||
| while | |||||
| APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path | |||||
| [ -h "$app_path" ] | |||||
| do | |||||
| ls=$( ls -ld "$app_path" ) | |||||
| link=${ls#*' -> '} | |||||
| case $link in #( | |||||
| /*) app_path=$link ;; #( | |||||
| *) app_path=$APP_HOME$link ;; | |||||
| esac | |||||
| done | |||||
| APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit | |||||
| APP_NAME="Gradle" | |||||
| APP_BASE_NAME=${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 "$*" | |||||
| } >&2 | |||||
| die () { | |||||
| echo | |||||
| echo "$*" | |||||
| echo | |||||
| exit 1 | |||||
| } >&2 | |||||
| # 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 ;; #( | |||||
| MSYS* | 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" && ! "$darwin" && ! "$nonstop" ; then | |||||
| case $MAX_FD in #( | |||||
| max*) | |||||
| MAX_FD=$( ulimit -H -n ) || | |||||
| warn "Could not query maximum file descriptor limit" | |||||
| esac | |||||
| case $MAX_FD in #( | |||||
| '' | soft) :;; #( | |||||
| *) | |||||
| ulimit -n "$MAX_FD" || | |||||
| warn "Could not set maximum file descriptor limit to $MAX_FD" | |||||
| esac | |||||
| fi | |||||
| # Collect all arguments for the java command, stacking in reverse order: | |||||
| # * args from the command line | |||||
| # * the main class name | |||||
| # * -classpath | |||||
| # * -D...appname settings | |||||
| # * --module-path (only if needed) | |||||
| # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. | |||||
| # For Cygwin or MSYS, switch paths to Windows format before running java | |||||
| if "$cygwin" || "$msys" ; then | |||||
| APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) | |||||
| CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) | |||||
| JAVACMD=$( cygpath --unix "$JAVACMD" ) | |||||
| # Now convert the arguments - kludge to limit ourselves to /bin/sh | |||||
| for arg do | |||||
| if | |||||
| case $arg in #( | |||||
| -*) false ;; # don't mess with options #( | |||||
| /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath | |||||
| [ -e "$t" ] ;; #( | |||||
| *) false ;; | |||||
| esac | |||||
| then | |||||
| arg=$( cygpath --path --ignore --mixed "$arg" ) | |||||
| fi | |||||
| # Roll the args list around exactly as many times as the number of | |||||
| # args, so each arg winds up back in the position where it started, but | |||||
| # possibly modified. | |||||
| # | |||||
| # NB: a `for` loop captures its iteration list before it begins, so | |||||
| # changing the positional parameters here affects neither the number of | |||||
| # iterations, nor the values presented in `arg`. | |||||
| shift # remove old arg | |||||
| set -- "$@" "$arg" # push replacement arg | |||||
| done | |||||
| fi | |||||
| # Collect all arguments for the java command; | |||||
| # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of | |||||
| # shell script including quotes and variable substitutions, so put them in | |||||
| # double quotes to make sure that they get re-expanded; and | |||||
| # * put everything else in single quotes, so that it's not re-expanded. | |||||
| set -- \ | |||||
| "-Dorg.gradle.appname=$APP_BASE_NAME" \ | |||||
| -classpath "$CLASSPATH" \ | |||||
| org.gradle.wrapper.GradleWrapperMain \ | |||||
| "$@" | |||||
| # Use "xargs" to parse quoted args. | |||||
| # | |||||
| # With -n1 it outputs one arg per line, with the quotes and backslashes removed. | |||||
| # | |||||
| # In Bash we could simply go: | |||||
| # | |||||
| # readarray ARGS < <( xargs -n1 <<<"$var" ) && | |||||
| # set -- "${ARGS[@]}" "$@" | |||||
| # | |||||
| # but POSIX shell has neither arrays nor command substitution, so instead we | |||||
| # post-process each arg (as a line of input to sed) to backslash-escape any | |||||
| # character that might be a shell metacharacter, then use eval to reverse | |||||
| # that process (while maintaining the separation between arguments), and wrap | |||||
| # the whole thing up as a single "set" statement. | |||||
| # | |||||
| # This will of course break if any of these variables contains a newline or | |||||
| # an unmatched quote. | |||||
| # | |||||
| eval "set -- $( | |||||
| printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | | |||||
| xargs -n1 | | |||||
| sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | | |||||
| tr '\n' ' ' | |||||
| )" '"$@"' | |||||
| exec "$JAVACMD" "$@" | |||||
| @ -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 | |||||
| @ -0,0 +1,4 @@ | |||||
| 515646289250222098: | |||||
| prefix: '-' | |||||
| eventChannelID: null | |||||
| mirrorEvents: false | |||||
| @ -0,0 +1,3 @@ | |||||
| rootProject.name = "IEEEvents" | |||||
| @ -0,0 +1,9 @@ | |||||
| package xyz.brettb.discord.ieeevents | |||||
| object Constants { | |||||
| object Colors { | |||||
| val BLUE = 0x0066A1 | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,194 @@ | |||||
| package xyz.brettb.discord.ieeevents | |||||
| import ch.qos.logback.classic.Level | |||||
| import ch.qos.logback.classic.Logger | |||||
| import net.dv8tion.jda.api.JDA | |||||
| import net.dv8tion.jda.api.JDABuilder | |||||
| import net.dv8tion.jda.api.Permission | |||||
| import net.dv8tion.jda.api.events.GenericEvent | |||||
| import net.dv8tion.jda.api.events.ReadyEvent | |||||
| import net.dv8tion.jda.api.hooks.EventListener | |||||
| import net.dv8tion.jda.api.requests.GatewayIntent | |||||
| import net.dv8tion.jda.api.utils.ChunkingFilter | |||||
| import net.dv8tion.jda.api.utils.MemberCachePolicy | |||||
| import net.fortuna.ical4j.data.CalendarBuilder | |||||
| import net.fortuna.ical4j.filter.predicate.PeriodRule | |||||
| import net.fortuna.ical4j.model.Calendar | |||||
| import net.fortuna.ical4j.model.Date | |||||
| import net.fortuna.ical4j.model.DateTime | |||||
| import net.fortuna.ical4j.model.Period | |||||
| import net.fortuna.ical4j.model.component.VEvent | |||||
| import org.slf4j.LoggerFactory | |||||
| import org.yaml.snakeyaml.Yaml | |||||
| import tech.junodevs.discord.kriess.impl.managers.CommandManager | |||||
| import tech.junodevs.discord.kriess.menus.MenuListener | |||||
| import xyz.brettb.discord.ieeevents.commands.CommandBase | |||||
| import xyz.brettb.discord.ieeevents.commands.info.HelpCommand | |||||
| import xyz.brettb.discord.ieeevents.commands.info.UpcomingEventsCommand | |||||
| import xyz.brettb.discord.ieeevents.commands.settings.ChangePrefixCommand | |||||
| import xyz.brettb.discord.ieeevents.data.settings.IEEEventsGuildSettings | |||||
| import xyz.brettb.discord.ieeevents.data.settings.IEEEventsGuildSettingsManager | |||||
| import xyz.brettb.discord.ieeevents.services.StatusService | |||||
| import java.io.File | |||||
| import java.io.FileInputStream | |||||
| import java.nio.file.Files | |||||
| import java.time.Duration | |||||
| import java.time.temporal.ChronoUnit | |||||
| import kotlin.system.exitProcess | |||||
| val logger = LoggerFactory.getLogger(IEEEventsBot.javaClass) | |||||
| fun main() { | |||||
| logger.info("IEEEvents Bot") | |||||
| IEEEventsBot.load() | |||||
| IEEEventsBot.JDA = JDABuilder | |||||
| .createDefault(IEEEventsBot.token) | |||||
| .enableIntents(GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MEMBERS) | |||||
| .addEventListeners(IEEEventsBot.commandManager, IEEEventsBot, MenuListener) | |||||
| .setChunkingFilter(ChunkingFilter.ALL) | |||||
| .setMemberCachePolicy(MemberCachePolicy.ALL) | |||||
| .build() | |||||
| // Give the person running the bot an invite URL | |||||
| logger.info( | |||||
| "Invite @ ${ | |||||
| IEEEventsBot.JDA.getInviteUrl( | |||||
| Permission.MESSAGE_WRITE, | |||||
| Permission.MESSAGE_MANAGE, | |||||
| Permission.MESSAGE_EMBED_LINKS, | |||||
| Permission.MESSAGE_ATTACH_FILES, | |||||
| Permission.MESSAGE_ADD_REACTION | |||||
| ) | |||||
| }" | |||||
| ) | |||||
| // Ensure the StatusService stops correctly | |||||
| Runtime.getRuntime().addShutdownHook(Thread { | |||||
| StatusService.shutdown() | |||||
| }) | |||||
| } | |||||
| object IEEEventsBot : EventListener { | |||||
| /** | |||||
| * The [JDA] instance used by the bot. | |||||
| */ | |||||
| lateinit var JDA: JDA | |||||
| /** | |||||
| * The [CommandManager] used by the bot. | |||||
| */ | |||||
| lateinit var commandManager: CommandManager<IEEEventsGuildSettings> | |||||
| /** | |||||
| * The YAML instance used. | |||||
| */ | |||||
| private val yaml = Yaml() | |||||
| /** | |||||
| * The bot's default prefix. | |||||
| */ | |||||
| lateinit var prefix: String | |||||
| /** | |||||
| * The bot's login token. | |||||
| */ | |||||
| lateinit var token: String | |||||
| /** | |||||
| * The calendar url that we're attempting to mirror between discord + the source. | |||||
| */ | |||||
| lateinit var calendarUrl: String | |||||
| /** | |||||
| * The iCal [Calendar] object gotten from the [calendarUrl]. | |||||
| */ | |||||
| val calendar: Calendar? | |||||
| get() = | |||||
| try { | |||||
| val inputStream = Utils.downloadCalendar(calendarUrl) | |||||
| CalendarBuilder().build(inputStream) | |||||
| } catch (t: Throwable) { | |||||
| null | |||||
| } | |||||
| /** | |||||
| * The list of iCal [VEvent]s on the [calendar]. | |||||
| */ | |||||
| val events: Collection<VEvent>? | |||||
| get() = | |||||
| try { | |||||
| val cal = calendar | |||||
| var events = cal!!.getComponents<VEvent>("VEVENT") | |||||
| val period = Period(DateTime(Date().time), Duration.of(60, ChronoUnit.DAYS)) | |||||
| val rule = PeriodRule<VEvent>(period) | |||||
| events = events.filter { rule.test(it) } | |||||
| events = events.sortedBy { it.startDate } | |||||
| events | |||||
| } catch (t: Throwable) { | |||||
| null | |||||
| } | |||||
| /** | |||||
| * Is the bot going to update its status? | |||||
| */ | |||||
| var updateStatus: Boolean = true | |||||
| /** | |||||
| * The commands the bot has. | |||||
| */ | |||||
| val commands: List<CommandBase> = listOf( | |||||
| // SetEventsChannelCommand, | |||||
| HelpCommand, | |||||
| ChangePrefixCommand, | |||||
| UpcomingEventsCommand, | |||||
| // PingCommand | |||||
| ) | |||||
| /** | |||||
| * Initializes the bot | |||||
| */ | |||||
| fun load() { | |||||
| // Configuration | |||||
| val configFile = File("config.yml") | |||||
| if (!configFile.exists()) { | |||||
| javaClass.getResourceAsStream("/config.yml")?.let { Files.copy(it, configFile.toPath()) } | |||||
| logger.error("No config file found! Default copied to current directory.") | |||||
| exitProcess(1) | |||||
| } | |||||
| val output = yaml.load(FileInputStream(configFile)) as Map<String, Any> | |||||
| prefix = output["prefix"].toString() | |||||
| token = output["token"].toString() | |||||
| updateStatus = output["update-status"]?.toString()?.toBoolean() ?: true | |||||
| calendarUrl = output["calendar-url"].toString() | |||||
| val logLevel = output["log-level"]?.toString() ?: "INFO" | |||||
| val level = Level.toLevel(logLevel, Level.INFO) | |||||
| (LoggerFactory.getLogger("ROOT") as Logger).level = level | |||||
| // Managers | |||||
| IEEEventsGuildSettingsManager.start() | |||||
| commandManager = CommandManager(IEEEventsGuildSettingsManager, prefix) { cEvent, t -> | |||||
| cEvent.replyError("An unknown error occurred!", { }, { }) | |||||
| logger.error("An uncaught exception occurred in a command: $t") | |||||
| } | |||||
| // Initialize the commands | |||||
| commands.forEach { command -> | |||||
| commandManager.addCommand(command) | |||||
| } | |||||
| } | |||||
| override fun onEvent(event: GenericEvent) { | |||||
| when (event) { | |||||
| is ReadyEvent -> { | |||||
| StatusService.start() | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,34 @@ | |||||
| package xyz.brettb.discord.ieeevents | |||||
| import okhttp3.OkHttpClient | |||||
| import okhttp3.Request | |||||
| import java.io.InputStream | |||||
| import java.net.HttpURLConnection | |||||
| import java.util.concurrent.TimeUnit | |||||
| object Utils { | |||||
| fun downloadCalendar(url: String): InputStream { | |||||
| val client = OkHttpClient.Builder() | |||||
| .connectTimeout(5, TimeUnit.SECONDS) | |||||
| .readTimeout(5, TimeUnit.SECONDS) | |||||
| .build() | |||||
| val request = Request.Builder() | |||||
| .url(url) | |||||
| .build() | |||||
| val response = client.newCall(request).execute() | |||||
| val body = response.body | |||||
| val resCode = response.code | |||||
| if (resCode >= HttpURLConnection.HTTP_OK && | |||||
| resCode < HttpURLConnection.HTTP_MULT_CHOICE && | |||||
| body != null) { | |||||
| return body.byteStream() | |||||
| } else { | |||||
| throw error("failed to download file") | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,76 @@ | |||||
| package xyz.brettb.discord.ieeevents.commands | |||||
| import net.dv8tion.jda.api.Permission | |||||
| import tech.junodevs.discord.kriess.command.Command | |||||
| import tech.junodevs.discord.kriess.command.CommandCategory | |||||
| import tech.junodevs.discord.kriess.command.CommandEvent | |||||
| import tech.junodevs.discord.kriess.exceptions.MissingArgumentException | |||||
| import xyz.brettb.discord.ieeevents.logger | |||||
| import java.util.concurrent.TimeUnit | |||||
| abstract class CommandBase( | |||||
| name: String, | |||||
| description: String, | |||||
| category: CommandCategory, | |||||
| aliases: List<String> = listOf(), | |||||
| arguments: String = "", | |||||
| showInHelp: Boolean = true, | |||||
| ownerOnly: Boolean = false, | |||||
| val permissions: List<Permission> = listOf(), | |||||
| ) : Command(name, aliases, arguments, description, category, showInHelp, ownerOnly) { | |||||
| val realUsage: String by lazy { | |||||
| if (this.arguments.isEmpty()) name else { | |||||
| name + " " + this.arguments.joinToString(" ") { it.usage } | |||||
| } | |||||
| } | |||||
| fun checkPermission(event: CommandEvent): Boolean { | |||||
| if (event.isOwner) return true | |||||
| if (ownerOnly) return false | |||||
| if (!event.member.hasPermission(event.textChannel, permissions)) return false | |||||
| return true | |||||
| } | |||||
| override fun preHandle(event: CommandEvent): Boolean { | |||||
| // Check for required arguments! | |||||
| try { | |||||
| event.arguments | |||||
| } catch (t: MissingArgumentException) { | |||||
| event.replyError( | |||||
| "It looks like the required argument **${t.argument.name}** is missing\n Please check the **usage** and try again.", | |||||
| { | |||||
| it.delete().queueAfter(5, TimeUnit.SECONDS) | |||||
| }) | |||||
| return false | |||||
| } | |||||
| if (!checkPermission(event)) { | |||||
| if (ownerOnly) { | |||||
| event.replyError( | |||||
| "Only the bot owner can run that command!", | |||||
| { | |||||
| it.delete().queueAfter(5, TimeUnit.SECONDS) | |||||
| }) | |||||
| } else { | |||||
| event.replyError( | |||||
| "It looks like you don't have the proper permissions for that command!\n**Needed:** ${ | |||||
| permissions.joinToString( | |||||
| ", " | |||||
| ) { "`${it.getName()}`" } | |||||
| }", | |||||
| { | |||||
| it.delete().queueAfter(5, TimeUnit.SECONDS) | |||||
| }) | |||||
| } | |||||
| return false | |||||
| } | |||||
| logger.info("${event.command.name} | ${event.author.asTag} [${event.author.id}] | ${event.textChannel.name} [${event.textChannel.id}] | ${event.guild.name} [${event.guild.id}] | ${event.message.contentRaw}") | |||||
| return true | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,8 @@ | |||||
| package xyz.brettb.discord.ieeevents.commands | |||||
| import tech.junodevs.discord.kriess.command.CommandCategory | |||||
| object CommandCategories { | |||||
| val INFO = CommandCategory("Information") | |||||
| val SETTINGS = CommandCategory("Settings") | |||||
| } | |||||
| @ -0,0 +1,70 @@ | |||||
| package xyz.brettb.discord.ieeevents.commands.info | |||||
| import net.dv8tion.jda.api.EmbedBuilder | |||||
| import net.dv8tion.jda.api.entities.MessageEmbed | |||||
| import tech.junodevs.discord.kriess.command.CommandEvent | |||||
| import tech.junodevs.discord.kriess.menus.PaginationOptions | |||||
| import tech.junodevs.discord.kriess.menus.PaginatorMenu | |||||
| import xyz.brettb.discord.ieeevents.Constants | |||||
| import xyz.brettb.discord.ieeevents.IEEEventsBot | |||||
| import xyz.brettb.discord.ieeevents.commands.CommandBase | |||||
| import xyz.brettb.discord.ieeevents.commands.CommandCategories | |||||
| import xyz.brettb.discord.ieeevents.data.settings.settings | |||||
| import java.util.concurrent.TimeUnit | |||||
| object HelpCommand : CommandBase( | |||||
| "help", | |||||
| "Get some help on the bot", | |||||
| CommandCategories.INFO, | |||||
| listOf("h"), | |||||
| "[command]" | |||||
| ) { | |||||
| override fun handle(event: CommandEvent) { | |||||
| val prefix = event.guild.settings.get().realPrefix | |||||
| if (event.arguments.contains("command")) { | |||||
| val command = event.arguments.command("command") as CommandBase | |||||
| event.reply(EmbedBuilder().run { | |||||
| setDescription("IEEEvents Help") | |||||
| setColor(Constants.Colors.BLUE) | |||||
| val aliases = | |||||
| if (command.aliases.isNotEmpty()) command.aliases.joinToString(", ") { "`$it`" } else "**No Aliases**" | |||||
| addField( | |||||
| command.name, | |||||
| "Description: **${command.description}**\nAliases: ${aliases}\nUsage: `${command.realUsage}`", | |||||
| false | |||||
| ) | |||||
| setFooter("Arguments in [] are optional | Requested by: ${event.author.asTag}") | |||||
| build() | |||||
| }) | |||||
| } else { | |||||
| val commands = | |||||
| event.commandManager.getCommands() | |||||
| .filter { it.showInHelp && (it as CommandBase).checkPermission(event) } | |||||
| .groupBy { it.category }.toSortedMap(Comparator.comparingInt { it.priority }) | |||||
| val fields = commands.map { (category, commands) -> | |||||
| MessageEmbed.Field( | |||||
| category.name, | |||||
| commands.joinToString("\n") { "$prefix${it.name}" }, | |||||
| true | |||||
| ) | |||||
| }.chunked(1) | |||||
| PaginatorMenu( | |||||
| event.textChannel, | |||||
| event.author, | |||||
| "IEEEvents Help", | |||||
| PaginationOptions(TimeUnit.MINUTES.toMillis(1), embedColor = Constants.Colors.BLUE), | |||||
| fields | |||||
| ).begin() | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,89 @@ | |||||
| package xyz.brettb.discord.ieeevents.commands.info | |||||
| import net.dv8tion.jda.api.EmbedBuilder | |||||
| import net.dv8tion.jda.api.entities.MessageEmbed | |||||
| import net.fortuna.ical4j.model.component.VEvent | |||||
| import tech.junodevs.discord.kriess.command.CommandEvent | |||||
| import tech.junodevs.discord.kriess.menus.PaginationOptions | |||||
| import tech.junodevs.discord.kriess.menus.PaginatorMenu | |||||
| import xyz.brettb.discord.ieeevents.Constants | |||||
| import xyz.brettb.discord.ieeevents.IEEEventsBot | |||||
| import xyz.brettb.discord.ieeevents.commands.CommandBase | |||||
| import xyz.brettb.discord.ieeevents.commands.CommandCategories | |||||
| import java.time.ZoneId | |||||
| import java.time.format.DateTimeFormatter | |||||
| import java.util.concurrent.TimeUnit | |||||
| object UpcomingEventsCommand : CommandBase( | |||||
| "upcoming", | |||||
| "Get a list of upcoming events", | |||||
| CommandCategories.INFO, | |||||
| listOf("uc") | |||||
| ) { | |||||
| // Date Formatter of pattern: Wed Nov 2 2022 @ 6:00pm | |||||
| private val df = DateTimeFormatter.ofPattern("EEE MMM d uuuu '@' hh:mma") | |||||
| override fun handle(event: CommandEvent) { | |||||
| val events = try { | |||||
| IEEEventsBot.events | |||||
| } catch (t: Throwable) { | |||||
| event.replyError("Failed to retrieve upcoming events!") | |||||
| return | |||||
| }!! | |||||
| if (events.isNotEmpty()) { | |||||
| if (events.size > 1) { | |||||
| val fields = events.map { calEvent -> | |||||
| MessageEmbed.Field( | |||||
| calEvent.summary.value.toString(), | |||||
| createEventString(calEvent), | |||||
| false | |||||
| ) | |||||
| }.chunked(3) | |||||
| PaginatorMenu( | |||||
| event.textChannel, | |||||
| event.author, | |||||
| "IEEE Upcoming Events", | |||||
| PaginationOptions(TimeUnit.MINUTES.toMillis(1), embedColor = Constants.Colors.BLUE), | |||||
| fields | |||||
| ).begin() | |||||
| } else { | |||||
| val calEvent = events.first() | |||||
| event.reply( | |||||
| EmbedBuilder() | |||||
| .setTitle("IEEE Upcoming Events") | |||||
| .addField( | |||||
| calEvent.summary.value.toString(), | |||||
| createEventString(calEvent), | |||||
| false | |||||
| ) | |||||
| .build() | |||||
| ) | |||||
| } | |||||
| } else { | |||||
| event.reply( | |||||
| EmbedBuilder() | |||||
| .setTitle("IEEE Upcoming Events") | |||||
| .setDescription("There are no upcoming events within the next 60 days.") | |||||
| .setColor(Constants.Colors.BLUE) | |||||
| .build() | |||||
| ) | |||||
| } | |||||
| } | |||||
| private fun createEventString(calEvent: VEvent): String { | |||||
| var temp = "From **${ | |||||
| df.format(calEvent.startDate.date.toInstant().atZone(ZoneId.systemDefault())) | |||||
| }** until **${df.format(calEvent.endDate.date.toInstant().atZone(ZoneId.systemDefault()))}**\n" | |||||
| temp += calEvent.description.value.toString().substring(0..256 - temp.length) + "...\n" + calEvent.url.value | |||||
| return temp | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,32 @@ | |||||
| package xyz.brettb.discord.ieeevents.commands.settings | |||||
| import net.dv8tion.jda.api.Permission | |||||
| import tech.junodevs.discord.kriess.command.CommandEvent | |||||
| import xyz.brettb.discord.ieeevents.commands.CommandBase | |||||
| import xyz.brettb.discord.ieeevents.commands.CommandCategories | |||||
| import xyz.brettb.discord.ieeevents.data.settings.IEEEventsGuildSettingsManager | |||||
| object ChangePrefixCommand : CommandBase( | |||||
| "changeprefix", | |||||
| "Change the bot prefix", | |||||
| CommandCategories.SETTINGS, | |||||
| listOf("cp"), | |||||
| "[new_prefix:text]", | |||||
| true, | |||||
| false, | |||||
| listOf(Permission.MANAGE_SERVER) | |||||
| ) { | |||||
| override fun handle(event: CommandEvent) { | |||||
| IEEEventsGuildSettingsManager.editSettings(event.guild) { | |||||
| if (event.arguments.contains("new_prefix") && !event.arguments.text("new_prefix").equals("none")) { | |||||
| prefix = event.arguments.text("new_prefix") | |||||
| event.reply(":white_check_mark: Updated prefix to `$prefix`!") | |||||
| } else { | |||||
| prefix = null | |||||
| event.reply(":white_check_mark: Removed custom prefix, you are now using the default: $realPrefix") | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,28 @@ | |||||
| package xyz.brettb.discord.ieeevents.data.settings | |||||
| import net.dv8tion.jda.api.entities.Guild | |||||
| import net.dv8tion.jda.api.entities.TextChannel | |||||
| import tech.junodevs.discord.kriess.providers.GuildSettingsProvider | |||||
| import xyz.brettb.discord.ieeevents.IEEEventsBot | |||||
| import java.util.concurrent.CompletableFuture | |||||
| class IEEEventsGuildSettings(val guildid: Long, var prefix: String? = null, val eventChannelID: Long? = null, var mirrorEvents: Boolean = false) : GuildSettingsProvider { | |||||
| override val realPrefix: String | |||||
| get() = prefix ?: IEEEventsBot.prefix | |||||
| val eventChannel: TextChannel? | |||||
| get() = if (eventChannelID != null) IEEEventsBot.JDA.getTextChannelById(eventChannelID) else null | |||||
| override fun toMap(): Map<String, Any?> { | |||||
| return mapOf( | |||||
| "prefix" to prefix, | |||||
| "eventChannelID" to eventChannelID, | |||||
| "mirrorEvents" to mirrorEvents, | |||||
| ) | |||||
| } | |||||
| } | |||||
| val Guild.settings: CompletableFuture<IEEEventsGuildSettings> | |||||
| get() = IEEEventsGuildSettingsManager.getSettingsFor(this) | |||||
| @ -0,0 +1,20 @@ | |||||
| package xyz.brettb.discord.ieeevents.data.settings | |||||
| import tech.junodevs.discord.kriess.impl.managers.GuildSettingsManager | |||||
| object IEEEventsGuildSettingsManager : GuildSettingsManager<IEEEventsGuildSettings>() { | |||||
| override fun createAbsentInstance(guildId: Long): IEEEventsGuildSettings { | |||||
| return IEEEventsGuildSettings(guildId) | |||||
| } | |||||
| override fun createInstance(guildId: Long, properties: Map<String, Any?>): IEEEventsGuildSettings { | |||||
| return IEEEventsGuildSettings( | |||||
| guildId, | |||||
| properties["prefix"] as String?, | |||||
| properties["eventChannelID"] as Long?, | |||||
| properties["mirrorEvents"] as Boolean? ?: false | |||||
| ) | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,25 @@ | |||||
| package xyz.brettb.discord.ieeevents.services | |||||
| import tech.junodevs.discord.kriess.services.Service | |||||
| import xyz.brettb.discord.ieeevents.IEEEventsBot | |||||
| import xyz.brettb.discord.ieeevents.data.settings.settings | |||||
| import java.util.concurrent.TimeUnit | |||||
| object EventsService : Service(TimeUnit.MINUTES.toSeconds(30), 15) { | |||||
| override fun execute() { | |||||
| val events = IEEEventsBot.events | |||||
| IEEEventsBot.JDA.guilds.forEach { guild -> | |||||
| if (guild.settings.get().mirrorEvents) { | |||||
| // Go through events in events | |||||
| /// Ensure each event exists | |||||
| //// If not, create + send announcement message | |||||
| /// Ensure Description (With URL (UID on last line)) + Location Matches | |||||
| //// If not, update | |||||
| /// Ensure start + end time match | |||||
| //// If not, update | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,21 @@ | |||||
| package xyz.brettb.discord.ieeevents.services | |||||
| import net.dv8tion.jda.api.OnlineStatus | |||||
| import net.dv8tion.jda.api.entities.Activity | |||||
| import tech.junodevs.discord.kriess.services.Service | |||||
| import xyz.brettb.discord.ieeevents.IEEEventsBot | |||||
| object StatusService : Service(60, 0) { | |||||
| override fun execute() { | |||||
| if (IEEEventsBot.updateStatus) { | |||||
| val events = IEEEventsBot.events | |||||
| IEEEventsBot.JDA.presence.setPresence( | |||||
| OnlineStatus.DO_NOT_DISTURB, Activity.watching( | |||||
| "over ${events?.size ?: "unknown"} upcoming events" | |||||
| ) | |||||
| ) | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,6 @@ | |||||
| prefix: "!" | |||||
| token: "12396510498gmj0194c8g1m0394gjm" | |||||
| update-status: true | |||||
| calendar-url: "icalURL" | |||||
| log-level: "INFO" | |||||
| @ -0,0 +1,9 @@ | |||||
| ical4j.unfolding.relaxed=true | |||||
| ical4j.parsing.relaxed=true | |||||
| ical4j.validation.relaxed=true | |||||
| ical4j.compatibility.outlook=true | |||||
| ical4j.compatibility.notes=true | |||||
| @ -0,0 +1,28 @@ | |||||
| <configuration> | |||||
| <!-- Prevent configuration information from printing at startup --> | |||||
| <statusListener class="ch.qos.logback.core.status.NopStatusListener" /> | |||||
| <appender name="Console" class="ch.qos.logback.core.ConsoleAppender"> | |||||
| <encoder> | |||||
| <pattern>%yellow([%d{HH:mm:ss.SSS}]) %highlight(%-5level) %green([%t]) %cyan(%c{0}) %boldRed(::) %msg%n</pattern> | |||||
| </encoder> | |||||
| </appender> | |||||
| <appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender"> | |||||
| <file>logs/latest.log</file> | |||||
| <immediateFlush>true</immediateFlush> | |||||
| <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> | |||||
| <fileNamePattern>logs/%d{yyyy,aux}/%d{MM,aux}/%d{ddMMyy}.log.gz</fileNamePattern> | |||||
| </rollingPolicy> | |||||
| <encoder> | |||||
| <pattern>[%d{MM.dd.yy HH:mm:ss.SSS}] %-5level [%t] %c :: %msg%n</pattern> | |||||
| </encoder> | |||||
| </appender> | |||||
| <root level="info"> | |||||
| <appender-ref ref="Console" /> | |||||
| <appender-ref ref="File" /> | |||||
| </root> | |||||
| </configuration> | |||||