diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d98cfcf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.idea/ +.gradle/ +build/ +out/ +test.db +gradle/ +gradlew +gradlew.bat +.gitignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index e76af5c..2f360b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .gradle/ build/ out/ +test.db \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..881ea44 --- /dev/null +++ b/.gitlab-ci.yml @@ -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 . diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d69d746 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM gradle:jdk11 as builder + +COPY --chown=gradle:gradle . /home/gradle/src +WORKDIR /home/gradle/src +RUN gradle build + +FROM openjdk:11-jre-slim +COPY --from=builder /home/gradle/src/build/distributions/watchtower.tar /app/watchtower.tar +WORKDIR /app +RUN tar -xvf watchtower.tar +WORKDIR /app/watchtower +CMD bin/watchtower \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 69896dd..477cdb6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { java - kotlin("jvm") version "1.3.61" + application + kotlin("jvm") version "1.3.70" } group = "nl.voidcorp" @@ -10,11 +11,21 @@ repositories { jcenter() } +fun DependencyHandlerScope.exposed(part: String) = implementation("org.jetbrains.exposed", part, "0.22.1") + dependencies { implementation(kotlin("stdlib-jdk8")) - implementation("net.dv8tion:JDA:4.1.1_108") + implementation("net.dv8tion:JDA:4.1.1_108") { + exclude(module = "opus-java") + } testImplementation("junit", "junit", "4.12") 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") + } configure { @@ -27,4 +38,14 @@ tasks { compileTestKotlin { kotlinOptions.jvmTarget = "1.8" } -} \ No newline at end of file + + distTar { + archiveName = "watchtower.tar" + } +} + +application { + applicationName = "watchtower" + mainClassName = "nl.voidcorp.watchtower.MainKt" +} + diff --git a/settings.gradle.kts b/settings.gradle.kts index 8166e44..4b06e68 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,2 +1,2 @@ -rootProject.name = "OttoLogs" +rootProject.name = "WatchTower" diff --git a/src/main/kotlin/nl/voidcorp/ottologs/Main.kt b/src/main/kotlin/nl/voidcorp/ottologs/Main.kt deleted file mode 100644 index e0cc01d..0000000 --- a/src/main/kotlin/nl/voidcorp/ottologs/Main.kt +++ /dev/null @@ -1,49 +0,0 @@ -package nl.voidcorp.ottologs - -import net.dv8tion.jda.api.EmbedBuilder -import net.dv8tion.jda.api.JDABuilder -import net.dv8tion.jda.api.entities.Activity -import net.dv8tion.jda.api.events.message.guild.GuildMessageDeleteEvent -import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent -import net.dv8tion.jda.api.hooks.ListenerAdapter -import java.awt.Color - -fun main() { - val token: String = System.getenv("DISCORD_TOKEN") ?: throw RuntimeException("Missing DISCORD_TOKEN in env!") - val jda = JDABuilder().setToken(token).setActivity(Activity.watching("you")).addEventListeners(LogListener).build() -} - -data class Message(val user: Long, val content: String, val channel: Long, val guild: Long, val message: Long) - -val messages = mutableListOf() - -object LogListener : ListenerAdapter() { - override fun onGuildMessageReceived(event: GuildMessageReceivedEvent) { - if (!event.isWebhookMessage && !event.author.isBot) { - val m = Message( - event.author.idLong, - event.message.contentRaw, - event.message.channel.idLong, - event.guild.idLong, - event.message.idLong - ) - messages += m - } - } - - override fun onGuildMessageDelete(event: GuildMessageDeleteEvent) { - val m = messages.firstOrNull { it.message == event.messageIdLong } ?: return - val logs = event.guild.textChannels.find { it.name == "logs" } - logs?.sendMessage( - EmbedBuilder().setDescription(m.content).setTitle("Message Deleted!") - .addField("Channel", event.channel.asMention, true).addField( - "User", - event.jda.getUserById(m.user)?.asTag ?: "No Idea...", - true - ).setColor(Color.RED) - .build() - )?.queue { - - } - } -} diff --git a/src/main/kotlin/nl/voidcorp/watchtower/DAO.kt b/src/main/kotlin/nl/voidcorp/watchtower/DAO.kt new file mode 100644 index 0000000..f84d762 --- /dev/null +++ b/src/main/kotlin/nl/voidcorp/watchtower/DAO.kt @@ -0,0 +1,37 @@ +package nl.voidcorp.watchtower + +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.sql.transactions.transaction + +class Message(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(Messages) + + var user by Messages.user + var channel by Messages.channel + var guild by Messages.guild + var message by Messages.message + val history by MessageHistory referrersOn MessageHistories.original + + + fun update(new: String) { + transaction { + MessageHistory.new { + content = new + original = this@Message + } + } + } + + val lastVersion + get() = history.maxBy { it.updated }!! +} + +class MessageHistory(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(MessageHistories) + + var content by MessageHistories.content + var updated by MessageHistories.updated + var original by Message referencedOn MessageHistories.original +} \ No newline at end of file diff --git a/src/main/kotlin/nl/voidcorp/watchtower/Main.kt b/src/main/kotlin/nl/voidcorp/watchtower/Main.kt new file mode 100644 index 0000000..a71b8a5 --- /dev/null +++ b/src/main/kotlin/nl/voidcorp/watchtower/Main.kt @@ -0,0 +1,92 @@ +package nl.voidcorp.watchtower + +import net.dv8tion.jda.api.EmbedBuilder +import net.dv8tion.jda.api.JDABuilder +import net.dv8tion.jda.api.entities.Activity +import net.dv8tion.jda.api.events.message.guild.GenericGuildMessageEvent +import net.dv8tion.jda.api.events.message.guild.GuildMessageDeleteEvent +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent +import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent +import net.dv8tion.jda.api.hooks.ListenerAdapter +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.transactions.TransactionManager +import org.jetbrains.exposed.sql.transactions.transaction +import java.awt.Color +import java.sql.Connection +import java.time.Instant + +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" + ) + TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE + transaction { + SchemaUtils.createMissingTablesAndColumns(Messages, MessageHistories) + } + JDABuilder().setToken(token).setActivity(Activity.watching("you")).addEventListeners(LogListener).build() +} + +fun GuildMessageReceivedEvent.log() { + transaction { + Message.new { + channel = this@log.channel.idLong + user = this@log.author.idLong + guild = this@log.guild.idLong + message = this@log.messageIdLong + }.update(this@log.message.contentRaw) + } +} + +object LogListener : ListenerAdapter() { + override fun onGuildMessageReceived(event: GuildMessageReceivedEvent) { + if (!event.isWebhookMessage && !event.author.isBot) { + event.log() + } + } + + override fun onGuildMessageUpdate(event: GuildMessageUpdateEvent) { + if (!event.author.isBot) { + val m = transaction { Message.find { Messages.message eq event.messageIdLong }.firstOrNull() } ?: return + m.update(event.message.contentRaw) + messageEdit(event, "Message Edited!") + } + } + + override fun onGuildMessageDelete(event: GuildMessageDeleteEvent) { + messageEdit(event, "Message Deleted!") + + } + + private fun messageEdit(event: GenericGuildMessageEvent, what: String) { + val m = transaction { Message.find { Messages.message eq event.messageIdLong }.firstOrNull() } ?: return + val logs = event.guild.textChannels.find { it.name == "logs" } + logs?.sendMessage( + EmbedBuilder().setDescription(transaction { m.lastVersion.content }).setTitle(what) + .addField("Channel", event.channel.asMention, true).addField( + "User", + event.jda.getUserById(m.user)?.asTag ?: "No Idea...", + true + ).setColor(Color.ORANGE.let { + if (what.contains("delete", true)) Color.RED.darker() else it + }).apply { + this.addField( + "History", + transaction { + m.history.sortedBy { it.updated }.dropLast(1).joinToString("\n") { it.content } + }, false + ) + }.setTimestamp(Instant.ofEpochMilli(transaction { m.lastVersion.updated })).setFooter("Last changed at") + .build() + )?.queue { + if (event is GuildMessageDeleteEvent) { + transaction { + MessageHistory.find { MessageHistories.original eq m.id }.forEach { it.delete() } + m.delete() + } + } + } + } +} diff --git a/src/main/kotlin/nl/voidcorp/watchtower/Tables.kt b/src/main/kotlin/nl/voidcorp/watchtower/Tables.kt new file mode 100644 index 0000000..f31203a --- /dev/null +++ b/src/main/kotlin/nl/voidcorp/watchtower/Tables.kt @@ -0,0 +1,16 @@ +package nl.voidcorp.watchtower + +import org.jetbrains.exposed.dao.id.IntIdTable + +object Messages : IntIdTable() { + val user = long("user") + val channel = long("channel") + val guild = long("guild") + val message = long("message") +} + +object MessageHistories : IntIdTable() { + val original = reference("original", Messages) + val content = text("content") + val updated = long("updated").clientDefault { System.currentTimeMillis() } +} \ No newline at end of file