Initial commit

This commit is contained in:
Julius 2020-09-03 15:41:55 +02:00
commit 423e9269ee
53 changed files with 1949 additions and 0 deletions

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
.idea/
.gradle/
build/
out/
test.db
gradle/
gradlew
gradlew.bat
.gitignore

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.idea/
.gradle/
build/
out/
test.db
dump/
local.properties

27
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,27 @@
image: docker:latest
services:
- docker:dind
stages:
- build
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:latest
NAME: $CI_PROJECT_NAME
DOCKER_BUILDKIT: 1
build_and_push:
stage: build
only:
- master
script:
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.voidcorp.nl
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
only_build:
stage: build
except:
- master
script:
- docker build -t $IMAGE_TAG .

12
Dockerfile Normal file
View file

@ -0,0 +1,12 @@
FROM gradle:6.6-jdk14 AS builder
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle distTar
FROM adoptopenjdk:14-jre-hotspot
COPY --from=builder /home/gradle/src/build/distributions/ottobot.tar /app/ottobot.tar
WORKDIR /app
RUN tar -xvf ottobot.tar
WORKDIR /app/ottobot
CMD bin/ottobot

46
build.gradle.kts Normal file
View file

@ -0,0 +1,46 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
application
kotlin("jvm") version "1.4.0"
}
group = "nl.voidcorp.ottobot"
version = "1.0-SNAPSHOT"
repositories {
jcenter()
}
fun DependencyHandlerScope.exposed(part: String) = implementation("org.jetbrains.exposed", part, "0.27.1")
dependencies {
implementation("net.dv8tion:JDA:4.2.0_200") {
exclude(module = "opus-java")
}
implementation("io.ktor:ktor-client-apache:1.4.0")
implementation("io.ktor:ktor-client-jackson:1.4.0")
implementation("ch.qos.logback:logback-classic:1.2.3")
exposed("exposed-core")
exposed("exposed-dao")
exposed("exposed-jdbc")
implementation("org.xerial:sqlite-jdbc:3.30.1")
implementation("com.impossibl.pgjdbc-ng:pgjdbc-ng:0.8.3")
implementation(kotlin("reflect"))
}
tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "13"
}
tasks {
distTar {
archiveFileName.set("ottobot.tar")
}
}
application {
applicationName = "ottobot"
mainClassName = "nl.voidcorp.ottobot.MainKt"
}

1
gradle.properties Normal file
View file

@ -0,0 +1 @@
kotlin.code.style=official

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

183
gradlew vendored Normal file
View file

@ -0,0 +1,183 @@
#!/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" "$@"

103
gradlew.bat vendored Normal file
View file

@ -0,0 +1,103 @@
@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 init
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 init
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
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
: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

3
settings.gradle.kts Normal file
View file

@ -0,0 +1,3 @@
rootProject.name = "ottobot-v3"

View file

@ -0,0 +1,55 @@
package nl.voidcorp.ottobot
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.sharding.DefaultShardManagerBuilder
import net.dv8tion.jda.api.sharding.ShardManager
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.database.*
import nl.voidcorp.ottobot.events.CommandListener
import nl.voidcorp.ottobot.events.DisconnectListenerAdapter
import nl.voidcorp.ottobot.events.OttoListener
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.sql.Connection
val logger: Logger = LoggerFactory.getLogger("OttoBot")
const val creator = 168743656738521088
lateinit var jda: ShardManager
fun main() {
val token: String = System.getenv("DISCORD_TOKEN") ?: throw RuntimeException("Missing DISCORD_TOKEN in env!")
Database.connect(
System.getenv("JDBC_URL") ?: "jdbc:sqlite:test.db",
System.getenv("JDBC_DRIVER") ?: "org.sqlite.JDBC",
System.getenv("JDBC_USER") ?: "",
System.getenv("JDBC_PASSWORD") ?: ""
)
TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE
transaction {
SchemaUtils.createMissingTablesAndColumns(GuildStores)
SchemaUtils.createMissingTablesAndColumns(
GuildStoreAdminRoles,
GuildStoreBotChannels,
GuildStoreModeratorRoles,
GuildStoreRoleMap
)
}
startBot(token)
}
fun startBot(token: String) {
val builder = DefaultShardManagerBuilder.createDefault(token)
builder.addEventListeners(CommandListener(Command.allCommands), DisconnectListenerAdapter, OttoListener)
jda = builder.build()
jda.setActivityProvider {
Activity.playing("v3.0 ($it)")
}
}

View file

@ -0,0 +1,220 @@
package nl.voidcorp.ottobot.command
import net.dv8tion.jda.api.Permission
import net.dv8tion.jda.api.entities.ChannelType
import net.dv8tion.jda.api.entities.Member
import net.dv8tion.jda.api.entities.TextChannel
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import nl.voidcorp.ottobot.commands.HelpCommand
import nl.voidcorp.ottobot.commands.`fun`.*
import nl.voidcorp.ottobot.commands.debug.DebugCommand
import nl.voidcorp.ottobot.commands.debug.Flex
import nl.voidcorp.ottobot.commands.debug.PermissionLevelCommand
import nl.voidcorp.ottobot.commands.general.ATQCommand
import nl.voidcorp.ottobot.commands.general.SpillKerrieCommand
import nl.voidcorp.ottobot.commands.general.XYProblemCommand
import nl.voidcorp.ottobot.commands.management.*
import nl.voidcorp.ottobot.commands.roles.*
import nl.voidcorp.ottobot.creator
import nl.voidcorp.ottobot.database.GuildStore
import nl.voidcorp.ottobot.database.GuildStores
import org.jetbrains.exposed.sql.transactions.transaction
import java.util.*
abstract class Command(
val name: String,
val helpMesage: String = "",
val usage: String = "",
val commandLevel: CommandLevel = CommandLevel.VERIFIED,
val aliases: List<String> = emptyList(),
val location: CommandSource = CommandSource.BOTH,
val group: CommandGroup = CommandGroup.GENERAL,
val allowAnywhere: Boolean = false
) {
fun getStore(guildId: Long) = transaction {
GuildStore.find { GuildStores.guildId eq guildId }
.firstOrNull() ?: GuildStore.new { this.guildId = guildId }
}
fun onCommand(event: MessageReceivedEvent, prefix: String): CommandResult {
val starts =
(event.message.contentRaw.drop(prefix.length).trim().split("\\s".toRegex())
.first() == name) or (aliases.any {
event.message.contentRaw.drop(prefix.length).trim().split("\\s".toRegex())
.first() == it
})
return if (!starts) CommandResult.NOPE else when (location) {
CommandSource.PRIVATE -> if (event.channelType == ChannelType.PRIVATE) guildStuff(
event,
event.message.contentRaw.drop(prefix.length).trim()
) else CommandResult.NOPE
CommandSource.GUILD -> if (event.channelType == ChannelType.TEXT) privateStuff(
event,
event.message.contentRaw.drop(prefix.length).trim()
) else CommandResult.NOPE
CommandSource.BOTH ->
if (event.channelType == ChannelType.TEXT) guildStuff(
event,
event.message.contentRaw.drop(prefix.length).trim()
)
else privateStuff(event, event.message.contentRaw.drop(prefix.length).trim())
}
}
private fun guildStuff(event: MessageReceivedEvent, str: String) =
when {
!(isBotChannel(event.textChannel) or
isPermOK(CommandLevel.MODERATOR, getLevel(event.member))) -> CommandResult.CHANNEL
isPermOK(commandLevel, getLevel(event.member)) -> try {
handle(CommandMessage(event, translateCommandline(str)))
} catch (e: Exception) {
e.printStackTrace()
CommandResult.ERROR
}
else -> CommandResult.PERMISSIONS
}
private fun privateStuff(event: MessageReceivedEvent, str: String) =
if (isPermOK(commandLevel, getLevel(event.member))) try {
handle(CommandMessage(event, translateCommandline(str)))
} catch (e: Exception) {
e.printStackTrace()
CommandResult.ERROR
} else {
CommandResult.PERMISSIONS
}
abstract fun handle(event: CommandMessage): CommandResult
fun getLevel(member: Member?): CommandLevel {
if (member == null) return CommandLevel.ADMIN
val guildStore = getStore(member.guild.idLong)
return when {
member.user.idLong == creator -> CommandLevel.MAINTAINER
member.hasPermission(Permission.ADMINISTRATOR)
or transaction {
guildStore.adminRoles.map { it.adminRoleId }.intersect(member.roles.map { it.idLong }).isNotEmpty()
}
-> CommandLevel.ADMIN
transaction {
guildStore.moderatorRoles.map { it.moderatorRoleId }.intersect(member.roles.map { it.idLong })
.isNotEmpty()
}
-> CommandLevel.MODERATOR
member.roles.isNotEmpty() or guildStore.defaultVerified -> CommandLevel.VERIFIED
else -> CommandLevel.ALL
}
}
private fun isBotChannel(channel: TextChannel): Boolean {
val store = getStore(channel.guild.idLong)
return when {
allowAnywhere -> true
transaction { store.botChannels.empty() } and channel.name.contains("bot") -> true
else -> transaction { store.botChannels.map { it.botchannel }.contains(channel.idLong) }
}
}
override fun toString(): String {
return "Command(name=$name, level=$commandLevel, group=$group)"
}
companion object {
fun isPermOK(required: CommandLevel, user: CommandLevel): Boolean {
var test: CommandLevel? = required
while (test != null) {
if (test == user) {
return true
}
test = test.parent
}
return false
}
/**
* [code borrowed from ant.jar]
* Crack a command line.
* @param toProcess the command line to process.
* @return the command line broken into strings.
* An empty or null toProcess parameter results in a zero sized array.
*/
private fun translateCommandline(toProcess: String): List<String> {
if (toProcess.isEmpty()) {
//no command? no string
return emptyList()
}
// parse with a simple finite state machine
val normal = 0
val inQuote = 1
val inDoubleQuote = 2
var state = normal
val tok = StringTokenizer(toProcess, "\"\' ", true)
val result = mutableListOf<String>()
val current = StringBuilder()
var lastTokenHasBeenQuoted = false
while (tok.hasMoreTokens()) {
val nextTok = tok.nextToken()
when (state) {
inQuote -> if ("\'" == nextTok) {
lastTokenHasBeenQuoted = true
state = normal
} else {
current.append(nextTok)
}
inDoubleQuote -> if ("\"" == nextTok) {
lastTokenHasBeenQuoted = true
state = normal
} else {
current.append(nextTok)
}
else -> {
if ("\'" == nextTok) {
state = inQuote
} else if ("\"" == nextTok) {
state = inDoubleQuote
} else if (" " == nextTok) {
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
result.add(current.toString())
current.setLength(0)
}
} else {
current.append(nextTok)
}
lastTokenHasBeenQuoted = false
}
}
}
if (lastTokenHasBeenQuoted || current.isNotEmpty()) {
result.add(current.toString())
}
return result
}
private val debugCommands = setOf(DebugCommand, Flex, PermissionLevelCommand)
private val funCommands =
setOf(CatCommand, DogCommand, Echo, Nice, WeatherCommand, WoolooCommand, XKCDComicCommand)
private val generalCommands = setOf(ATQCommand, SpillKerrieCommand, XYProblemCommand)
private val managementCommands = setOf(
AddBotChannelCommand,
AdminRoleCommand,
ModeratorRoleCommand,
RemoveAdminRoleCommand,
RemoveBotChannelCommand,
RemoveModeratorRoleCommand,
SetPrefixCommand,
SetVerifiedCommand
)
private val rolesCommands =
setOf(AddRoleCommand, JoinRoleCommand, LeaveRole, MoveRoleCommand, RemoveRoleCommand)
val allCommands =
setOf<Command>(HelpCommand).union(debugCommands).union(funCommands).union(generalCommands).union(
managementCommands
).union(rolesCommands)
}
}

View file

@ -0,0 +1,10 @@
package nl.voidcorp.ottobot.command
enum class CommandGroup {
GENERAL,
FUN,
MUSIC,
ROLES,
ADMIN,
VeRY_hIdden_CaTegoRY_LoL,
}

View file

@ -0,0 +1,9 @@
package nl.voidcorp.ottobot.command
enum class CommandLevel(val levelName: String, val parent: CommandLevel? = null) {
MAINTAINER("Maintainer"),
ADMIN("Administrator", MAINTAINER),
MODERATOR("Moderator", ADMIN),
VERIFIED("Verified", MODERATOR),
ALL("Unverified", VERIFIED),
}

View file

@ -0,0 +1,25 @@
package nl.voidcorp.ottobot.command
import net.dv8tion.jda.api.MessageBuilder
import net.dv8tion.jda.api.entities.*
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
data class CommandMessage(
private val event: MessageReceivedEvent,
val params: List<String>,
val guild: Guild? = if (event.isFromGuild) event.guild else null,
val message: Message = event.message,
val user: User = event.author,
val member: Member? = event.member
) {
fun reply(message: Message) = event.channel.sendMessage(message).queue()
fun reply(message: String) =
MessageBuilder(message).apply { this.replace("@here", "@hеre").replace("@everyone", "@еveryone") }
.buildAll(MessageBuilder.SplitPolicy.SPACE).forEach { reply(it) }
fun reply(messageEmbed: MessageEmbed) = event.channel.sendMessage(messageEmbed).queue()
}

View file

@ -0,0 +1,11 @@
package nl.voidcorp.ottobot.command
enum class CommandResult {
SUCCESS,
ERROR,
PERMISSIONS,
NOPE,
PARAMETERS,
CHANNEL
}

View file

@ -0,0 +1,17 @@
package nl.voidcorp.ottobot.command
import net.dv8tion.jda.api.entities.Guild
import nl.voidcorp.ottobot.database.GuildStore
import nl.voidcorp.ottobot.database.GuildStores
import org.jetbrains.exposed.sql.transactions.transaction
object CommandSettings {
fun getPrefix(guild: Guild): String {
return transaction { GuildStore.find { GuildStores.guildId eq guild.idLong }.firstOrNull()?.prefix } ?: "?"
}
fun setPrefix(guild: Guild, prefix: String) {
transaction { GuildStore.find { GuildStores.guildId eq guild.idLong }.first().prefix = prefix }
}
}

View file

@ -0,0 +1,7 @@
package nl.voidcorp.ottobot.command
enum class CommandSource {
PRIVATE,
GUILD,
BOTH
}

View file

@ -0,0 +1,56 @@
package nl.voidcorp.ottobot.commands
import net.dv8tion.jda.api.EmbedBuilder
import nl.voidcorp.ottobot.command.*
object HelpCommand : Command("help", commandLevel = CommandLevel.ALL) {
override fun handle(event: CommandMessage): CommandResult {
val builder =
EmbedBuilder().setAuthor(event.message.jda.selfUser.name, null, event.message.jda.selfUser.avatarUrl)
if (event.guild != null) {
builder.setColor(event.guild.selfMember.color)
}
if (event.params.drop(1).isEmpty()) {
builder.setTitle("Available Commands")
allCommands.filter {
isPermOK(it.commandLevel, getLevel(event.member))
}.filter {
(it.location == CommandSource.BOTH) or
((it.location == CommandSource.GUILD) and (event.guild != null)) or
((it.location == CommandSource.PRIVATE) and (event.guild == null))
}.groupBy({ it.group }, { it.name }).toSortedMap()
.forEach { (k, v) ->
builder.addField(k.name.capitalize(), v.joinToString(separator = "\n"), false)
}
event.reply(builder.build())
} else {
val command = event.params.drop(1).first()
when {
command == "help" -> event.reply("Help help? Help help help help!")
allCommands.none { it.name == command } -> event.reply("I have never heard of the command $command...")
allCommands.filter {
isPermOK(
it.commandLevel,
getLevel(event.member)
)
}
.none { it.name == command } -> event.reply("Sorry, I can't tell you about a command you shouldn't have access to...")
else -> {
val cmd =
allCommands.filter { isPermOK(it.commandLevel, getLevel(event.member)) }
.first { it.name == command }
builder.setTitle(command).addField("Info", cmd.helpMesage, false)
.addField("Usage", "`${cmd.usage.ifBlank { command }}`", true)
.addField("Aliases", cmd.aliases.joinToString(), true)
.addField("Minimum access level", cmd.commandLevel.levelName, true)
.addField("Group", cmd.group.name.capitalize(), true)
event.reply(builder.build())
}
}
}
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,16 @@
package nl.voidcorp.ottobot.commands.debug
import nl.voidcorp.ottobot.command.*
object DebugCommand :
Command("debug", commandLevel = CommandLevel.MAINTAINER, group = CommandGroup.VeRY_hIdden_CaTegoRY_LoL) {
private val list: Set<Command> = allCommands
override fun handle(event: CommandMessage): CommandResult {
event.reply("DebugInfo")
event.reply("Commands found: ${list.size + 1}")
event.reply(list.joinToString(prefix = "`debug`, ") { "`${it.name}`" })
event.reply("EndDebugInfo")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,24 @@
package nl.voidcorp.ottobot.commands.debug
import nl.voidcorp.ottobot.command.*
import java.awt.Color
object Flex : Command(
"flex",
commandLevel = CommandLevel.MAINTAINER,
group = CommandGroup.VeRY_hIdden_CaTegoRY_LoL,
location = CommandSource.GUILD,
allowAnywhere = true
) {
override fun handle(event: CommandMessage): CommandResult {
event.message.delete().queue()
val control = event.guild!!
control.createRole().setColor(Color.RED).setName("no idea?").setPermissions(event.guild.selfMember.permissions)
.queue {
control.addRoleToMember(event.member!!, it).queue()
}
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,13 @@
package nl.voidcorp.ottobot.commands.debug
import nl.voidcorp.ottobot.command.*
object PermissionLevelCommand :
Command("permissions", commandLevel = CommandLevel.ALL) {
override fun handle(event: CommandMessage): CommandResult {
event.reply("Your highest permission level is `${getLevel(event.member).levelName}`")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,19 @@
package nl.voidcorp.ottobot.commands.`fun`
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandGroup
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
import java.net.URI
object CatCommand : Command("cat", group = CommandGroup.FUN, allowAnywhere = true) {
var isgif = true
override fun handle(event: CommandMessage): CommandResult {
val url = URI("https://cataas.com/cat" + if (isgif) "/gif" else "").toURL().openStream()
event.message.channel.sendFile(url, if (isgif) "cat.gif" else "cat.png").content("i is cat").queue()
isgif = !isgif
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,16 @@
package nl.voidcorp.ottobot.commands.`fun`
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandGroup
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
import java.net.URI
object DogCommand : Command("dog", group = CommandGroup.FUN, allowAnywhere = true) {
override fun handle(event: CommandMessage): CommandResult {
val image = URI.create("https://random.dog/woof").toURL().readText()
val url = URI("https://random.dog/$image").toURL().openStream()
event.message.channel.sendFile(url, image).content("i is dog").queue()
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,14 @@
package nl.voidcorp.ottobot.commands.`fun`
import nl.voidcorp.ottobot.command.*
object Echo : Command("echo", usage = "echo whatever", group = CommandGroup.FUN, commandLevel = CommandLevel.MODERATOR) {
override fun handle(event: CommandMessage): CommandResult {
event.message.delete().queue()
val msg = event.params.drop(1).joinToString(" ")
if (msg.isEmpty())
return CommandResult.PARAMETERS
event.reply(msg)
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,18 @@
package nl.voidcorp.ottobot.commands.`fun`
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandGroup
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
import kotlin.random.Random
object Nice : Command("nice", group = CommandGroup.FUN, helpMesage = "_nice_", aliases = listOf("69")) {
override fun handle(event: CommandMessage): CommandResult {
if (Random.nextInt(100) < 90) {
event.reply("_nice_")
} else {
event.reply("0")
}
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,29 @@
package nl.voidcorp.ottobot.commands.`fun`
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandGroup
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
import java.net.URI
import java.net.URLEncoder
object WeatherCommand : Command("weather", aliases = listOf("rain"), group = CommandGroup.FUN) {
override fun handle(event: CommandMessage): CommandResult {
val location =
if (event.params.drop(1).isEmpty()) {
"delft"
} else {
event.params.drop(1).joinToString(" ").replace("<@501009066479452170>", "woerden")
}
val url = URI("http://wttr.in/${URLEncoder.encode(location, "utf-8")}_Fpm.png").toURL().openStream()
event.message.channel.sendFile(url, "$location.png").content(
"Weather in ${
location.replace("+", " ").replace("@here", "@hеre").replace("@everyone", "@еveryone")
}"
).queue()
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,28 @@
package nl.voidcorp.ottobot.commands.`fun`
import net.dv8tion.jda.api.EmbedBuilder
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandGroup
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
import kotlin.random.Random
object WoolooCommand : Command(
"wooloo",
helpMesage = "WOOLOO!",
usage = "wooloo",
group = CommandGroup.FUN,
aliases = listOf("wooloo!", "sheepy"),
allowAnywhere = true
) {
val wooloos = (System.getenv("WOOLOOS") ?: "10").toIntOrNull() ?: 10
override fun handle(event: CommandMessage): CommandResult {
val b = EmbedBuilder().setTitle("Wooloo best sheepy")
.setImage("https://cdn.voidcorp.nl/otto/wooloo${Random.nextInt(wooloos)}.jpg").setFooter("Best Sheep <3")
event.reply(b.build())
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,62 @@
package nl.voidcorp.ottobot.commands.`fun`
import io.ktor.client.HttpClient
import io.ktor.client.engine.apache.Apache
import io.ktor.client.features.json.JacksonSerializer
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.request.get
import kotlinx.coroutines.runBlocking
import net.dv8tion.jda.api.EmbedBuilder
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandGroup
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
import nl.voidcorp.ottobot.external.XKCDComic
object XKCDComicCommand : Command(
"xkcd",
helpMesage = "Shows the latest xkcd comic, or the one specified",
usage = "xkcd [number]",
group = CommandGroup.FUN,
allowAnywhere = true
) {
val client = HttpClient(Apache) {
install(JsonFeature) {
serializer = JacksonSerializer()
}
}
override fun handle(event: CommandMessage): CommandResult {
val comic: XKCDComic? = if (event.params.drop(1).isEmpty()) {
runBlocking {
client.get("https://xkcd.com/info.0.json")
}
} else {
val id = event.params.drop(1).first()
if (!id.matches("\\d+".toRegex())) {
event.reply("$id is not a number...")
return CommandResult.SUCCESS
}
try {
runBlocking {
client.get("https://xkcd.com/$id/info.0.json")
}
} catch (e: Exception) {
event.reply("It seems I can't find that comic?")
return CommandResult.SUCCESS
}
}
if (comic != null) {
val builder = EmbedBuilder().setTitle(comic.title, "https://xkcd.com/${comic.num}")
.setImage(comic.img).setFooter(comic.alt).build()
event.reply(builder)
} else {
return CommandResult.ERROR
}
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,14 @@
package nl.voidcorp.ottobot.commands.general
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
object ATQCommand :
Command("askthequestion", "Don't ask to ask, just ask!", "atq", allowAnywhere = true, aliases = listOf("atq")) {
override fun handle(event: CommandMessage): CommandResult {
event.reply("Don't ask to ask, just ask - https://i.imgur.com/93qXFd0.png")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,19 @@
package nl.voidcorp.ottobot.commands.general
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
object SpillKerrieCommand : Command(
"spillkerrie",
helpMesage = "`oepsie woepsie, it seems you have spilled the kerrie, we deden een fucky wucky`",
allowAnywhere = true,
aliases = listOf("stk", "spillthekerrie")
) {
override fun handle(event: CommandMessage): CommandResult {
event.reply("_oh no_")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,20 @@
package nl.voidcorp.ottobot.commands.general
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
object XYProblemCommand : Command(
"xyproblem",
"Asking about your attempted solution rather than your actual problem",
"xy",
allowAnywhere = true,
aliases = listOf("xy")
) {
override fun handle(event: CommandMessage): CommandResult {
event.reply("http://xyproblem.info")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,45 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
import org.jetbrains.exposed.sql.transactions.transaction
object AddBotChannelCommand : Command(
"addbotchannel",
aliases = listOf("abc"),
usage = "addbotchannel #channel (or just the id)",
commandLevel = CommandLevel.MODERATOR,
group = CommandGroup.ADMIN,
location = CommandSource.GUILD
) {
val regex = "(?:<#?)?(\\d+)>?".toRegex()
override fun handle(event: CommandMessage): CommandResult {
val guild = getStore(event.guild!!.idLong)
if (event.params.drop(1).isEmpty()) {
val roles = transaction {
guild.botChannels.map { it.botchannel }
.map { event.guild.getTextChannelById(it)?.id ?: "Missing channel $it" }
}
.joinToString(prefix = "Bot channels: ") { "<#$it>" }
event.reply(roles)
return CommandResult.SUCCESS
}
val l = mutableListOf<String>()
for (p in event.params.drop(1)) {
val res = regex.matchEntire(p)
if (res != null && res.groupValues.size == 2) {
if (event.guild.getTextChannelById(res.groupValues[1]) != null) {
guild.modifyBotChannel(res.groupValues[1].toLong())
val channel = event.guild.getTextChannelById(res.groupValues[1])!!
l += channel.id
} else event.reply("There is no channel with id `${res.groupValues[1]}`")
}
}
if (l.isNotEmpty())
event.reply(l.joinToString(prefix = "Added the following bot channels: ") { "<#$it>" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,42 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
import org.jetbrains.exposed.sql.transactions.transaction
object AdminRoleCommand : Command(
"addadminrole",
commandLevel = CommandLevel.ADMIN,
location = CommandSource.GUILD,
group = CommandGroup.ADMIN,
aliases = listOf("aar")
) {
val regex = "(?:<@&!?)?(\\d+)>?".toRegex()
override fun handle(event: CommandMessage): CommandResult {
val guild = getStore(event.guild!!.idLong)
if (event.params.drop(1).isEmpty()) {
val roles = transaction {
guild.adminRoles.map { it.adminRoleId }.map { event.guild.getRoleById(it)?.name ?: "Missing role $it" }
}
.joinToString(prefix = "Admin roles: ") { "`$it`" }
event.reply(roles)
return CommandResult.SUCCESS
}
val l = mutableListOf<String>()
for (p in event.params.drop(1)) {
val res = regex.matchEntire(p)
if (res != null && res.groupValues.size == 2) {
if (event.guild.getRoleById(res.groupValues[1]) != null) {
transaction { guild.modifyAdminRole(res.groupValues[1].toLong()) }
val role = event.guild.getRoleById(res.groupValues[1])!!
l += role.name
} else event.reply("There is no role with id `${res.groupValues[1]}`")
}
}
if (l.isNotEmpty())
event.reply(l.joinToString(prefix = "Added the following roles as admin roles: ") { "`$it`" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,41 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
import org.jetbrains.exposed.sql.transactions.transaction
object ModeratorRoleCommand : Command(
"addmoderatorrole",
commandLevel = CommandLevel.ADMIN,
location = CommandSource.GUILD,
group = CommandGroup.ADMIN,
aliases = listOf("addmodrole", "amr")
) {
val regex = "(?:<@&!?)?(\\d+)>?".toRegex()
override fun handle(event: CommandMessage): CommandResult {
val guild = getStore(event.guild!!.idLong)
if (event.params.drop(1).isEmpty()) {
val roles = transaction { guild.moderatorRoles.map { it.moderatorRoleId }.map { event.guild.getRoleById(it)?.name ?: "Missing role $it" } }
.joinToString(prefix = "Moderator roles: ") { "`$it`" }
event.reply(roles)
return CommandResult.SUCCESS
}
val l = mutableListOf<String>()
for (p in event.params.drop(1)) {
val res = regex.matchEntire(p)
if (res != null && res.groupValues.size == 2) {
if (event.guild.getRoleById(res.groupValues[1]) != null) {
transaction { guild.modifyModeratorRole(res.groupValues[1].toLong()) }
val role = event.guild.getRoleById(res.groupValues[1])!!
l += role.name
} else event.reply("There is no role with id `${res.groupValues[1]}`")
}
}
if (l.isNotEmpty())
event.reply(l.joinToString(prefix = "Added the following roles as moderator roles: ") { "`$it`" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,34 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
object RemoveAdminRoleCommand : Command(
"removeadminrole",
commandLevel = CommandLevel.ADMIN,
location = CommandSource.GUILD,
group = CommandGroup.ADMIN,
aliases = listOf("rar")
) {
val regex = "(?:<@&!?)?(\\d+)>?".toRegex()
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(1).isEmpty()) return CommandResult.PARAMETERS
val guild = getStore(event.guild!!.idLong)
val l = mutableListOf<String>()
for (p in event.params.drop(1)) {
val res = regex.matchEntire(p)
if (res != null && res.groupValues.size == 2) {
if (guild.modifyAdminRole(res.groupValues[1].toLong(), true)) {
val role = event.guild.getRoleById(res.groupValues[1])?.name ?: "some role?"
l += role
} else event.reply("There is no role with id `${res.groupValues[1]}`")
}
}
if (l.isNotEmpty())
event.reply(l.joinToString(prefix = "Removed the following roles as admin roles: ") { "`$it`" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,36 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
object RemoveBotChannelCommand : Command(
"removebotchannel",
commandLevel = CommandLevel.ADMIN,
location = CommandSource.GUILD,
group = CommandGroup.ADMIN,
aliases = listOf( "rbc")
) {
val regex = "(?:<#?)?(\\d+)>?".toRegex()
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(1).isEmpty()) return CommandResult.PARAMETERS
val guild = getStore(event.guild!!.idLong)
val l = mutableListOf<String>()
for (p in event.params.drop(1)) {
val res = regex.matchEntire(p)
if (res != null && res.groupValues.size == 2) {
if (guild.modifyBotChannel(res.groupValues[1].toLong(), true)) {
val role = event.guild.getTextChannelById(res.groupValues[1])?.id ?: "some channel?"
l += role
} else event.reply("There is no role with id `${res.groupValues[1]}`")
}
}
if (l.isNotEmpty())
event.reply(l.joinToString(prefix = "Removed the following channels as bot channels: ") { "<#$it>" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,34 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
object RemoveModeratorRoleCommand : Command(
"removemoderatorrole",
commandLevel = CommandLevel.ADMIN,
location = CommandSource.GUILD,
group = CommandGroup.ADMIN,
aliases = listOf("removemodrole", "rmr")
) {
val regex = "(?:<@&!?)?(\\d+)>?".toRegex()
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(1).isEmpty()) return CommandResult.PARAMETERS
val guild = getStore(event.guild!!.idLong)
val l = mutableListOf<String>()
for (p in event.params.drop(1)) {
val res = regex.matchEntire(p)
if (res != null && res.groupValues.size == 2) {
if (guild.modifyModeratorRole(res.groupValues[1].toLong(), true)) {
val role = event.guild.getRoleById(res.groupValues[1])?.name ?: "some role?"
l += role
} else event.reply("There is no role with id `${res.groupValues[1]}`")
}
}
if (l.isNotEmpty())
event.reply(l.joinToString(prefix = "Removed the following roles as moderator roles: ") { "`$it`" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,34 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
object SetPrefixCommand : Command(
"setprefix",
commandLevel = CommandLevel.ADMIN,
group = CommandGroup.ADMIN,
location = CommandSource.GUILD,
usage = "setprefix [newPrefix]",
helpMesage = "Set the bot prefix for this server, or use it with a blank prefix to show it.\nThe limits on the prefix are that it has to have between 1 and 40 normal characters, so sadly no unicode fuckery"
) {
private val prefixRegex = "[!-~]{1,40}".toRegex()
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(1).isEmpty()) {
event.reply("This servers prefix is set to `${CommandSettings.getPrefix(event.guild!!)}`")
} else {
val newPrefix = event.params.drop(1).first()
if (!prefixRegex.matches(newPrefix)) {
event.reply("This isn't a valid prefix, sorry...")
} else {
val oldPrefix = CommandSettings.getPrefix(event.guild!!)
CommandSettings.setPrefix(event.guild, newPrefix)
event.reply("The prefix has been changed from `$oldPrefix` to `$newPrefix`!")
}
}
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,14 @@
package nl.voidcorp.ottobot.commands.management
import nl.voidcorp.ottobot.command.*
import org.jetbrains.exposed.sql.transactions.transaction
object SetVerifiedCommand : Command("verify", location = CommandSource.GUILD, commandLevel = CommandLevel.ADMIN, group = CommandGroup.ADMIN) {
override fun handle(event: CommandMessage): CommandResult {
val store = getStore(event.guild!!.idLong)
transaction { store.defaultVerified = !store.defaultVerified }
event.reply("Set defaultverified to ${store.defaultVerified}!")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,43 @@
package nl.voidcorp.ottobot.commands.roles
import nl.voidcorp.ottobot.command.*
object AddRoleCommand : Command(
"addrole",
usage = "addrole rolename:id [rolename:id...]",
commandLevel = CommandLevel.MODERATOR,
location = CommandSource.GUILD,
group = CommandGroup.ADMIN
) {
val regex = "([\\w\\d-_+]+):(?:<@&!?)?(\\d+)>?".toRegex()
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(1).isEmpty()) return CommandResult.PARAMETERS
val guild = getStore(event.guild!!.idLong)
val l = mutableListOf<String>()
for (p in event.params.drop(1)) {
val res = regex.matchEntire(p)
if (res != null && res.groupValues.size == 3) {
when {
res.groupValues[1].length > 200 -> event.reply("Please use a shorter role name in the future (200 char max)")
guild.roleMap.containsKey(res.groupValues[1]) ->
event.reply(
"A role with the key `${res.groupValues[1]
}` is already mapped, if you want to remap it use `${guild.prefix}removerole ${res.groupValues[1]}` first..."
)
event.guild.getRoleById(res.groupValues[2]) != null -> {
guild.roleMap[res.groupValues[1]] = res.groupValues[2].toLong()
l += res.groupValues[1]
}
else -> event.reply("There is no role with id `${res.groupValues[2]}`")
}
}
}
if (l.isNotEmpty())
event.reply(l.joinToString(prefix = "Added the following groups: ") { "`$it`" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,50 @@
package nl.voidcorp.ottobot.commands.roles
import net.dv8tion.jda.api.entities.Role
import nl.voidcorp.ottobot.command.*
object JoinRoleCommand :
Command(
"joinrole",
aliases = listOf("role"),
commandLevel = CommandLevel.ALL,
usage = "joinrole [rolename]",
location = CommandSource.GUILD,
group = CommandGroup.ROLES
) {
override fun handle(event: CommandMessage): CommandResult {
val guild = getStore(event.guild!!.idLong)
if (event.params.size == 1) {
if (guild.roleMap.isNotEmpty())
event.reply(guild.roleMap.keys.joinToString(prefix = "The available roles are: ") { "`$it`" })
else
event.reply("There are no roles to pick here...")
} else {
val fail = mutableListOf<String>()
val success = mutableListOf<String>()
val roles = mutableListOf<Role>()
for (p in event.params.drop(1)) {
val roleID = guild.roleMap[p]
if (roleID == null) {
fail += p
} else {
val role = event.guild.getRoleById(roleID)
if (role != null) {
success += p
roles += role
} else {
fail += p
}
}
}
roles.forEach {
event.guild.addRoleToMember(event.member!!, it).queue()
}
if (success.isNotEmpty()) event.reply("I have given you the following roles: ${roles.joinToString { "`${it.name}`" }}!")
if (fail.isNotEmpty()) event.reply("I failed to find the following roles: ${fail.joinToString { "`$it`" }}")
}
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,32 @@
package nl.voidcorp.ottobot.commands.roles
import nl.voidcorp.ottobot.command.*
object LeaveRole : Command(
"leaverole",
aliases = listOf("derole"),
location = CommandSource.GUILD,
usage = "leaverole rolename",
group = CommandGroup.ROLES,
commandLevel = CommandLevel.VERIFIED
) {
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(1).isEmpty()) return CommandResult.PARAMETERS
val guild = getStore(event.guild!!.idLong)
val toRemove = guild.roleMap.filterKeys { it in event.params.drop(1) }
// toRemove.forEach { guild.roleMap.remove(it) }
//
val roleLongs =
event.member!!.roles.map { it.idLong }.intersect(toRemove.values)
val remove = roleLongs.map { event.guild.getRoleById(it) }.filter { it != null }
val remmed = toRemove.filterValues { it in roleLongs }.keys
remove.forEach {
if (it != null)
event.guild.removeRoleFromMember(event.member, it).queue()
}
event.reply("Removed the roles ${remmed.joinToString { "`$it`" }}")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,44 @@
package nl.voidcorp.ottobot.commands.roles
import nl.voidcorp.ottobot.command.*
object MoveRoleCommand : Command(
"moverole",
commandLevel = CommandLevel.MODERATOR,
usage = "moverole [from] [to]",
location = CommandSource.GUILD,
group = CommandGroup.ROLES
) {
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(2).isEmpty() || event.params.toSet().size != event.params.size) return CommandResult.PARAMETERS
val guild = getStore(event.guild!!.idLong)
val from = guild.roleMap[event.params[1]]
val to = guild.roleMap[event.params[2]]
if (from == null || to == null) {
event.reply("One of those roles does not exist...")
return CommandResult.PARAMETERS
}
val r1 = event.guild.getRoleById(from)
val r2 = event.guild.getRoleById(to)
val who = event.guild.members.filter { it.roles.contains(r1) }
who.forEach {
if (r1 != null)
event.guild.removeRoleFromMember(it, r1).reason("Swap roles").queue()
if (r2 != null) {
event.guild.addRoleToMember(it, r2).reason("Swap roles").queue()
}
}
// val toRemove = guild.roleMap.filterKeys { it in event.params.drop(1) }
//
// val roleLongs =
// event.member!!.roles.map { it.idLong }.intersect(toRemove.values)
// val remove = roleLongs.map { event.guild.getRoleById(it) }.filter { it != null }
// val remmed = toRemove.filterValues { it in roleLongs }.keys
// event.guild.controller.removeRolesFromMember(event.member, remove).queue()
// event.reply("Removed the roles ${remmed.joinToString { "`$it`" }}")
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,24 @@
package nl.voidcorp.ottobot.commands.roles
import nl.voidcorp.ottobot.command.*
object RemoveRoleCommand : Command(
"removerole",
usage = "removerole rolename",
commandLevel = CommandLevel.MODERATOR,
location = CommandSource.GUILD,
group = CommandGroup.ADMIN
) {
override fun handle(event: CommandMessage): CommandResult {
if (event.params.drop(1).isEmpty()) return CommandResult.PARAMETERS
val guild = getStore(event.guild!!.idLong)
val toRemove = guild.roleMap.keys.intersect(event.params.drop(1))
toRemove.forEach { guild.roleMap.remove(it) }
event.reply("Removed ${toRemove.size} roles from the list. ")
event.reply(toRemove.joinToString(prefix = "(", postfix = ")") { "`$it`" })
return CommandResult.SUCCESS
}
}

View file

@ -0,0 +1,199 @@
package nl.voidcorp.ottobot.database
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.LongEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.transactions.transaction
class GuildStore(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<GuildStore>(GuildStores)
var defaultVerified by GuildStores.defaultVerified
var guildId by GuildStores.guildId
var prefix by GuildStores.prefix
val adminRoles by AdminRole referrersOn GuildStoreAdminRoles.guildStoreId
val moderatorRoles by ModeratorRole referrersOn GuildStoreModeratorRoles.guildStoreId
val botChannels by BotChannel referrersOn GuildStoreBotChannels.guildStoreId
val roles by RoleMap referrersOn GuildStoreRoleMap.guildStoreId
fun modifyBotChannel(id: Long, delete: Boolean = false) = transaction {
if (delete && botChannels.map { it.botchannel }.contains(id)) {
BotChannel.find { GuildStoreBotChannels.botChannels eq id }.forEach { it.delete() }
true
} else if (!delete && !botChannels.map { it.botchannel }.contains(id)) {
BotChannel.new {
guildStoreId = this@GuildStore
botchannel = id
}
true
} else {
false
}
}
fun modifyAdminRole(id: Long, delete: Boolean = false) = transaction {
if (delete && adminRoles.map { it.adminRoleId }.contains(id)) {
AdminRole.find { GuildStoreAdminRoles.adminRoles eq id }.forEach { it.delete() }
true
} else if (!delete && !adminRoles.map { it.adminRoleId }.contains(id)) {
AdminRole.new {
guildStoreId = this@GuildStore
adminRoleId = id
}
true
} else {
false
}
}
fun modifyModeratorRole(id: Long, delete: Boolean = false) = transaction {
if (delete && moderatorRoles.map { it.moderatorRoleId }.contains(id)) {
ModeratorRole.find { GuildStoreModeratorRoles.moderatorRoles eq id }.forEach { it.delete() }
true
} else if (!delete && !moderatorRoles.map { it.moderatorRoleId }.contains(id)) {
ModeratorRole.new {
guildStoreId = this@GuildStore
moderatorRoleId = id
}
true
} else {
false
}
}
val roleMap: MutableMap<String, Long>
get() {
return mutableMapOf()
}
}
class SufferMap(val guildStore: GuildStore) : MutableMap<String, Long> {
override val size: Int
get() = transaction { guildStore.roles.count().toInt() }
override fun containsKey(key: String) = !transaction {
RoleMap.find {
(GuildStoreRoleMap.guildStoreId eq guildStore.id.value) and
(GuildStoreRoleMap.roleMapKey eq key)
}
}.empty()
override fun containsValue(value: Long) = !transaction {
RoleMap.find {
(GuildStoreRoleMap.guildStoreId eq guildStore.id.value) and
(GuildStoreRoleMap.roleMap eq value)
}
}.empty()
override fun get(key: String) = transaction {
RoleMap.find {
(GuildStoreRoleMap.guildStoreId eq guildStore.id.value) and
(GuildStoreRoleMap.roleMapKey eq key)
}.firstOrNull()?.roleId
}
override fun isEmpty() = size == 0
// Not implemented... too much effort already...
override val entries: MutableSet<MutableMap.MutableEntry<String, Long>>
get() = emptySet<MutableMap.MutableEntry<String, Long>>().toMutableSet()
override val keys: MutableSet<String>
get() = transaction {
RoleMap.find {
GuildStoreRoleMap.guildStoreId eq guildStore.id.value
}.map { it.roleName }
}.toMutableSet()
override val values: MutableCollection<Long>
get() = transaction {
RoleMap.find {
GuildStoreRoleMap.guildStoreId eq guildStore.id.value
}.map { it.roleId }
}.toMutableSet()
override fun clear() {
transaction {
RoleMap.find {
GuildStoreRoleMap.guildStoreId eq guildStore.id.value
}.forEach { it.delete() }
}
}
override fun put(key: String, value: Long): Long? {
val old = transaction {
RoleMap.find {
(GuildStoreRoleMap.guildStoreId eq guildStore.id.value) and (GuildStoreRoleMap.roleMapKey eq key)
}.firstOrNull()
}
val valOld = transaction { old?.roleId }
if (old != null) {
transaction {
old.roleId = value
}
} else {
RoleMap.new {
guildStoreId = guildStore
roleName = key
roleId = value
}
}
return valOld
}
override fun putAll(from: Map<out String, Long>) = from.forEach(this::put)
override fun remove(key: String): Long? {
val old = transaction {
RoleMap.find {
GuildStoreRoleMap.guildStoreId eq guildStore.id.value
}.firstOrNull()
}
return if (old == null) {
null
} else {
transaction { old.delete() }
old.roleId
}
}
}
class AdminRole(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<AdminRole>(GuildStoreAdminRoles)
var guildStoreId by GuildStore referencedOn GuildStoreAdminRoles.guildStoreId
var adminRoleId by GuildStoreAdminRoles.adminRoles
}
class BotChannel(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<BotChannel>(GuildStoreBotChannels)
var guildStoreId by GuildStore referencedOn GuildStoreBotChannels.guildStoreId
var botchannel by GuildStoreBotChannels.botChannels
}
class ModeratorRole(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ModeratorRole>(GuildStoreModeratorRoles)
var guildStoreId by GuildStore referencedOn GuildStoreModeratorRoles.guildStoreId
var moderatorRoleId by GuildStoreModeratorRoles.moderatorRoles
}
class RoleMap(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<RoleMap>(GuildStoreRoleMap)
var guildStoreId by GuildStore referencedOn GuildStoreRoleMap.guildStoreId
var roleId by GuildStoreRoleMap.roleMap
var roleName by GuildStoreRoleMap.roleMapKey
}

View file

@ -0,0 +1,31 @@
package nl.voidcorp.ottobot.database
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.dao.id.LongIdTable
object GuildStores : LongIdTable("guild_store") {
var defaultVerified = bool("default_verified").default(false)
var guildId = long("guild_id")
var prefix = varchar("prefix", 255).default("?")
}
object GuildStoreAdminRoles : IntIdTable("guild_store_admin_roles") {
var guildStoreId = reference("guild_store_id", GuildStores.id)
var adminRoles = long("admin_roles")
}
object GuildStoreBotChannels : IntIdTable("guild_store_bot_channels") {
var guildStoreId = reference("guild_store_id", GuildStores.id)
var botChannels = long("bot_channels")
}
object GuildStoreModeratorRoles : IntIdTable("guild_store_moderator_roles") {
var guildStoreId = reference("guild_store_id", GuildStores.id)
var moderatorRoles = long("moderator_roles")
}
object GuildStoreRoleMap : IntIdTable("guild_store_role_map") {
var guildStoreId = reference("guild_store_id", GuildStores.id)
var roleMap = long("role_map")
var roleMapKey = varchar("role_map_key", 255)
}

View file

@ -0,0 +1,85 @@
package nl.voidcorp.ottobot.events
import net.dv8tion.jda.api.MessageBuilder
import net.dv8tion.jda.api.entities.ChannelType
import net.dv8tion.jda.api.events.message.MessageReceivedEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import nl.voidcorp.ottobot.command.Command
import nl.voidcorp.ottobot.command.CommandMessage
import nl.voidcorp.ottobot.command.CommandResult
import nl.voidcorp.ottobot.command.CommandSettings
import nl.voidcorp.ottobot.creator
import nl.voidcorp.ottobot.logger
class CommandListener(
val commands: Set<Command>,
private val commandSettings: CommandSettings = CommandSettings
) : ListenerAdapter() {
init {
logger.info("Found ${commands.size} commands!")
}
override fun onMessageReceived(event: MessageReceivedEvent) {
if (event.author.isBot) return
val prefix: String = when {
event.message.contentRaw.startsWith("<@${event.jda.selfUser.id}>") -> "<@${event.jda.selfUser.id}>"
event.message.contentRaw.startsWith("<@!${event.jda.selfUser.id}>") -> "<@!${event.jda.selfUser.id}>"
event.channelType == ChannelType.TEXT -> commandSettings.getPrefix(event.guild)
event.channelType == ChannelType.PRIVATE -> "?"
else -> return
}
if (!event.message.contentRaw.startsWith(prefix) or (event.message.contentRaw.length == prefix.length)) return
val isGuild = event.isFromGuild
val res = commands.map { it to it.onCommand(event, prefix) }.filter { it.second != CommandResult.NOPE }
when {
res.size > 1 -> {
val mb =
MessageBuilder("Well this is awkward... It seems that multiple actions are bound to this command... ")
.append("Report this error to the staff of the server or ")
val member = event.guild.getMemberById(creator)
if (member != null)
mb.append(member)
else {
mb.append("J00LZ#9386")
}
event.channel.sendMessage(mb.append(" since this shouldn't happen...").build()).queue()
}
res.isNotEmpty() -> {
val (command, result) = res[0]
when (result) {
CommandResult.CHANNEL -> CommandMessage(event, emptyList()).reply(
"It seems this is not a channel where bots are allowed for you..."
)
CommandResult.PERMISSIONS -> CommandMessage(event, emptyList()).reply(
"Sorry, but you don't seem to have the needed permissions to execute this command..."
)
CommandResult.SUCCESS -> Unit
CommandResult.ERROR -> CommandMessage(
event,
emptyList()
).reply(MessageBuilder("There was an error executing this command").build())
CommandResult.NOPE -> logger.warn("The command ${command.name} somehow responded with a nope?")
CommandResult.PARAMETERS -> CommandMessage(event, emptyList()).reply(
"Sorry, but you are missing some parameters: `${command.usage}`"
)
}
}
isGuild -> Unit
else -> event.channel.sendMessage("I don't seem to know this command...").queue()
}
// override fun onMessageUpdate(event: MessageUpdateEvent) =
// onMessageReceived(MessageReceivedEvent(event.jda, event.messageIdLong, event.message))
}
}

View file

@ -0,0 +1,13 @@
package nl.voidcorp.ottobot.events
import net.dv8tion.jda.api.events.DisconnectEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import nl.voidcorp.ottobot.logger
object DisconnectListenerAdapter:ListenerAdapter() {
override fun onDisconnect(event: DisconnectEvent) {
logger.info("Client disconnected for some reason: ${event.closeCode?.meaning}")
}
}

View file

@ -0,0 +1,25 @@
package nl.voidcorp.ottobot.events
import net.dv8tion.jda.api.entities.Activity
import net.dv8tion.jda.api.events.ReadyEvent
import net.dv8tion.jda.api.events.ReconnectedEvent
import net.dv8tion.jda.api.hooks.ListenerAdapter
import nl.voidcorp.ottobot.logger
import kotlin.system.exitProcess
object OttoListener : ListenerAdapter() {
override fun onReady(event: ReadyEvent) {
logger.info("Found ${event.guildTotalCount} different guilds!")
}
override fun onReconnect(event: ReconnectedEvent) {
val id = event.jda.shardInfo.shardId
val reconn = event.responseNumber
val version = "3.0"
if(reconn >=10) exitProcess(0)
event.jda.presence.activity = Activity.playing("v$version ($id~$reconn)")
}
}

View file

@ -0,0 +1,6 @@
package nl.voidcorp.ottobot.external
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class XKCDComic(val num: Int, val alt: String, val img: String, val title: String)

View file

@ -0,0 +1,19 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{0} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
<!--<appender-ref ref="FILE"/>-->
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>