Initial commit

This commit is contained in:
Gabriel Tofvesson 2021-03-30 00:34:57 +02:00
commit 11f66d334f
12 changed files with 373 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Project exclude paths
/.gradle/
/build/
/.idea/

55
build.gradle Normal file
View File

@ -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"
)
}
}

2
settings.gradle Normal file
View File

@ -0,0 +1,2 @@
rootProject.name = 'Swearnt'

View File

@ -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<String>.addMentions(message: Message) = addAllNoDups(message.getMentionedIDs())
private fun MutableCollection<String>.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)
}
}
}

View File

@ -0,0 +1,20 @@
package dev.w1zzrd.swearnt
open class Config(
val content: MutableCollection<String> = ArrayList(),
private val fileName: String,
sort: Boolean = true
): MutableCollection<String> 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)
}

View File

@ -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 <T> MutableCollection<T>.addAllNoDups(elements: Collection<T>): Boolean =
addAll(elements.filterNot(this::contains))
fun RestAction<Message>.submitAndDelete(
delay: Long = BOT_MESSAGE_CLEANUP_DELAY,
timeUnit: TimeUnit = TimeUnit.MILLISECONDS
) = submit().thenAccept { it.delete().submitAfter(delay, timeUnit) }
fun <T> T.load(fileName: String): T where T: MutableCollection<String> {
val file = File(fileName)
if (file.isFile) addAllNoDups(file.readLines())
return this
}
fun Iterable<String>.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" })
}

View File

@ -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<String>,
private val parrotExemptionList: Collection<String>
): 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)
}
}
}

View File

@ -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<Message>::submitAndDelete),
FilterListener(settings, swearList, filteredUIDS.content, parrotList.content)
)
.build()
}

View File

@ -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<String>,
private val submissionPolicy: RestAction<Message>.() -> 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()
}
}

View File

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

View File

@ -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<String> = 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
}
}

View File

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