diff --git a/.gitignore b/.gitignore index fea1822..5086076 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build .idea/ +config.json .env *.tar *.tar.gz diff --git a/app/src/main/kotlin/de/rpr/githubreleases/App.kt b/app/src/main/kotlin/de/rpr/githubreleases/App.kt index 3fa1abd..9f3d205 100644 --- a/app/src/main/kotlin/de/rpr/githubreleases/App.kt +++ b/app/src/main/kotlin/de/rpr/githubreleases/App.kt @@ -40,12 +40,12 @@ class App( } private fun execute() { - config.githubRepos.forEach { githubRepo -> - log("Processing releases feed for ${githubRepo.name}...") - val existingReleases = releaseRepo.getExistingReleases(githubRepo) - val feedService = FeedService(githubRepo, httpClient) + config.accounts.forEach { account -> + log("Processing releases feed for ${account.repo.name}...") + val existingReleases = releaseRepo.getExistingReleases(account.repo) + val feedService = FeedService(account.repo, httpClient) val newReleases = feedService.getNewReleases(existingReleases) - val publisher = publishers.forName(githubRepo.name) + val publisher = publishers.forName(account.name) val publishedReleases = publisher.sendReleases(newReleases) releaseRepo.save(publishedReleases) log("Finished feed processing...") diff --git a/app/src/main/kotlin/de/rpr/githubreleases/Config.kt b/app/src/main/kotlin/de/rpr/githubreleases/Config.kt index 881ca81..6824ad2 100644 --- a/app/src/main/kotlin/de/rpr/githubreleases/Config.kt +++ b/app/src/main/kotlin/de/rpr/githubreleases/Config.kt @@ -1,82 +1,63 @@ -package de.rpr.githubreleases; +package de.rpr.githubreleases import com.google.gson.Gson import com.google.gson.GsonBuilder import de.rpr.githubreleases.LogLevel.ERROR import de.rpr.githubreleases.model.GithubRepo -import de.rpr.terminenbg.LocalDateAdapter -import de.rpr.terminenbg.LocalDateTimeAdapter -import java.time.LocalDate -import java.time.LocalDateTime +import java.io.InputStream +import java.nio.file.Files -class Config { +class Config(configInputStream: InputStream) { + constructor(configFile: String = "config.json") : this(Files.newInputStream(configFile.toPath())) + + data class Account( + private val accountName: String?, + private val github: String?, + private val mastodonInstance: String?, + private val mastodonAccessToken: String?, + ) { + val repo get() = GithubRepo(github!!) + val name get() = accountName!! + val publishingInstance get() = mastodonInstance!! + val publishingAccessToken get() = mastodonAccessToken!! + + fun validate(): Boolean { + var valid = true + if (accountName.isNullOrEmpty()) { + ERROR.log("Account should have a name defined.") + valid = false + } + if (github.isNullOrEmpty()) { + ERROR.log("Account should have a github repository defined.") + valid = false + } + if (mastodonInstance.isNullOrEmpty()) { + ERROR.log("Account should have a mastodon instance defined.") + valid = false + } + if (mastodonAccessToken.isNullOrEmpty()) { + ERROR.log("Account should have a mastodon access token defined.") + valid = false + } + return valid + } + } + + class ConfigFile(val accounts: List) { + fun validate() = accounts.none { !it.validate() } + } @Transient private val gson: Gson = GsonBuilder().setPrettyPrinting().create() - data class PublishingCredentials( - val instanceUrl: String?, - @Transient val accessToken: String?, - @Transient private val instanceUrlEnvName: String, - @Transient private val accessTokenEnvName: String - ) { - // Serialization helper - @Suppress("unused") - val token: String? = accessToken?.let { "${it.take(10)}..." } - - @Transient - var valid: Boolean = true - - - init { - validate() - } - - private fun validate() { - if (instanceUrl.isNullOrBlank()) { - ERROR.log("No instance url available, please set the environment variable $instanceUrlEnvName") - valid = false - } - if (accessToken.isNullOrBlank()) { - ERROR.log("No access token available, please set the environment variable $accessTokenEnvName") - valid = false - } - } - } - - val githubRepos: List - val mastodonCredentials: Map + val accounts: List val schedulingDelay: Long = (System.getenv("SCHEDULING_DELAY")?.toLong() ?: 120) * 60 init { + val configFile = gson.fromJson(configInputStream.reader(), ConfigFile::class.java) + var valid = configFile.validate() - var valid = true - - val githubReposString = System.getenv("GITHUB_REPOS") - if (githubReposString == null) { - ERROR.log("No github repos defined, please set repo urls in environment variable GITHUB_REPOS") - throw IllegalStateException("Invalid configuration") - - } - - githubRepos = githubReposString.split(",").map { - GithubRepo(it) - } - - mastodonCredentials = githubRepos.associate { - val instanceUrlEnvName = "${it.name.uppercase()}_MASTODON_INSTANCE_URL" - val accessTokenEnvName = "${it.name.uppercase()}_MASTODON_ACCESS_TOKEN" - it.name to PublishingCredentials( - instanceUrlEnvName = instanceUrlEnvName, - instanceUrl = System.getenv(instanceUrlEnvName), - accessTokenEnvName = accessTokenEnvName, - accessToken = System.getenv(accessTokenEnvName) - ) - }.onEach { (_, credentials) -> - if (!credentials.valid) { - valid = false - } - } + accounts = configFile.accounts if (schedulingDelay < 300) { ERROR.log("To avoid hammering the source webpage, scheduling delay has to be > 5 minutes") @@ -91,6 +72,4 @@ class Config { override fun toString(): String { return "Config: " + gson.toJson(this) } - - -} \ No newline at end of file +} diff --git a/app/src/main/kotlin/de/rpr/githubreleases/publishing/Publisher.kt b/app/src/main/kotlin/de/rpr/githubreleases/publishing/Publisher.kt index eb31d8c..6611128 100644 --- a/app/src/main/kotlin/de/rpr/githubreleases/publishing/Publisher.kt +++ b/app/src/main/kotlin/de/rpr/githubreleases/publishing/Publisher.kt @@ -2,8 +2,8 @@ package de.rpr.githubreleases.publishing import de.rpr.githubreleases.Config import de.rpr.githubreleases.LogLevel -import de.rpr.githubreleases.model.Releases import de.rpr.githubreleases.log +import de.rpr.githubreleases.model.Releases import social.bigbone.MastodonClient import social.bigbone.api.entity.data.Visibility import social.bigbone.api.exception.BigBoneRequestException @@ -11,55 +11,46 @@ import social.bigbone.api.exception.BigBoneRequestException class Publisher( val name: String, private val client: MastodonClient, - private val dryRun: Boolean = System.getenv("PUBLISH_DRY_RUN").toBoolean() + private val dryRun: Boolean = System.getenv("PUBLISH_DRY_RUN").toBoolean(), ) { - fun sendReleases(releases: Releases): Releases { log("${releases.size} new releases to publish") - val result = releases - .onEach { release -> log("Publishing release: ${release.title}") } - .mapNotNull { release -> - val request = client.statuses.postStatus( - status = release.text, - language = "en", - visibility = Visibility.PUBLIC - ) - try { - if (!dryRun) { - request.execute() - } else { - log("Dry-Run, skipping publishing of events...") + val result = + releases + .onEach { release -> log("Publishing release: ${release.title}") } + .mapNotNull { release -> + val request = + client.statuses.postStatus( + status = release.text, + language = "en", + visibility = Visibility.PUBLIC, + ) + try { + if (!dryRun) { + request.execute() + } else { + log("Dry-Run, skipping publishing of events...") + } + return@mapNotNull release + } catch (ex: BigBoneRequestException) { + log("ERROR: Event with id ${release.id} couldn't be published: " + ex.httpStatusCode) + LogLevel.ERROR.log("Cause: ${ex.message}") + LogLevel.ERROR.log("Root cause: ${ex.cause?.message}") + return@mapNotNull null } - return@mapNotNull release - } catch (ex: BigBoneRequestException) { - log("ERROR: Event with id ${release.id} couldn't be published: " + ex.httpStatusCode) - LogLevel.ERROR.log("Cause: ${ex.message}") - LogLevel.ERROR.log("Root cause: ${ex.cause?.message}") - return@mapNotNull null } - } return Releases(result) } } class Publishers( - private val config: Config, + config: Config, private val mastodonClientFactory: MastodonClientFactory, -) : - Iterable { - - private val instances: List - - init { - val mastodonClients = config.githubRepos.associate { - val publishingCredentials = config.mastodonCredentials[it.name]!! - it.name to mastodonClientFactory.getClient( - publishingCredentials.instanceUrl!!, - publishingCredentials.accessToken!! - ) +) : Iterable { + private val instances: List = + config.accounts.map { + Publisher(it.name, mastodonClientFactory.getClient(it.publishingInstance, it.publishingAccessToken)) } - instances = config.githubRepos.map { Publisher(it.name, mastodonClients[it.name]!!) } - } fun forName(name: String) = instances.first { it.name == name } @@ -67,4 +58,3 @@ class Publishers( return instances.iterator() } } - diff --git a/app/src/test/kotlin/de/rpr/githubreleases/ConfigTest.kt b/app/src/test/kotlin/de/rpr/githubreleases/ConfigTest.kt index 2346bb1..7ca6707 100644 --- a/app/src/test/kotlin/de/rpr/githubreleases/ConfigTest.kt +++ b/app/src/test/kotlin/de/rpr/githubreleases/ConfigTest.kt @@ -2,49 +2,29 @@ package de.rpr.githubreleases import assertk.assertThat import assertk.assertions.containsExactly -import assertk.assertions.containsOnly -import de.rpr.githubreleases.model.GithubRepo import io.kotest.core.spec.style.DescribeSpec -import io.kotest.extensions.system.withEnvironment import org.junit.jupiter.api.assertThrows class ConfigTest : DescribeSpec({ describe("instantiation") { - withEnvironment( - mapOf( - "GITHUB_REPOS" to "https://github.com/navidrome/navidrome/", - "NAVIDROME_MASTODON_INSTANCE_URL" to "example.com", - "NAVIDROME_MASTODON_ACCESS_TOKEN" to "token" + it("should populate accounts from config") { + assertThat(testConfig.accounts).containsExactly( + Config.Account( + accountName = "navidrome", + github = "navidrome/navidrome", + mastodonInstance = "mastodon.social", + mastodonAccessToken = "token", + ), ) - ) { - - it("should populate instances from config") { - val config = Config() - assertThat(config.githubRepos).containsExactly( - GithubRepo("https://github.com/navidrome/navidrome/") - ) - assertThat(config.mastodonCredentials).containsOnly( - "navidrome" to Config.PublishingCredentials( - instanceUrl = "example.com", - accessToken = "token", - instanceUrlEnvName = "NAVIDROME_MASTODON_INSTANCE_URL", - accessTokenEnvName = "NAVIDROME_MASTODON_ACCESS_TOKEN" - ) - ) - } - } - - withEnvironment( - mapOf("GITHUB_REPOS" to "https://github.com/navidrome/navidrome/") - ) { - - it("should throw error if env variables for repo are not present") { - assertThrows { - Config() - } - } } } -}) \ No newline at end of file + + it("should throw error if account is invalid for repo are not present") { + val configInputStream = Config::class.java.classLoader.getResourceAsStream("invalid-test-config.json")!! + assertThrows { + Config(configInputStream) + } + } +}) diff --git a/app/src/test/kotlin/de/rpr/githubreleases/TestData.kt b/app/src/test/kotlin/de/rpr/githubreleases/TestData.kt index 4c23910..198e63b 100644 --- a/app/src/test/kotlin/de/rpr/githubreleases/TestData.kt +++ b/app/src/test/kotlin/de/rpr/githubreleases/TestData.kt @@ -6,9 +6,13 @@ import java.time.LocalDateTime val testGithubRepo = GithubRepo("https://github.com/example/example") -val testRelease = Release( - id = "id", - link = "https://example.com", - created = LocalDateTime.of(2023, 11, 29, 12, 11), - githubRepo = testGithubRepo -) \ No newline at end of file +val testRelease = + Release( + id = "id", + link = "https://example.com", + created = LocalDateTime.of(2023, 11, 29, 12, 11), + githubRepo = testGithubRepo, + ) + +private val configInputStream = Config::class.java.classLoader.getResourceAsStream("test-config.json")!! +val testConfig = Config(configInputStream) diff --git a/app/src/test/kotlin/de/rpr/githubreleases/publishing/PublishersTest.kt b/app/src/test/kotlin/de/rpr/githubreleases/publishing/PublishersTest.kt index 9c99375..c5841e7 100644 --- a/app/src/test/kotlin/de/rpr/githubreleases/publishing/PublishersTest.kt +++ b/app/src/test/kotlin/de/rpr/githubreleases/publishing/PublishersTest.kt @@ -4,9 +4,9 @@ import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isTrue import assertk.assertions.prop -import de.rpr.githubreleases.Config import de.rpr.githubreleases.model.Releases import de.rpr.githubreleases.model.asCollection +import de.rpr.githubreleases.testConfig import de.rpr.githubreleases.testRelease import io.kotest.core.spec.style.DescribeSpec import io.kotest.extensions.system.withEnvironment @@ -37,12 +37,12 @@ class PublishersTest : DescribeSpec({ mapOf( "GITHUB_REPOS" to "https://github.com/navidrome/navidrome/", "NAVIDROME_MASTODON_INSTANCE_URL" to "example.com", - "NAVIDROME_MASTODON_ACCESS_TOKEN" to "token" - ) + "NAVIDROME_MASTODON_ACCESS_TOKEN" to "token", + ), ) { it("should populate instances from config") { - val publishers = Publishers(Config(), mastodonClientFactory) + val publishers = Publishers(testConfig, mastodonClientFactory) assertThat(publishers.iterator().hasNext()).isTrue() assertThat(publishers.forName("navidrome")) .prop("name") { it.name } @@ -62,7 +62,7 @@ class PublishersTest : DescribeSpec({ spoilerText = any(), status = any(), language = any(), - visibility = any() + visibility = any(), ) } returns mockRequest @@ -77,7 +77,7 @@ class PublishersTest : DescribeSpec({ sensitive = any(), spoilerText = any(), language = "en", - addIdempotencyKey = any() + addIdempotencyKey = any(), ) } } @@ -88,7 +88,7 @@ class PublishersTest : DescribeSpec({ spoilerText = any(), status = any(), language = any(), - visibility = any() + visibility = any(), ) } returns mockRequest @@ -105,11 +105,15 @@ class PublishersTest : DescribeSpec({ sensitive = any(), spoilerText = any(), language = any(), - addIdempotencyKey = any() + addIdempotencyKey = any(), ) } - assertThat(statusSlot.captured).isEqualTo("\uD83C\uDF89 Example v0.0.0 has been published!\n\nRelease notes are available here: https://example.com\n\n#Example") + assertThat(statusSlot.captured) + .isEqualTo( + "\uD83C\uDF89 Example v0.0.0 has been published!" + + "\n\nRelease notes are available here: https://example.com\n\n#Example", + ) } } @@ -124,7 +128,7 @@ class PublishersTest : DescribeSpec({ spoilerText = any(), status = any(), language = any(), - visibility = any() + visibility = any(), ) } returns mockRequest @@ -133,4 +137,4 @@ class PublishersTest : DescribeSpec({ verify(exactly = 0) { mockRequest.execute() } } } -}) \ No newline at end of file +}) diff --git a/app/src/test/kotlin/de/rpr/githubreleases/repository/ReleaseRepositoryTest.kt b/app/src/test/kotlin/de/rpr/githubreleases/repository/ReleaseRepositoryTest.kt index fa6ecce..bb86e95 100644 --- a/app/src/test/kotlin/de/rpr/githubreleases/repository/ReleaseRepositoryTest.kt +++ b/app/src/test/kotlin/de/rpr/githubreleases/repository/ReleaseRepositoryTest.kt @@ -20,7 +20,9 @@ import kotlin.io.path.listDirectoryEntries class ReleaseRepositoryTest : DescribeSpec({ - val sampleReleaseContent = """{ + val sampleReleaseContent = + """ + { "id": "v0.51.0", "link": "https://github.com/navidrome/navidrome/releases/tag/v0.51.0", "created": "2024-01-22T23:59:21Z", @@ -29,7 +31,8 @@ class ReleaseRepositoryTest : DescribeSpec({ "url": "https://github.com/navidrome/navidrome/" }, "text": "🎉 Navidrome v0.51.0 has been published!\n\nRelease notes are available here: https://github.com/navidrome/navidrome/releases/tag/v0.51.0\n\n#Navidrome" - }""".trimIndent() + } + """.trimIndent() val basePath = System.getProperty("java.io.tmpdir") + "/releases" @@ -38,18 +41,12 @@ class ReleaseRepositoryTest : DescribeSpec({ } afterSpec { - basePath.toPath().listDirectoryEntries() - .forEach { - it.deleteExisting() - } + basePath.toPath().listDirectoryEntries().forEach { it.deleteExisting() } Files.deleteIfExists(basePath.toPath()) } afterTest { - basePath.toPath().listDirectoryEntries() - .forEach { - it.deleteExisting() - } + basePath.toPath().listDirectoryEntries().forEach { it.deleteExisting() } } describe("ReleaseRepository is created") { @@ -70,9 +67,9 @@ class ReleaseRepositoryTest : DescribeSpec({ id = "id", link = "https://example.com", created = LocalDateTime.now(), - githubRepo = testGithubRepo - ) - ) + githubRepo = testGithubRepo, + ), + ), ) assertThat(basePath.toPath().listDirectoryEntries().map { it.fileName.toString() }) .contains("${LocalDate.now()}-id.example.json") @@ -86,9 +83,7 @@ class ReleaseRepositoryTest : DescribeSpec({ val postRepository = ReleaseRepository(basePath) val existingEvents = postRepository.getExistingReleases(testGithubRepo) - assertThat(existingEvents).contains(testRelease.copy(id="v0.51.0")) + assertThat(existingEvents).contains(testRelease.copy(id = "v0.51.0")) } } - - -}) \ No newline at end of file +}) diff --git a/app/src/test/resources/invalid-test-config.json b/app/src/test/resources/invalid-test-config.json new file mode 100644 index 0000000..6796f14 --- /dev/null +++ b/app/src/test/resources/invalid-test-config.json @@ -0,0 +1,9 @@ +{ + "accounts": [ + { + "accountName": "navidrome", + "github": "navidrome/navidrome", + "mastodonAccessToken": "token" + } + ] +} \ No newline at end of file diff --git a/app/src/test/resources/test-config.json b/app/src/test/resources/test-config.json new file mode 100644 index 0000000..470b9ef --- /dev/null +++ b/app/src/test/resources/test-config.json @@ -0,0 +1,10 @@ +{ + "accounts": [ + { + "accountName": "navidrome", + "github": "navidrome/navidrome", + "mastodonInstance": "mastodon.social", + "mastodonAccessToken": "token" + } + ] +} \ No newline at end of file