commit 11f66d334f16b355b32186a3e52991ac06195354 Author: Gabriel Tofvesson Date: Tue Mar 30 00:34:57 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..645db82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Project exclude paths +/.gradle/ +/build/ +/.idea/ \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..38a01fd --- /dev/null +++ b/build.gradle @@ -0,0 +1,55 @@ +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.4.10' + id "com.github.johnrengelman.shadow" version "6.1.0" +} + +repositories { + mavenCentral() + jcenter() +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = "1.8" + } +} + +tasks.named('jar') { + manifest { + attributes('Implementation-Title': project.name, + 'Implementation-Version': project.version) + } +} + +group 'dev.w1zzrd' +version '1.0-SNAPSHOT' + +compileJava { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' +} + +dependencies { + implementation "com.fasterxml.jackson.core:jackson-core:2.12.2" + implementation "com.fasterxml.jackson.core:jackson-databind:2.12.2" + implementation "org.jetbrains.kotlin:kotlin-stdlib" + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + compile("net.dv8tion:JDA:4.2.0_168") +} + +test { + useJUnitPlatform() +} + +shadowJar { + manifest { + attributes( + 'Implementation-Title': project.name, + 'Implementation-Version': project.version, + 'Main-Class': "dev.w1zzrd.swearnt.InitKt" + ) + } +} + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..24f567a --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'Swearnt' + diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/CommandListener.kt b/src/main/kotlin/dev/w1zzrd/swearnt/CommandListener.kt new file mode 100644 index 0000000..9817bdf --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/CommandListener.kt @@ -0,0 +1,44 @@ +package dev.w1zzrd.swearnt + +import net.dv8tion.jda.api.MessageBuilder +import net.dv8tion.jda.api.entities.Message +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.hooks.ListenerAdapter + +private fun Message.getMentionedIDs() = mentionedMembers.map(net.dv8tion.jda.api.entities.ISnowflake::getId) +private fun MutableCollection.addMentions(message: Message) = addAllNoDups(message.getMentionedIDs()) +private fun MutableCollection.removeMentions(message: Message) = removeAll(message.getMentionedIDs()) + +class CommandListener ( + private val settings: Settings, + private val adminIDs: Config, + private val filterList: Config, + private val parrotList: Config, + private val swearFilter: SwearFilter +): ListenerAdapter() { + override fun onMessageReceived(event: MessageReceivedEvent) { + if (event.author.id in adminIDs) { + var check = event.message.contentDisplay.toLowerCase() + + if (!check.startsWith("${settings.commandTrigger} ")) + return + + check = check.substring(settings.commandTrigger.length + 1) + + when { + check.startsWith("filter ") -> filterList.addMentions(event.message) + check.startsWith("unfilter ") -> filterList.removeMentions(event.message) + check.startsWith("parrot ") -> parrotList.addMentions(event.message) + check.startsWith("unparrot ") -> parrotList.removeMentions(event.message) + check.startsWith("disallow ") -> swearFilter += check.substring("disallow ".length) + check.startsWith("allow ") -> swearFilter -= check.substring("allow ".length) + else -> return + } + + event.message.delete().submit() + event.channel + .sendMessage("Cool beans") + .submitAndDelete(settings.botCleanupDelay) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/Config.kt b/src/main/kotlin/dev/w1zzrd/swearnt/Config.kt new file mode 100644 index 0000000..f73ba0b --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/Config.kt @@ -0,0 +1,20 @@ +package dev.w1zzrd.swearnt + +open class Config( + val content: MutableCollection = ArrayList(), + private val fileName: String, + sort: Boolean = true +): MutableCollection by content { + init { + load() + + if (sort) { + val sorted = content.sorted() + content.clear() + content.addAll(sorted) + } + } + + fun load() = content.load(fileName) + fun save() = content.save(fileName) +} diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/Extensions.kt b/src/main/kotlin/dev/w1zzrd/swearnt/Extensions.kt new file mode 100644 index 0000000..d0201b2 --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/Extensions.kt @@ -0,0 +1,29 @@ +package dev.w1zzrd.swearnt + +import net.dv8tion.jda.api.entities.Message +import net.dv8tion.jda.api.requests.RestAction +import java.io.File +import java.util.concurrent.TimeUnit + +fun MutableCollection.addAllNoDups(elements: Collection): Boolean = + addAll(elements.filterNot(this::contains)) + +fun RestAction.submitAndDelete( + delay: Long = BOT_MESSAGE_CLEANUP_DELAY, + timeUnit: TimeUnit = TimeUnit.MILLISECONDS +) = submit().thenAccept { it.delete().submitAfter(delay, timeUnit) } + +fun T.load(fileName: String): T where T: MutableCollection { + val file = File(fileName) + if (file.isFile) addAllNoDups(file.readLines()) + + return this +} + +fun Iterable.save(fileName: String) { + val file = File(fileName) + if (file.isFile && (!file.delete() || !file.createNewFile())) + System.err.println("Failed to save file \"$fileName\"") + else + file.writeText(reduce { acc, s -> "$acc\n$s" }) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/FilterListener.kt b/src/main/kotlin/dev/w1zzrd/swearnt/FilterListener.kt new file mode 100644 index 0000000..14aaefc --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/FilterListener.kt @@ -0,0 +1,28 @@ +package dev.w1zzrd.swearnt + +import net.dv8tion.jda.api.MessageBuilder +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.hooks.ListenerAdapter + +class FilterListener ( + private val settings: Settings, + private val filter: SwearFilter, + private val filterList: Collection, + private val parrotExemptionList: Collection +): ListenerAdapter() { + override fun onMessageReceived(event: MessageReceivedEvent) { + if (event.author.id !in parrotExemptionList && + filter shouldFilter event.message.contentDisplay && + event.author.id in filterList) { + + event.message.delete().submit() + event.channel.sendMessage( + MessageBuilder() + .append("You said a naughty word, ") + .append(event.author) + .append('!') + .build() + ).submitAndDelete(settings.botCleanupDelay) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/Init.kt b/src/main/kotlin/dev/w1zzrd/swearnt/Init.kt new file mode 100644 index 0000000..273f9d9 --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/Init.kt @@ -0,0 +1,46 @@ +package dev.w1zzrd.swearnt + +import net.dv8tion.jda.api.JDABuilder +import net.dv8tion.jda.api.entities.Activity +import net.dv8tion.jda.api.entities.Message +import net.dv8tion.jda.api.requests.RestAction +import java.util.* + + +const val BOT_MESSAGE_CLEANUP_DELAY = 5000L + +fun main(vararg args: String) { + val settings = loadSettings() + val adminIDs = Config(fileName = settings.adminConf) + val filteredUIDS = Config(fileName = settings.filterConf) + val parrotList = Config(fileName = settings.parrotConf) + val swearList = SwearFilter(fileName = settings.swearsConf) + + + Runtime.getRuntime().addShutdownHook(Thread { + println("Saving configs...") + settings.save() + adminIDs.save() + filteredUIDS.save() + parrotList.save() + swearList.save() + println("Configs saved!") + }) + + val token: String = if (args.isEmpty()) { + print("Enter discord bot token: ") + Scanner(System.`in`).nextLine() + } + else args[0] + + + JDABuilder.createDefault(token) + .setBulkDeleteSplittingEnabled(false) + .setActivity(Activity.playing("with ur mom")) + .addEventListeners( + CommandListener(settings, adminIDs, filteredUIDS, parrotList, swearList), + ParrotListener(settings, swearList, parrotList.content, RestAction::submitAndDelete), + FilterListener(settings, swearList, filteredUIDS.content, parrotList.content) + ) + .build() +} \ No newline at end of file diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/ParrotListener.kt b/src/main/kotlin/dev/w1zzrd/swearnt/ParrotListener.kt new file mode 100644 index 0000000..826b773 --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/ParrotListener.kt @@ -0,0 +1,21 @@ +package dev.w1zzrd.swearnt + +import net.dv8tion.jda.api.MessageBuilder +import net.dv8tion.jda.api.entities.Message +import net.dv8tion.jda.api.events.message.MessageReceivedEvent +import net.dv8tion.jda.api.hooks.ListenerAdapter +import net.dv8tion.jda.api.requests.RestAction +import java.util.concurrent.CompletableFuture + +class ParrotListener( + private val settings: Settings, + private val filter: SwearFilter, + private val parrotList: Collection, + private val submissionPolicy: RestAction.() -> CompletableFuture<*> = { submitAndDelete(settings.botCleanupDelay) } +): ListenerAdapter() { + override fun onMessageReceived(event: MessageReceivedEvent) { + if (event.author.id in parrotList && filter shouldFilter event.message.contentDisplay) + event.channel.sendMessage(MessageBuilder(event.message.contentRaw.toUpperCase()).build()) + .submissionPolicy() + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/Settings.kt b/src/main/kotlin/dev/w1zzrd/swearnt/Settings.kt new file mode 100644 index 0000000..5ff4e9c --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/Settings.kt @@ -0,0 +1,42 @@ +package dev.w1zzrd.swearnt + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.ObjectMapper +import java.io.File +import java.io.FileOutputStream + +fun loadSettings(settingsFile: String = "settings.json"): Settings { + val file = File(settingsFile) + if (file.isFile) return ObjectMapper().readValue(file, Settings::class.java) + + return Settings() +} + +class Settings { + @JsonProperty + var commandTrigger = "bruh" + + @JsonProperty + var botCleanupDelay = 5000L + + @JsonProperty + var adminConf = "admin.conf" + + @JsonProperty + var filterConf = "filter.conf" + + @JsonProperty + var parrotConf = "parrot.conf" + + @JsonProperty + var swearsConf = "swears.conf" + + fun save(fileName: String = "settings.json") { + val file = File(fileName) + if (file.isFile && (!file.delete() || !file.createNewFile())) + System.err.println("Failed to save settings") + else FileOutputStream(file).use { + ObjectMapper().writeValue(it, this) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/w1zzrd/swearnt/SwearFilter.kt b/src/main/kotlin/dev/w1zzrd/swearnt/SwearFilter.kt new file mode 100644 index 0000000..386fc3e --- /dev/null +++ b/src/main/kotlin/dev/w1zzrd/swearnt/SwearFilter.kt @@ -0,0 +1,61 @@ +package dev.w1zzrd.swearnt + +private fun Char.similarChars(): String = + arrayOf( + "!1il", + "o0ö", + "aåä4", + "b8", + "e3", + "t7", + "5s" + ).firstOrNull { it.contains(this.toLowerCase()) } ?: toString() + +private fun String.makeSwearFilter(start: Boolean = false, end: Boolean = false) = Regex ( + (if (start) "^.*?" else "^") + + map(Char::similarChars) + .map { if (it.length==1) it else "[$it]" } + .reduce(String::plus) + + (if (end) ".*?$" else "$") +) + +private fun String.makeStartSwearFilter() = makeSwearFilter(start = true, end = false) +private fun String.makeEndSwearFilter() = makeSwearFilter(start = false, end = true) + +private fun String.filteredBy(filter: Filter) = this in filter + +data class Filter(val text: String) { + private val filterPre = text.makeStartSwearFilter() + private val filterPost = text.makeEndSwearFilter() + + operator fun contains(text: String) = this.text == text || filterPre.matches(text) || filterPost.matches(text) +} + +class SwearFilter(content: MutableCollection = ArrayList(), fileName: String): Config(content, fileName) { + private val filters = map(::Filter).toMutableList() + + infix fun shouldFilter(phrase: String) = + phrase.toLowerCase() + .replace(Regex("[.,?*;:<>|]"), "") // Remove some punctuation and stuff + .split("\n", " ", "\t") + .firstOrNull { word -> filters.firstOrNull(word::filteredBy) != null } != null + + override operator fun contains(element: String) = this shouldFilter element + override fun add(element: String) = + if (element !in this) { + filters += Filter(element) // Whack + super.add(element) + true + } else { + false + } + + override fun remove(element: String) = + if (element in this) { + filters.removeIf(element::filteredBy) + super.remove(element) + true + } else { + false + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/w1zzrd/swearnt/test/SwearFilterTest.kt b/src/test/kotlin/dev/w1zzrd/swearnt/test/SwearFilterTest.kt new file mode 100644 index 0000000..8fd54c6 --- /dev/null +++ b/src/test/kotlin/dev/w1zzrd/swearnt/test/SwearFilterTest.kt @@ -0,0 +1,21 @@ +package dev.w1zzrd.swearnt.test + +import dev.w1zzrd.swearnt.SwearFilter +import org.junit.jupiter.api.Test + +val acceptableWords = arrayOf("frick", "heck", "darn", "hello", "cringe") +val veryBadWords = arrayOf("dang", "cr4p", "stup!d") + +class SwearFilterTest { + private val filter = SwearFilter(mutableListOf("dang", "crap", "stupid"), "filter") + + private fun testStrings(vararg words: String, shouldAllow: Boolean) = + words.forEach { assert(filter shouldFilter it != shouldAllow) { it } } + + + @Test + fun testAcceptableStrings() = testStrings(*acceptableWords, shouldAllow = true) + + @Test + fun testVeryBadStrings() = testStrings(*veryBadWords, shouldAllow = false) +} \ No newline at end of file