diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ed68e6894931a78d267d5abaaf3094c0659781a2..25a5f250012e6bacc3112111f9a5e1fc26dd04bb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -27,15 +27,13 @@ test:
     SSL_KEY_DATA: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUpRUUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQ1Nzd2dna25BZ0VBQW9JQ0FRRE5qbVUybnVBU3M2YVEKZHB4QmRudHFCaTVkV0JtZ242bWxYa0l5alV0dzJQSDR5QnZaVytvUTh4TlRKM3FCcmFMdTAybjFyNm9kcnRVUApLV2tUYVhGdm43VTZuRkhuQmo3akUwQngwbG8yU1E2SEx4OXJ4dWNUT1BMdVE2aXpWRTRiTzNoS0Q0ck93QnFiCnJPOFp2TUpveHFKQ21HVytsblZ4aTNEaER5MjltVWtCK0RFUjVBRExUTmN4ZC95OUcyTTBTNHBmZm1rT3N0RDkKLytJVVgxSXN1V1pDSWowdC9ueVR6by8yV2dBYnBDN1gzMW9uRzNnTHpRNU95YWRoYjZGWDF6MTFOZUFsMEdsZAplZ1lYd20xaDJwbERPSGRHVnJ2T1NmQ1c2Si9pVVg1aUw0UHRNK0o1V1dpZW10dDU1RHVKWXkraUNuY0pSRkdHCnpMbWVHNHBnc1pwTm9ON2R1dzhRd1dBSHJuUWtHRUk3NVFPMzJIL1c5clFZM2IxY2V3VWNoWk5QV2ZzMGxpbDEKY2V1NGkrWjFFV1BJam1QckJ4ZTdmdy83Mnc4ZGY1dzVGMlQ4UkpkWTJaUTU3dHNRcERQTEdJS0hGN0g0eUdsdAozOVhYQmllcGErZElPdlM0RXdRYms5ZzI5eTR2ZFp3Szk3UGxlVkszOVFuRXpCWXFUUUhYbU42amU5ZXk4eXJDClVoYVl3TGIxQ1VJVDRwL1NMTkFHWkdBcXc4U0dHU1dJRUdXbmd3cTEwVU1LRXZLaUlWMW00QjEwVDFFOElCczUKa3FJT3R2cTM4Sjl5UTBReWpocm9tQ0hQTG9RWTlKampsZnhnWG9CWU5YTEpHbC9oREwzYnE4ZG84alhPc3RTVApobGlSMmRuTFRFaDVaSnVQN3h2WlJCNkZ1TngzRHdJREFRQUJBb0lDQUExeGFKRWY1c3VTVUN4V2RYV2FpQXV4CkI4czIvY1lSYXdqVGwwU3pGT0gyYml5MCtZMUhnNUZFTkZsVjFaOHhlZHhnbXlka0s1M3hWeUc4dFpCOWJ0dTcKK0NBekpQQVU2bnZ6UUYyeFFoRVd5Z3B4UEg3UjdUN0dsS3ljWkNZR04yMTBnRE5udk00MHBnalVVSGJBYjM1bQpyeW5ueVkralMxNzNuWlE2WitWa1p1L29DVjJBS2NVaDYxamkzZmFJR2Y3TGllc2cyMElJMDc0b3crSk5NWlNYCk0yYlQwbWgxb2pRUWFEM1dPUGVWenpKeSt2UmZ5WVFNRHdsOENxUkdwcWlWL1FEeld3dGVDK1gvR3ZMbTFqeDIKRFZ2bUQzYmNLVUFlZWN1RXZ5QVA4RkgvaFlNM3gzSGtOUUZhWTB3ZmJ2MVhMVUJOcXVkQ1BvcXdUTnRZTmI1cQo3L0lEUkphQVh2MDE2WEhkMzFBSFB0Ynp3bEx6dEZpVHljbm1ML2V4ZnhaREVYYUFVMjhDNVB1WXlMUU5xbTdzCms2QTlCdE5Zd2NmZmJzMlR5QXE2UmdPUXMyRVJXZEhoazhzeVBOd0JkZ2VYVFhTTHF1YlREODd5U2IvWWZMaFkKL1kvbzkxYTBONEFoSkpEZjVqSnhKZmt6RGZSd0lpam81bW5VU3BrRXRkNDh1UjE0c3RzeU9MdEZXUmwrb2NsTgpvOHJDandhdUhHN0FlRnB6MWhkaDJDUWUwdE1vaGYvVG51U3BEeG9rSlF3bVRJS2dYZ3Vic2xmSHRub1cwS0ZrCmpLaHdRcDJyMysxbW54T1MxbnFGVXRuUytHcTZ2WVkvbFQ3cTZuQ2pLS21sbDdjMW4xNFB0eCtVNklvQnJhcDEKcXQ4MWx0eG5UdllJb3JNR3VncGhBb0lCQVFELzQ1VGZTZDUrRytWYzJpOXJHazdYTjNhZ1drbVF0Sld0MTlORgpPN3NGZlBSM2JBOUpxSTNZQWw3ejRodnU0blRRSEVXTU5VeERwRGpnNS9DRW95YXI0V2N4TUFRODVKMHcyTGNNCklYU09JQ2JwV0haeDFUaUFqLzZpQWpnaGZ6WW1RUGNvUVhzNUdMbGdOOElMNitqSkdja2FjVWRKSkw4Ykc1VUgKb2JqWWc4WmMxRDNxUEVuTG9iTDhLdC9peFMrbHFzVncrRkRGMzVhbElVTUQwWkpyU1U5NG5ETnF0K0hDRTF4UApNRm1XWXN5NjhleTAzZ3hDMnZ3Myt0VERzMCs1MjFsLzB5dyttL0Y0cC85MkJIbmsvSnRhOGVnRi80bW9KbCtUCmo0QUhuWWlDcEgrNk8xS1FrdW04MkhrQkdiTUhrYjZpMGdNTXRMYkM1akh4R29aNUFvSUJBUUROcFRsWE9sZDEKOWt6b0lJNlpEVVFVRXVISWQ5QWRqQ243YmVraTJQWk9pcTMxajY3UWlTb0NJQUt0RmhyYjhQakp5YXhOWUthcwpOWGs2UDZNRDVnVkRRb2plQnYyNGNheUdHOTVGQ1VZdElWRDJIcm03L3NSMndrSlJ2QlJPbFlha1JDL3prUGtHCmR5RXRmaXZJVUJ6bkJNNE1JM2d4NzQxbEJWRnBwQWRxdzJxTjZKUU1vVVZBb0VQa2lFZzd3eStmUVVXejJPTXIKdloxeUlQeEpEaDJjN1BLSVVmN3g2SDA4VEhPWFZaK3NXem9GSFo0Q1ZzSGJzMjNaM0lSTHdUNFlEK1RkRCtpbgp6TWExbTE0ZTN4dHQ1cUxwRSt6ZVNCL1dVV1pLcFNsZ1VLaFJ5bGdkcWIvcjg1UklXN29hV2s3SUNmQ2E5WUwxCmplRHlMeVl1YmFmSEFvSUJBRGhiaFZSUVRxSnp2bVplMzRhMU9wd0g2U2FUL0JQVTBncUJ1RlJOUFhtTjljRy8KbVBaZUd5OXlCanVzbHY1Yi9lSS85OGxUaThKeUR0enArSDBkK2N4dFRtNzA1bG9LOTl2a1B4eDYyZExibmZaUwp0M09HeEhUOFFkYW9xbmdtTG1UcWRnVDF0dy95TkJITzlmdnVMMHpyVXZGeDlZTlVob3FQM3BqWnMzNXNOMm9HCmpNUmtGdFMwZmxrdmtETy92aWk4bmRPdHZReDNuQlF1YVRZVUdDMXM3Z1hnVVNxMTZSRDNkcU15UU9qd1JhcisKMFdWY2FsTG5MQm1nTXZBUE5BWDVHNU1kaldjVXJYQW5nQW9jSGtTaXBneTNycVJ6alh2dFI2dVdOVnA0QmJMUQpUQmxXSzI4UURFNjlWcGs1Y2NhL0FMK1hoWGhzN0x1c08xK2d1ZkVDZ2dFQWZHZ0xBMkVSRGhUZHU4UU9ZRHJ0ClIzT1EwYlRoMnk0eks1NzNYaFNCRlV6Q0puOHcwNGxYTjRmajlwQWIyWml5K1dnZTY0U2Y4Q2c5V1dhc0dLeXIKM0YvQTZ3aXhyMFpkaDVnT1pCZFRNL1FteFc0YkVNYjBWWi81ZlBiYUZoeFJJc2o1ZFZEcnhlU0YxcjZ3Zi9NdgpPUGJvSytHOVVnQkl1cWQzOC8rK1dQRTFZZm9rcm10VnVOMzdsS0o1aUdYeFJsZTNjakN3WllMRllBamlkdE9xClNJZnp4VkpOZUUwY2prRDE0TVIwMzFFbERYazRZTlBaWFM3ME1zczc0WlJiR3pWcVQrM1M0c2g0SWQrSEZnZ0UKMFB5bzYzWVpZdk9oQndlaGFXRDNZZ1FKZjhsNGV5RjVNS1hmdTlKNkNIMC9rYmFwcnlUOWY4M0FHdU01SnZkQgpld0tDQVFBb1ZOK0hTL2FmMk8wOSszVEV0Rnl2NG01b3pGQjZudGFDMTJkaHJib1Z3QXc4TVBMR1hhRXFzZlBWCkw5T1JNQTZsUkwrNE9JbzJGY002VGtsRC9yTUFIci9iOVZ0V291OEVSZWlPUjF6U2d0N29wQ1o2d0xoQm5wOVAKbXN6MXB1UC9PK1NSM2xyY1FPbUVUTUs3b2Zxb2V3ZW01cnowQ21UcmFYcXJlSHV6UlgrYy9uV1FxbUpEeU44bwpOZUhoRzVzSXRJaGVQcE5RK1FjUGV6VnlsKyt2TUhGd09POTJvK2kvejd6SDZ1Y1IrZzhPUjQxdlhqSVFFUDVuCjV3OU9HeU94VmZnRmhPNlErQ1VmSXMzSk1sd28vWGt3RnAyTkpvaE5XQVB6UEl5SjVWOVhwSmt1YVdKMTdjL1UKandmWU56Tmx2MXBEeVZHQWFJOG9NRFhSTWFaMwotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg=="
     SSL_REQUIRED: "true"
   script:
-    - "./gradlew check ktlintCheck coberturaTestReport --stacktrace"
+    - "./gradlew check ktlintCheck --stacktrace"
   artifacts:
     paths:
       - "*/build/test-results/**/TEST-*.xml"
       - "*/build/reports/*.xml"
-      - "*/build/reports/cobertura/*.xml"
+      - "*/build/reports/jacoco/*.xml"
     reports:
-      cobertura:
-        - "*/build/reports/cobertura/*.xml"
       junit:
         - "*/build/test-results/**/TEST-*.xml"
         - "*/build/reports/*.xml"
diff --git a/build.gradle.kts b/build.gradle.kts
index 4379fdba1b5b1cd31379076751c8d57dd0311403..c8b02b807705e7e1b99554417aff87c3a7890a81 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,77 +7,12 @@
  * obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-import de.justjanne.coverageconverter.CoverageConverterExtension
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
 plugins {
-  id("org.jlleitschuh.gradle.ktlint") version "10.1.0" apply false
-  id("com.vanniktech.maven.publish") version "0.13.0" apply false
-  id("de.justjanne.jacoco-cobertura-converter") apply false
-  id("org.jetbrains.dokka") version "1.4.32"
-}
-
-allprojects {
-  apply(plugin = "org.jetbrains.dokka")
-
-  repositories {
-    mavenCentral()
-    google()
-  }
+  id("justjanne.dokka")
+  id("justjanne.publish-maven-central")
+  idea
+  eclipse
 }
 
-subprojects {
-  apply(plugin = "org.jetbrains.kotlin.jvm")
-  apply(plugin = "org.jlleitschuh.gradle.ktlint")
-  apply(plugin = "jacoco")
-  apply(plugin = "de.justjanne.jacoco-cobertura-converter")
-
-  dependencies {
-    val implementation by configurations
-    val testImplementation by configurations
-    val testRuntimeOnly by configurations
-
-    implementation(kotlin("stdlib"))
-    testImplementation(kotlin("test-junit5"))
-
-    val kotlinxCoroutinesVersion: String by project
-    implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-core", kotlinxCoroutinesVersion)
-    testImplementation("org.jetbrains.kotlinx", "kotlinx-coroutines-test", kotlinxCoroutinesVersion)
-
-    val junit5Version: String by project
-    testImplementation("org.junit.jupiter", "junit-jupiter-api", junit5Version)
-    testImplementation("org.junit.jupiter", "junit-jupiter-params", junit5Version)
-    testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine", junit5Version)
-
-    val hamcrestVersion: String by project
-    testImplementation("org.hamcrest", "hamcrest-library", hamcrestVersion)
-  }
-
-  tasks.withType<Test> {
-    useJUnitPlatform()
-  }
-
-  tasks.withType<KotlinCompile> {
-    kotlinOptions {
-      jvmTarget = "1.8"
-      freeCompilerArgs = listOf(
-        "-Xinline-classes",
-        "-Xopt-in=kotlin.ExperimentalUnsignedTypes"
-      )
-    }
-  }
-
-  configure<JacocoPluginExtension> {
-    toolVersion = "0.8.7"
-  }
-
-  configure<CoverageConverterExtension> {
-    autoConfigureCoverage = true
-  }
-
-  configure<JavaPluginExtension> {
-    toolchain {
-      languageVersion.set(JavaLanguageVersion.of(8))
-    }
-  }
-}
+group = "de.justjanne.libquassel"
+version = "0.7.0"
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
deleted file mode 100644
index bf5b593ca0153642f7efdf1fab2311df2ea053fb..0000000000000000000000000000000000000000
--- a/buildSrc/build.gradle.kts
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * libquassel
- * Copyright (c) 2021 Janne Mareike Koschinski
- *
- * This Source Code Form is subject to the terms of the Mozilla Public License,
- * v. 2.0. If a copy of the MPL was not distributed with this file, You can
- * obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-plugins {
-  `kotlin-dsl`
-}
-
-kotlinDslPluginOptions {
-  experimentalWarning.set(false)
-}
-
-repositories {
-  google()
-  jcenter()
-}
-
-dependencies {
-  implementation("org.jetbrains.kotlin", "kotlin-gradle-plugin", "1.5.10")
-  implementation("de.justjanne", "jacoco-cobertura-converter", "1.0.0")
-  implementation(gradleApi())
-  implementation(localGroovy())
-}
diff --git a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterAction.kt b/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterAction.kt
deleted file mode 100644
index 02ea8b2c73106776e3170f224568d992aa11b8ab..0000000000000000000000000000000000000000
--- a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterAction.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * libquassel
- * Copyright (c) 2021 Janne Mareike Koschinski
- *
- * This Source Code Form is subject to the terms of the Mozilla Public License,
- * v. 2.0. If a copy of the MPL was not distributed with this file, You can
- * obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package de.justjanne.coverageconverter
-
-import com.fasterxml.jackson.databind.SerializationFeature
-import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule
-import com.fasterxml.jackson.dataformat.xml.XmlMapper
-import com.fasterxml.jackson.module.kotlin.KotlinModule
-import de.justjanne.coverageconverter.jacoco.CounterTypeDto
-import de.justjanne.coverageconverter.jacoco.ReportDto
-import org.gradle.api.Action
-import org.gradle.api.Task
-import org.gradle.testing.jacoco.tasks.JacocoReport
-import java.io.File
-
-internal class CoverageConverterAction(
-  private val jacocoReportTask: JacocoReport
-) : Action<Task> {
-  private fun findOutputFile(jacocoFile: File): File? {
-    val actualFile = jacocoFile.absoluteFile
-    if (actualFile.exists() && actualFile.parentFile.name == "jacoco") {
-      val folder = File(actualFile.parentFile.parentFile, "cobertura")
-      folder.mkdirs()
-      return File(folder, actualFile.name)
-    }
-
-    return null
-  }
-
-  override fun execute(task: Task) {
-    fun printTotal(data: ReportDto) {
-      val instructionRate = convertCounter(data.counters, CounterTypeDto.INSTRUCTION).rate * 100
-      val instructionMissed = convertCounter(data.counters, CounterTypeDto.INSTRUCTION).missed
-      val branchRate = convertCounter(data.counters, CounterTypeDto.BRANCH).rate * 100
-      val branchMissed = convertCounter(data.counters, CounterTypeDto.BRANCH).missed
-      println("[JacocoPrinter] Instructions  $instructionRate%  (Missed $instructionMissed)")
-      println("[JacocoPrinter] Branches      $branchRate%  (Missed $branchMissed)")
-    }
-
-    fun convertFile(input: File, output: File) {
-      val mapper = XmlMapper(
-        JacksonXmlModule().apply {
-          setDefaultUseWrapper(false)
-        }
-      ).apply {
-        enable(SerializationFeature.INDENT_OUTPUT)
-        enable(SerializationFeature.WRAP_ROOT_VALUE)
-        registerModule(KotlinModule(strictNullChecks = true))
-      }
-      val data = mapper.readValue(input, ReportDto::class.java)
-      val result = convertReport(data)
-      printTotal(data)
-      mapper.writeValue(output, result)
-    }
-
-    jacocoReportTask.reports.forEach {
-      if (it.isEnabled && it.destination.extension == "xml") {
-        val outputFile = findOutputFile(it.destination)
-        if (outputFile != null) {
-          convertFile(it.destination, outputFile)
-        }
-      }
-    }
-  }
-}
diff --git a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterPlugin.kt b/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterPlugin.kt
deleted file mode 100644
index 5feeb4092ee5449d0e7b0c165d6708683f839496..0000000000000000000000000000000000000000
--- a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterPlugin.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * libquassel
- * Copyright (c) 2021 Janne Mareike Koschinski
- *
- * This Source Code Form is subject to the terms of the Mozilla Public License,
- * v. 2.0. If a copy of the MPL was not distributed with this file, You can
- * obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-package de.justjanne.coverageconverter
-
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.kotlin.dsl.configure
-import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
-import org.gradle.testing.jacoco.tasks.JacocoReport
-import java.io.File
-
-class CoverageConverterPlugin : Plugin<Project> {
-  override fun apply(project: Project) {
-    val extension = project.extensions.create("coverage", CoverageConverterExtension::class.java)
-    project.afterEvaluate {
-      val testTask = tasks.getByName("test")
-
-      val jacocoReportTask = tasks.getByName("jacocoTestReport") as? JacocoReport
-      if (jacocoReportTask != null) {
-        jacocoReportTask.dependsOn(testTask)
-        if (extension.autoConfigureCoverage) {
-          jacocoReportTask.sourceDirectories.from(fileTree("src/main/kotlin"))
-          jacocoReportTask.classDirectories.from(fileTree("build/classes/kotlin/main"))
-          jacocoReportTask.reports {
-            xml.destination = File("${buildDir}/reports/jacoco/report.xml")
-            html.isEnabled = true
-            xml.isEnabled = true
-            csv.isEnabled = false
-          }
-        }
-
-        tasks.register("coberturaTestReport") {
-          dependsOn(jacocoReportTask)
-          mustRunAfter(jacocoReportTask)
-          group = "verification"
-
-          doLast(CoverageConverterAction(jacocoReportTask))
-        }
-      }
-    }
-  }
-}
diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/de.justjanne.jacoco-cobertura-converter.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/de.justjanne.jacoco-cobertura-converter.properties
deleted file mode 100644
index 899cb47c98b0506df1baebd4c32b49d8747217cc..0000000000000000000000000000000000000000
--- a/buildSrc/src/main/resources/META-INF/gradle-plugins/de.justjanne.jacoco-cobertura-converter.properties
+++ /dev/null
@@ -1,10 +0,0 @@
-#
-# libquassel
-# Copyright (c) 2021 Janne Mareike Koschinski
-#
-# This Source Code Form is subject to the terms of the Mozilla Public License,
-# v. 2.0. If a copy of the MPL was not distributed with this file, You can
-# obtain one at https://mozilla.org/MPL/2.0/.
-#
-
-implementation-class=de.justjanne.coverageconverter.CoverageConverterPlugin
diff --git a/gradle.properties b/gradle.properties
index 8d42b60cacb534f893e92582116ce2a6ddc4ae39..00006d501ab8fe44261320e1f04b0a4e35596077 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,29 +1,2 @@
 org.gradle.jvmargs=-XX:MaxMetaspaceSize=512m
 kotlin.code.style=official
-
-bouncyCastleVersion=1.68
-hamcrestVersion=2.2
-junit5Version=5.7.2
-kotlinVersion=1.5.10
-kotlinBitflagsVersion=1.3.0
-kotlinxCoroutinesVersion=1.5.0
-sl4jVersion=1.7.30
-testcontainersCiVersion=1.3.0
-threetenBpVersion=1.5.1
-kotlinPoetVersion=1.8.0
-kspVersion=1.5.10-1.0.0-beta01
-
-GROUP=de.justjanne.libquassel
-VERSION_NAME=0.6.2
-
-POM_URL=https://git.kuschku.de/justJanne/libquassel
-POM_SCM_URL=https://git.kuschku.de/justJanne/libquassel
-POM_SCM_CONNECTION=scm:git:https://git.kuschku.de/justJanne/libquassel.git
-POM_SCM_DEV_CONNECTION=scm:git:ssh://git.kuschku.de:2222/justJanne/libquassel.git
-
-POM_LICENSE_NAME=Mozilla Public License Version 2.0
-POM_LICENSE_URL=https://www.mozilla.org/en-US/MPL/2.0/
-POM_LICENSE_DIST=repo
-
-POM_DEVELOPER_ID=justJanne
-POM_DEVELOPER_NAME=Janne Mareike Koschinski
diff --git a/gradle/convention/build.gradle.kts b/gradle/convention/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..f9cf1d91e5f1f21a36fac80c70de02d073c1cb6c
--- /dev/null
+++ b/gradle/convention/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+  `kotlin-dsl`
+}
+
+repositories {
+  gradlePluginPortal()
+  mavenCentral()
+  google()
+}
+
+dependencies {
+  implementation("io.github.gradle-nexus:publish-plugin:1.1.0")
+  implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
+  implementation("org.jetbrains.dokka:dokka-gradle-plugin:1.6.10")
+  implementation("com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:1.6.10-1.0.4")
+  implementation("org.jlleitschuh.gradle:ktlint-gradle:10.2.1")
+}
+
+configure<JavaPluginExtension> {
+  toolchain {
+    languageVersion.set(JavaLanguageVersion.of(8))
+  }
+}
diff --git a/gradle/convention/gradle/wrapper/gradle-wrapper.jar b/gradle/convention/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000000000000000000000000000000000..7454180f2ae8848c63b8b4dea2cb829da983f2fa
Binary files /dev/null and b/gradle/convention/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/convention/gradle/wrapper/gradle-wrapper.properties b/gradle/convention/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000000000000000000000000000000000..aa520627104195ad680a007476109668c21ab7e8
--- /dev/null
+++ b/gradle/convention/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionSha256Szm=8cc27038d5dbd815759851ba53e70cf62e481b87494cc97cfd97982ada5ba634
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradle/convention/settings.gradle.kts b/gradle/convention/settings.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..6ef6296c7c9204e61d1ea637baeef34642ce7f9a
--- /dev/null
+++ b/gradle/convention/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "convention"
diff --git a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterExtension.kt b/gradle/convention/src/main/kotlin/justjanne.dokka.gradle.kts
similarity index 57%
rename from buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterExtension.kt
rename to gradle/convention/src/main/kotlin/justjanne.dokka.gradle.kts
index 7fed3cb75c2ac472590275d129cf28b439d386a3..0af2f3e6dd18ded330c15045d071e4236d6f23f0 100644
--- a/buildSrc/src/main/kotlin/de/justjanne/coverageconverter/CoverageConverterExtension.kt
+++ b/gradle/convention/src/main/kotlin/justjanne.dokka.gradle.kts
@@ -1,14 +1,12 @@
 /*
  * libquassel
- * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2022 Janne Mareike Koschinski
  *
  * This Source Code Form is subject to the terms of the Mozilla Public License,
  * v. 2.0. If a copy of the MPL was not distributed with this file, You can
  * obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-package de.justjanne.coverageconverter
-
-open class CoverageConverterExtension {
-  var autoConfigureCoverage: Boolean = false
+plugins {
+  id("org.jetbrains.dokka")
 }
diff --git a/gradle/convention/src/main/kotlin/justjanne.java.gradle.kts b/gradle/convention/src/main/kotlin/justjanne.java.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..fa7b551bdd3fae23b6abda147fb0f62e617ce9ae
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/justjanne.java.gradle.kts
@@ -0,0 +1,29 @@
+plugins {
+  java
+  jacoco
+}
+
+version = rootProject.version
+group = rootProject.group
+
+tasks.getByName("jacocoTestReport") {
+  enabled = false
+}
+
+tasks.withType<JavaCompile> {
+  sourceCompatibility = "1.8"
+  targetCompatibility = "1.8"
+}
+
+tasks.withType<Test> {
+  useJUnitPlatform()
+}
+
+configure<JavaPluginExtension> {
+  withJavadocJar()
+  withSourcesJar()
+
+  toolchain {
+    languageVersion.set(JavaLanguageVersion.of(8))
+  }
+}
diff --git a/gradle/convention/src/main/kotlin/justjanne.kotlin.gradle.kts b/gradle/convention/src/main/kotlin/justjanne.kotlin.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..1a48de656290ce6a85bfd86a23876bd2cabcef99
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/justjanne.kotlin.gradle.kts
@@ -0,0 +1,38 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+plugins {
+  id("justjanne.java")
+  id("justjanne.dokka")
+  id("justjanne.ktlint")
+  id("com.google.devtools.ksp")
+  kotlin("jvm")
+  kotlin("kapt")
+}
+
+repositories {
+  mavenCentral()
+  google()
+}
+
+dependencies {
+  implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.10")
+
+  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
+  testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0")
+
+  testImplementation("org.junit.jupiter", "junit-jupiter-api", "5.8.2")
+  testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.8.2")
+  testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine")
+
+  testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.6.10")
+}
+
+tasks.withType<KotlinCompile> {
+  kotlinOptions {
+    freeCompilerArgs = listOf(
+      "-Xinline-classes",
+      "-Xopt-in=kotlin.ExperimentalUnsignedTypes"
+    )
+    jvmTarget = "1.8"
+  }
+}
diff --git a/libquassel-annotations/src/main/kotlin/de/justjanne/libquassel/annotations/SyncedData.kt b/gradle/convention/src/main/kotlin/justjanne.ktlint.gradle.kts
similarity index 55%
rename from libquassel-annotations/src/main/kotlin/de/justjanne/libquassel/annotations/SyncedData.kt
rename to gradle/convention/src/main/kotlin/justjanne.ktlint.gradle.kts
index da3061c0a1578f10bd1e33ee6ee101d4b3c028d1..16f0b6fdb89d1548f045ccff70e53ee5d42ae4ce 100644
--- a/libquassel-annotations/src/main/kotlin/de/justjanne/libquassel/annotations/SyncedData.kt
+++ b/gradle/convention/src/main/kotlin/justjanne.ktlint.gradle.kts
@@ -1,15 +1,12 @@
 /*
  * libquassel
- * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2022 Janne Mareike Koschinski
  *
  * This Source Code Form is subject to the terms of the Mozilla Public License,
  * v. 2.0. If a copy of the MPL was not distributed with this file, You can
  * obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-package de.justjanne.libquassel.annotations
-
-@Retention(AnnotationRetention.SOURCE)
-annotation class SyncedData(
-  val name: String
-)
+plugins {
+  id("org.jlleitschuh.gradle.ktlint")
+}
diff --git a/gradle/convention/src/main/kotlin/justjanne.publication.gradle.kts b/gradle/convention/src/main/kotlin/justjanne.publication.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..b69b760a0aaaf7f6032d62a4193edc984d2be72a
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/justjanne.publication.gradle.kts
@@ -0,0 +1,100 @@
+/*
+ * libquassel
+ * Copyright (c) 2022 Janne Mareike Koschinski
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public License,
+ * v. 2.0. If a copy of the MPL was not distributed with this file, You can
+ * obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+plugins {
+  id("maven-publish")
+  id("signing")
+}
+
+version = rootProject.version
+group = rootProject.group
+
+val canSign = project.properties.keys
+  .any { it.startsWith("signing.") }
+
+publishing {
+  publications {
+    create<MavenPublication>("maven") {
+      publication()
+      pom()
+    }
+  }
+}
+
+configure<SigningExtension> {
+  if (canSign) {
+    useGpgCmd()
+    sign(publishing.publications["maven"])
+  }
+}
+
+fun MavenPublication.pom() {
+  pom {
+    name.set(buildHumanReadableName(artifactId))
+    description.set("Pure-Kotlin implementation of the Quassel Protocol")
+    url.set("https://git.kuschku.de/justJanne/libquassel")
+    licenses {
+      license {
+        name.set("Mozilla Public License Version 2.0")
+        url.set("https://www.mozilla.org/en-US/MPL/2.0/")
+      }
+    }
+    developers {
+      developer {
+        id.set("justJanne")
+        name.set("Janne Mareike Koschinski")
+      }
+    }
+    scm {
+      connection.set("scm:git:https://git.kuschku.de/justJanne/libquassel.git")
+      developerConnection.set("scm:git:ssh://git.kuschku.de:2222/justJanne/libquassel.git")
+      url.set("https://git.kuschku.de/justJanne/libquassel")
+    }
+  }
+}
+
+fun MavenPublication.publication() {
+  val projectName = project.name
+    .removePrefix("core")
+    .removePrefix("-")
+  artifactId = buildArtifactName(
+    extractArtifactGroup(project.group as String),
+    rootProject.name,
+    projectName.ifEmpty { null }
+  )
+  from(components["java"])
+}
+
+fun buildArtifactName(group: String? = null, project: String? = null, module: String? = null): String {
+  return removeConsecutive(listOfNotNull(group, project, module).flatMap { it.split('-') })
+    .joinToString("-")
+}
+
+fun buildHumanReadableName(name: String) = name
+  .splitToSequence('-')
+  .joinToString(" ", transform = String::capitalize)
+
+fun extractArtifactGroup(group: String): String? {
+  // split into parts by domain separator
+  val elements = group.split('.')
+  // drop the tld/domain part, e.g. io.datalbry
+  val withoutDomain = elements.drop(2)
+  // if anything remains, that’s our artifact group
+  return withoutDomain.lastOrNull()
+}
+
+fun <T> removeConsecutive(list: List<T>): List<T> {
+  val result = mutableListOf<T>()
+  for (el in list) {
+    if (el != result.lastOrNull()) {
+      result.add(el)
+    }
+  }
+  return result
+}
diff --git a/gradle/convention/src/main/kotlin/justjanne.publish-maven-central.gradle.kts b/gradle/convention/src/main/kotlin/justjanne.publish-maven-central.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..8b62fa428038543a0fde5d56355bdaa612d53f63
--- /dev/null
+++ b/gradle/convention/src/main/kotlin/justjanne.publish-maven-central.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+    id("io.github.gradle-nexus.publish-plugin")
+}
+
+val canSign = project.properties.keys
+    .any { it.startsWith("signing.") }
+
+if (canSign) {
+    nexusPublishing {
+        repositories {
+          sonatype()
+        }
+    }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000000000000000000000000000000000000..c1220c121110fd0558340e282e8e022784928c2e
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,22 @@
+[versions]
+bouncycastle = "1.70"
+hamcrest = "2.2"
+junit = "5.7.2"
+kotlin-stdlib = "1.6.10"
+kotlin-bitflags = "1.3.0"
+kotlin-coroutines = "1.5.0"
+slf4j = "1.7.30"
+testcontainers = "1.3.0"
+threetenbp = "1.5.2"
+kotlinpoet = "1.10.2"
+ksp = "1.6.10-1.0.4"
+
+[libraries]
+testcontainers = { module = "de.justjanne:testcontainers-ci", version.ref = "testcontainers" }
+slf4j = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
+hamcrest = { module = "org.hamcrest:hamcrest-library", version.ref = "hamcrest" }
+ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
+kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" }
+threetenbp = { module = "org.threeten:threetenbp", version.ref = "threetenbp" }
+kotlin-bitflags = { module = "de.justjanne:kotlin-bitflags", version.ref = "kotlin-bitflags" }
+bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 0ecc037b964692dd5818060af8b2a7365c742260..aa520627104195ad680a007476109668c21ab7e8 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
-distributionSha256Szm=0e46229820205440b48a5501122002842b82886e76af35f0f3a069243dca4b3c
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionSha256Szm=8cc27038d5dbd815759851ba53e70cf62e481b87494cc97cfd97982ada5ba634
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
diff --git a/libquassel-annotations/build.gradle.kts b/libquassel-annotations/build.gradle.kts
index a71022e497151d038fb2e4c3442c5ece57ec4461..a268c30643e075880a270256f1ed2f8e8b9b93e0 100644
--- a/libquassel-annotations/build.gradle.kts
+++ b/libquassel-annotations/build.gradle.kts
@@ -8,5 +8,6 @@
  */
 
 plugins {
-  id("com.vanniktech.maven.publish")
+  id("justjanne.kotlin")
+  id("justjanne.publication")
 }
diff --git a/libquassel-annotations/gradle.properties b/libquassel-annotations/gradle.properties
deleted file mode 100644
index d290eb77e270898828797cf9887fbb4ae4593949..0000000000000000000000000000000000000000
--- a/libquassel-annotations/gradle.properties
+++ /dev/null
@@ -1,12 +0,0 @@
-#
-# libquassel
-# Copyright (c) 2021 Janne Mareike Koschinski
-#
-# This Source Code Form is subject to the terms of the Mozilla Public License,
-# v. 2.0. If a copy of the MPL was not distributed with this file, You can
-# obtain one at https://mozilla.org/MPL/2.0/.
-#
-
-POM_ARTIFACT_ID=libquassel-annotations
-POM_NAME=libquassel Annotations
-POM_DESCRIPTION=Annotations for the libquassel code generator
diff --git a/libquassel-client/build.gradle.kts b/libquassel-client/build.gradle.kts
index 874e61a1fd44941e0974f1cace4635020a17205a..b2648fa68d3b30f62809d5bd560ea14f1e9cd776 100644
--- a/libquassel-client/build.gradle.kts
+++ b/libquassel-client/build.gradle.kts
@@ -8,14 +8,16 @@
  */
 
 plugins {
-  id("com.vanniktech.maven.publish")
+  id("justjanne.kotlin")
+  id("justjanne.publication")
+  id("jacoco-report-aggregation")
 }
 
 dependencies {
   api(project(":libquassel-protocol"))
-
-  val testcontainersCiVersion: String by project
-  testImplementation("de.justjanne", "testcontainers-ci", testcontainersCiVersion)
-  val sl4jVersion: String by project
-  implementation("org.slf4j", "slf4j-simple", sl4jVersion)
+  testImplementation(libs.testcontainers)
+  implementation(libs.slf4j)
+}
+tasks.check {
+  dependsOn(tasks.named<JacocoReport>("testCodeCoverageReport"))
 }
diff --git a/libquassel-client/gradle.properties b/libquassel-client/gradle.properties
deleted file mode 100644
index a0f4930ee63cae7523e75abe7e5874ba46908ccd..0000000000000000000000000000000000000000
--- a/libquassel-client/gradle.properties
+++ /dev/null
@@ -1,12 +0,0 @@
-#
-# libquassel
-# Copyright (c) 2021 Janne Mareike Koschinski
-#
-# This Source Code Form is subject to the terms of the Mozilla Public License,
-# v. 2.0. If a copy of the MPL was not distributed with this file, You can
-# obtain one at https://mozilla.org/MPL/2.0/.
-#
-
-POM_ARTIFACT_ID=libquassel-client
-POM_NAME=libquassel Client
-POM_DESCRIPTION=High-Level Client interface for Quassel
diff --git a/libquassel-generator/build.gradle.kts b/libquassel-generator/build.gradle.kts
index 59ffef3d023bbf29fbd86ed71f59913d8d49d65d..36912da9bb77f9a3439bf69a3521af67693f667d 100644
--- a/libquassel-generator/build.gradle.kts
+++ b/libquassel-generator/build.gradle.kts
@@ -7,14 +7,13 @@
  * obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-repositories {
-  google()
+plugins {
+  id("justjanne.kotlin")
+  id("justjanne.publication")
 }
 
 dependencies {
-  val kspVersion: String by project
-  implementation("com.google.devtools.ksp", "symbol-processing-api", kspVersion)
   implementation(project(":libquassel-annotations"))
-  val kotlinPoetVersion: String by project
-  implementation("com.squareup", "kotlinpoet", kotlinPoetVersion)
+  implementation(libs.ksp)
+  implementation(libs.kotlinpoet)
 }
diff --git a/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/annotation/RpcFunctionAnnotation.kt b/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/annotation/RpcFunctionAnnotation.kt
index bc7e9d6d78796170cce3bba71a43503a468f15a1..e3a53322bacfda9c2398ef13444d60b97874178b 100644
--- a/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/annotation/RpcFunctionAnnotation.kt
+++ b/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/annotation/RpcFunctionAnnotation.kt
@@ -27,7 +27,7 @@ data class RpcFunctionAnnotation(
       val annotation = it.findAnnotationWithType<SyncedCall>(resolver)
         ?: return null
       return RpcFunctionAnnotation(
-        name = annotation.getMember("name"),
+        name = annotation.getMember<String>("name")?.ifBlank { null },
         target = annotation.getMember<KSType>("target")
           ?.toEnum<ProtocolSide>(),
       )
diff --git a/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/visitors/KotlinSaver.kt b/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/visitors/KotlinSaver.kt
index 6566541fab41e4a1fe24e8d9768a177dbf7c7c1c..9eac312e1ffcbb310ec8a7777f6d50a49cc2cac4 100644
--- a/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/visitors/KotlinSaver.kt
+++ b/libquassel-generator/src/main/kotlin/de/justjanne/libquassel/generator/visitors/KotlinSaver.kt
@@ -27,11 +27,16 @@ class KotlinSaver : KotlinModelVisitor<CodeGenerator, Unit> {
       "Source may not be empty. Sources was empty for $model"
     }
 
-    val writer = data.createNewFile(
-      generateDependencies(model.source),
-      model.data.packageName,
-      model.data.name
-    ).bufferedWriter(Charsets.UTF_8)
+    val file = try {
+      data.createNewFile(
+        generateDependencies(model.source),
+        model.data.packageName,
+        model.data.name
+      )
+    } catch (_: FileAlreadyExistsException) {
+      return
+    }
+    val writer = file.bufferedWriter(Charsets.UTF_8)
     model.data.writeTo(writer)
     try {
       writer.close()
diff --git a/libquassel-protocol/build.gradle.kts b/libquassel-protocol/build.gradle.kts
index 388178253d43ad0e3a3fdf96f1eb65d381380b58..4ecbe5588386618dd8e603ae2138c956aad269f1 100644
--- a/libquassel-protocol/build.gradle.kts
+++ b/libquassel-protocol/build.gradle.kts
@@ -8,20 +8,16 @@
  */
 
 plugins {
-  id("java-library")
-  id("com.vanniktech.maven.publish")
-  id("com.google.devtools.ksp") version "1.5.10-1.0.0-beta01"
+  id("justjanne.kotlin")
+  id("justjanne.publication")
 }
 
 dependencies {
-  val threetenBpVersion: String by project
-  api("org.threeten", "threetenbp", threetenBpVersion)
-  val kotlinBitflagsVersion: String by project
-  api("de.justjanne", "kotlin-bitflags", kotlinBitflagsVersion)
-  val bouncyCastleVersion: String by project
-  implementation("org.bouncycastle", "bcpkix-jdk15on", bouncyCastleVersion)
-  val sl4jVersion: String by project
-  implementation("org.slf4j", "slf4j-simple", sl4jVersion)
   api(project(":libquassel-annotations"))
   ksp(project(":libquassel-generator"))
+  api(libs.threetenbp)
+  api(libs.kotlin.bitflags)
+  implementation(libs.bouncycastle)
+  implementation(libs.slf4j)
+  testImplementation(libs.hamcrest)
 }
diff --git a/libquassel-protocol/gradle.properties b/libquassel-protocol/gradle.properties
deleted file mode 100644
index 72301238b2bb1cf73790ba7ebd50e6743280d571..0000000000000000000000000000000000000000
--- a/libquassel-protocol/gradle.properties
+++ /dev/null
@@ -1,12 +0,0 @@
-#
-# libquassel
-# Copyright (c) 2021 Janne Mareike Koschinski
-#
-# This Source Code Form is subject to the terms of the Mozilla Public License,
-# v. 2.0. If a copy of the MPL was not distributed with this file, You can
-# obtain one at https://mozilla.org/MPL/2.0/.
-#
-
-POM_ARTIFACT_ID=libquassel-protocol
-POM_NAME=libquassel Protocol
-POM_DESCRIPTION=Library implementing Quassel's transport-level protocol in Kotlin
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 6ee2a05b14eee05991d800f51bdad00cc6ac5d1b..45551feff648941b1cc35b9c1383976e7f98ac65 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -6,9 +6,12 @@
  * v. 2.0. If a copy of the MPL was not distributed with this file, You can
  * obtain one at https://mozilla.org/MPL/2.0/.
  */
+enableFeaturePreview("VERSION_CATALOGS")
 
 rootProject.name = "libquassel"
 
+includeBuild("gradle/convention")
+
 include(
   ":libquassel-annotations",
   ":libquassel-protocol",