Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import org.gradle.kotlin.dsl.get
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.newInstance
import org.gradle.kotlin.dsl.register
import xyz.jpenilla.runtask.pluginsapi.discord.DiscordApi
import xyz.jpenilla.runtask.pluginsapi.discord.DiscordApiImpl
import xyz.jpenilla.runtask.pluginsapi.github.GitHubApi
import xyz.jpenilla.runtask.pluginsapi.github.GitHubApiImpl
import xyz.jpenilla.runtask.pluginsapi.hangar.HangarApi
Expand Down Expand Up @@ -67,6 +69,7 @@ public abstract class DownloadPluginsSpec @Inject constructor(
registry.registerFactory(ModrinthApi::class) { name -> objects.newInstance(ModrinthApiImpl::class, name) }
registry.registerFactory(GitHubApi::class) { name -> objects.newInstance(GitHubApiImpl::class, name) }
registry.registerFactory(UrlPluginProvider::class) { name -> objects.newInstance(UrlPluginProviderImpl::class, name) }
registry.registerFactory(DiscordApi::class) { name -> objects.newInstance(DiscordApiImpl::class, name) }

register("hangar", HangarApi::class) {
url.set(HangarApi.DEFAULT_URL)
Expand All @@ -76,6 +79,7 @@ public abstract class DownloadPluginsSpec @Inject constructor(
}
register("github", GitHubApi::class)
register("url", UrlPluginProvider::class)
register("discord", DiscordApi::class)
}

/**
Expand All @@ -96,6 +100,7 @@ public abstract class DownloadPluginsSpec @Inject constructor(
is ModrinthApi -> ModrinthApi::class
is GitHubApi -> GitHubApi::class
is UrlPluginProvider -> UrlPluginProvider::class
is DiscordApi -> DiscordApi::class
else -> throw IllegalStateException("Unknown PluginApi type: ${api.javaClass.name}")
}
configure(name, type) {
Expand Down Expand Up @@ -163,6 +168,26 @@ public abstract class DownloadPluginsSpec @Inject constructor(
github.configure { add(owner, repo, tag, assetName) }
}

// discord extensions

/**
* Access to the built-in [DiscordApi].
*/
@get:Internal
public val discord: NamedDomainObjectProvider<DiscordApi>
get() = named("discord", DiscordApi::class)

/**
* Add a plugin download from a Discord message link.
*
* @param channelId the Discord channel ID where the message is located
* @param messageId the Discord message ID containing the plugin download link
* @param token the Discord bot token to use for fetching the message
*/
public fun discord(channelId: String, messageId: String, token: String) {
discord.configure { add(channelId, messageId, token) }
}

// url extensions

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.gradle.api.tasks.Input
import xyz.jpenilla.runtask.util.HashingAlgorithm
import xyz.jpenilla.runtask.util.calculateHash
import xyz.jpenilla.runtask.util.toHexString
import kotlin.io.byteInputStream

public sealed class PluginApiDownload

Expand Down Expand Up @@ -173,3 +174,41 @@ public abstract class UrlDownload : PluginApiDownload() {
return toHexString(url.get().byteInputStream().calculateHash(HashingAlgorithm.SHA1))
}
}

public abstract class DiscordApiDownload : PluginApiDownload() {
@get:Input
public abstract val channelId: Property<String>

@get:Input
public abstract val messageId: Property<String>

@get:Input
public abstract val token: Property<String>

override fun toString(): String {
return "DiscordApiDownload{channelId=${channelId.get()}, messageId=${messageId.get()}, token=${"*".repeat(token.get().length)}}"
}

override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}

other as DiscordApiDownload

return channelId.get() == other.channelId.get() &&
messageId.get() == other.messageId.get() &&
token.get() == other.token.get()
}

override fun hashCode(): Int {
var result = channelId.get().hashCode()
result = 31 * result + messageId.get().hashCode()
result = 31 * result + token.get().hashCode()
return result
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package xyz.jpenilla.runtask.pluginsapi
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
import org.gradle.api.Project
Expand Down Expand Up @@ -93,6 +94,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
is ModrinthApiDownload -> resolveModrinthPlugin(project, download)
is GitHubApiDownload -> resolveGitHubPlugin(project, download)
is UrlDownload -> resolveUrl(project, download)
is DiscordApiDownload -> resolveDiscordPlugin(project, download)
}
}

Expand Down Expand Up @@ -196,6 +198,83 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
return download(ctx)
}

private fun resolveDiscordPlugin(project: Project, download: DiscordApiDownload): Path {
val cacheDir = parameters.cacheDirectory.get().asFile.toPath()
val targetDir = cacheDir.resolve(Constants.DISCORD_PLUGIN_DIR)

val discordAuthorization = "Bot ${download.token.get()}"
val messageUrl = URI.create("https://discord.com/api/v10/channels/${download.channelId.get()}/messages/${download.messageId.get()}").toURL()
val connection = messageUrl.openConnection() as HttpURLConnection

try {
connection.instanceFollowRedirects = true
connection.setRequestProperty("Accept", "application/json")
connection.setRequestProperty("User-Agent", Constants.USER_AGENT)
connection.setRequestProperty("Authorization", discordAuthorization)
connection.connect()
val status = connection.responseCode

if (status in 200..299) {
val responseBody = connection.inputStream.bufferedReader().readText()
val mapper = jacksonObjectMapper()
val response: DiscordMessage = mapper.readValue(responseBody)
val attachments = response.attachments

if (attachments.isEmpty()) {
throw IllegalStateException("No attachments found in Discord message ${download.messageId.get()} in channel ${download.channelId.get()}")
}

val firstAttachedJar = attachments.find { it.filename.endsWith(".jar") }

if(firstAttachedJar == null) {
throw IllegalStateException("No jar file found in Discord message ${download.messageId.get()} in channel ${download.channelId.get()}")
}

val downloadUrl = firstAttachedJar.url
val urlHash = toHexString(downloadUrl.byteInputStream().calculateHash(HashingAlgorithm.SHA1))
val version = manifest.urlProvider[urlHash] ?: PluginVersion(fileName = "$urlHash.jar", displayName = downloadUrl)
val targetFile = targetDir.resolve(version.fileName)

val setter: (PluginVersion) -> Unit = { manifest.urlProvider[urlHash] = it }
val ctx = DownloadCtx(project, "discord.com", downloadUrl, targetDir, targetFile, version, setter, authorization = discordAuthorization)
return download(ctx)

} else {
throw IllegalStateException("Failed to fetch Discord message ${download.messageId.get()} in channel ${download.channelId.get()}, status code: $status")
}

} finally {
connection.disconnect()
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
private data class DiscordMessage(
val attachments: Array<DiscordAttachment>
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as DiscordMessage

if (!attachments.contentEquals(other.attachments)) return false

return true
}

override fun hashCode(): Int {
return attachments.contentHashCode()
}
}

@JsonIgnoreProperties(ignoreUnknown = true)
private data class DiscordAttachment(
val id: String,
val filename: String,
val url: String
)

private fun download(ctx: DownloadCtx): Path {
if (refreshDependencies) {
return downloadFile(ctx)
Expand Down Expand Up @@ -229,6 +308,10 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
connection.setRequestProperty("Accept", "application/octet-stream")
connection.setRequestProperty("User-Agent", Constants.USER_AGENT)

if (ctx.authorization != null) {
connection.setRequestProperty("Authorization", ctx.authorization)
}

if (ctx.targetFile.isRegularFile()) {
if (ctx.version.lastUpdateCheck > 0 && ctx.version.hash?.check(ctx.targetFile) != false) {
// File matches what we expected
Expand Down Expand Up @@ -280,6 +363,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
}
}


private fun requireValidJarFile(ctx: DownloadCtx, displayName: String) {
if (!ctx.requireValidJar) {
return
Expand Down Expand Up @@ -319,6 +403,7 @@ internal abstract class PluginDownloadServiceImpl : PluginDownloadService {
val version: PluginVersion,
val setter: (PluginVersion) -> Unit,
val requireValidJar: Boolean = true,
val authorization: String? = null
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package xyz.jpenilla.runtask.pluginsapi.discord

import xyz.jpenilla.runtask.pluginsapi.DiscordApiDownload
import xyz.jpenilla.runtask.pluginsapi.PluginApi

/**
* [PluginApi] implementation for Discord message links.
*/
public interface DiscordApi : PluginApi<DiscordApi, DiscordApiDownload> {
/**
* Add a Discord message plugin download.
*
* @param channelId the ID of the Discord channel containing the message
* @param messageId the ID of the Discord message to fetch
* @param token the bot token to use for fetching the message
*/
public fun add(
channelId: String,
messageId: String,
token: String
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package xyz.jpenilla.runtask.pluginsapi.discord

import org.gradle.api.model.ObjectFactory
import org.gradle.kotlin.dsl.newInstance
import xyz.jpenilla.runtask.pluginsapi.DiscordApiDownload
import javax.inject.Inject

public abstract class DiscordApiImpl @Inject constructor(private val name: String, private val objects: ObjectFactory) : DiscordApi {
private val jobs: MutableList<DiscordApiDownload> = mutableListOf()

override fun getName(): String = name

override fun add(channelId: String, messageId: String, token: String) {
val job = objects.newInstance(DiscordApiDownload::class)
job.channelId.set(channelId)
job.messageId.set(messageId)
job.token.set(token)
jobs += job
}

override fun copyConfiguration(api: DiscordApi) {
jobs.addAll(api.downloads)
}

override val downloads: Iterable<DiscordApiDownload>
get() = jobs
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal object Constants {
const val MODRINTH_PLUGIN_DIR = "modrinth"
const val GITHUB_PLUGIN_DIR = "github"
const val URL_PLUGIN_DIR = "url"
const val DISCORD_PLUGIN_DIR = "discord"

const val USER_AGENT = "run-task-gradle-plugin (https://github.com/jpenilla/run-task)"

Expand Down
1 change: 1 addition & 0 deletions tester/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ val paperPlugins = runPaper.downloadPluginsSpec {
github("jpenilla", "MiniMOTD", "v2.1.6", "minimotd-bukkit-2.1.6.jar")
hangar("squaremap", "1.3.6")
url("https://download.luckperms.net/1593/bukkit/loader/LuckPerms-Bukkit-5.5.8.jar")
discord("1379024292548710400","1379024345845989440", project.property("bot_token") as String)
}

tasks {
Expand Down