From 097502b02b57488f746ef9b9943cf9296ea4add5 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:39:10 +0200 Subject: [PATCH 1/5] jda-ktx: Add `JDA#retrieveThreadChannelById(OrNull)` --- .../botcommands/jda/ktx/retrieve/Guild.kt | 8 +- .../retrieve/InvalidChannelTypeException.kt | 2 - .../botcommands/jda/ktx/retrieve/JDA.kt | 86 +++++++++++++++++++ 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt index bef7b6e31..25ac61b67 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt @@ -112,7 +112,7 @@ suspend fun Guild.retrieveVanityInviteOrNull(): VanityInvite? { * * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. * - * @see retrieveThreadChannelByIdOrNull + * @see Guild.retrieveThreadChannelByIdOrNull */ fun Guild.retrieveThreadChannelById(id: Long): CacheRestAction { return jda.deferredRestAction( @@ -137,7 +137,7 @@ fun Guild.retrieveThreadChannelById(id: Long): CacheRestAction { * * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. * - * @see retrieveThreadChannelByIdOrNull + * @see Guild.retrieveThreadChannelByIdOrNull */ fun Guild.retrieveThreadChannelById(id: String): CacheRestAction { return retrieveThreadChannelById(MiscUtil.parseSnowflake(id)) @@ -153,7 +153,7 @@ fun Guild.retrieveThreadChannelById(id: String): CacheRestAction * - The bot doesn't have access to it * - The channel isn't a thread * - * @see retrieveThreadChannelById + * @see Guild.retrieveThreadChannelById */ suspend fun Guild.retrieveThreadChannelByIdOrNull(id: Long): ThreadChannel? { return runIgnoringResponseOrNull(ErrorResponse.UNKNOWN_CHANNEL, ErrorResponse.MISSING_ACCESS) { @@ -175,7 +175,7 @@ suspend fun Guild.retrieveThreadChannelByIdOrNull(id: Long): ThreadChannel? { * - The bot doesn't have access to it * - The channel isn't a thread * - * @see retrieveThreadChannelById + * @see Guild.retrieveThreadChannelById */ suspend fun Guild.retrieveThreadChannelByIdOrNull(id: String): ThreadChannel? { return retrieveThreadChannelByIdOrNull(MiscUtil.parseSnowflake(id)) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt index e0390f2a9..aac919f87 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt @@ -2,7 +2,5 @@ package dev.freya02.botcommands.jda.ktx.retrieve /** * Exception thrown when retrieving a channel by ID, but the type is incorrect. - * - * @see retrieveThreadChannelById */ class InvalidChannelTypeException(message: String) : IllegalArgumentException(message) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt index 22dd707b3..2a79d4afc 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt @@ -2,14 +2,23 @@ package dev.freya02.botcommands.jda.ktx.retrieve import dev.freya02.botcommands.jda.ktx.IgnoreForMatch import dev.freya02.botcommands.jda.ktx.coroutines.await +import dev.freya02.botcommands.jda.ktx.deferredRestAction import dev.freya02.botcommands.jda.ktx.requests.runIgnoringResponseOrNull import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.entities.Entitlement import net.dv8tion.jda.api.entities.User import net.dv8tion.jda.api.entities.Webhook +import net.dv8tion.jda.api.entities.channel.ChannelType +import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel import net.dv8tion.jda.api.entities.sticker.StickerSnowflake import net.dv8tion.jda.api.entities.sticker.StickerUnion import net.dv8tion.jda.api.requests.ErrorResponse +import net.dv8tion.jda.api.requests.RestAction +import net.dv8tion.jda.api.requests.Route +import net.dv8tion.jda.api.requests.restaction.CacheRestAction +import net.dv8tion.jda.api.utils.MiscUtil +import net.dv8tion.jda.internal.JDAImpl +import net.dv8tion.jda.internal.requests.RestActionImpl /** * Retrieves a [User] with the provided ID. @@ -107,3 +116,80 @@ suspend fun JDA.retrieveWebhookByIdOrNull(webhookId: Long): Webhook? { retrieveWebhookById(webhookId).await() } } + +/** + * Retrieves a thread from any guild, by ID. + * + * The cached threads are checked first, and then a request is made. + * + * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. + * + * @see JDA.retrieveThreadChannelByIdOrNull + */ +fun JDA.retrieveThreadChannelById(id: Long): CacheRestAction { + return deferredRestAction( + valueSupplier = { getThreadChannelById(id) }, + actionSupplier = { + RestActionImpl(this, Route.Channels.GET_CHANNEL.compile(id.toString())) { res, _ -> + val json = res.`object` + val channelType = json.getInt("type").let(ChannelType::fromId) + if (!channelType.isThread) + throw InvalidChannelTypeException("Invalid channel type, expected a thread, got $channelType") + + val guildId = json.getUnsignedLong("guild_id") + (this as JDAImpl).entityBuilder.createThreadChannel(null, json, guildId, false) + } + } + ) +} + +/** + * Retrieves a thread from any guild, by ID. + * + * The cached threads are checked first, and then a request is made. + * + * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. + * + * @see JDA.retrieveThreadChannelByIdOrNull + */ +fun JDA.retrieveThreadChannelById(id: String): CacheRestAction { + return retrieveThreadChannelById(MiscUtil.parseSnowflake(id)) +} + +/** + * Retrieves a thread from any guild, by ID. + * + * The cached threads are checked first, and then a request is made. + * + * The returned thread may be `null` if: + * - It doesn't exist + * - The bot doesn't have access to it + * - The channel isn't a thread + * + * @see JDA.retrieveThreadChannelById + */ +suspend fun JDA.retrieveThreadChannelByIdOrNull(id: Long): ThreadChannel? { + return runIgnoringResponseOrNull(ErrorResponse.UNKNOWN_CHANNEL, ErrorResponse.MISSING_ACCESS) { + try { + retrieveThreadChannelById(id).await() + } catch (_: InvalidChannelTypeException) { + return null + } + } +} + +/** + * Retrieves a thread from any guild, by ID. + * + * The cached threads are checked first, and then a request is made. + * + * The returned thread may be `null` if: + * - It doesn't exist + * - The bot doesn't have access to it + * - The channel isn't a thread + * + * @see JDA.retrieveThreadChannelById + */ +suspend fun JDA.retrieveThreadChannelByIdOrNull(id: String): ThreadChannel? { + return retrieveThreadChannelByIdOrNull(MiscUtil.parseSnowflake(id)) +} From dad2c656a56ffa7318dbdf0c629522518104c325 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:40:31 +0200 Subject: [PATCH 2/5] jda-ktx: Make `Guild#retrieveThreadChannelById` throw `ParentGuildMismatchException` on guild mismatch --- .../botcommands/jda/ktx/retrieve/Guild.kt | 22 ++++++++++++++----- .../retrieve/ParentGuildMismatchException.kt | 6 +++++ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt index 25ac61b67..0791e6692 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt @@ -106,11 +106,12 @@ suspend fun Guild.retrieveVanityInviteOrNull(): VanityInvite? { } /** - * Retrieves a thread by ID. + * Retrieves a thread belonging to this guild, by ID. * * The cached threads are checked first, and then a request is made. * * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. + * It may also throw [ParentGuildMismatchException] if the channel isn't from the same guild. * * @see Guild.retrieveThreadChannelByIdOrNull */ @@ -124,6 +125,10 @@ fun Guild.retrieveThreadChannelById(id: Long): CacheRestAction { if (!channelType.isThread) throw InvalidChannelTypeException("Invalid channel type, expected a thread, got $channelType") + if (dataObject.getUnsignedLong("guild_id", 0) != this.idLong) { + throw ParentGuildMismatchException("Thread $id is not from the same guild (expected ${this.id})") + } + (jda as JDAImpl).entityBuilder.createThreadChannel(this as GuildImpl, dataObject, this.idLong, false) } } @@ -131,11 +136,12 @@ fun Guild.retrieveThreadChannelById(id: Long): CacheRestAction { } /** - * Retrieves a thread by ID. + * Retrieves a thread belonging to this guild, by ID. * * The cached threads are checked first, and then a request is made. * * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. + * It may also throw [ParentGuildMismatchException] if the channel isn't from the same guild. * * @see Guild.retrieveThreadChannelByIdOrNull */ @@ -144,14 +150,15 @@ fun Guild.retrieveThreadChannelById(id: String): CacheRestAction } /** - * Retrieves a thread by ID. + * Retrieves a thread belonging to this guild, by ID. * * The cached threads are checked first, and then a request is made. * - * The returned thread may be null if: + * The returned thread may be `null` if: * - It doesn't exist * - The bot doesn't have access to it * - The channel isn't a thread + * - The channel isn't from the correct guild * * @see Guild.retrieveThreadChannelById */ @@ -161,19 +168,22 @@ suspend fun Guild.retrieveThreadChannelByIdOrNull(id: Long): ThreadChannel? { retrieveThreadChannelById(id).await() } catch (_: InvalidChannelTypeException) { return null + } catch (_: ParentGuildMismatchException) { + return null } } } /** - * Retrieves a thread by ID. + * Retrieves a thread belonging to this guild, by ID. * * The cached threads are checked first, and then a request is made. * - * The returned thread may be null if: + * The returned thread may be `null` if: * - It doesn't exist * - The bot doesn't have access to it * - The channel isn't a thread + * - The channel isn't from the correct guild * * @see Guild.retrieveThreadChannelById */ diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt new file mode 100644 index 000000000..533ebad99 --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt @@ -0,0 +1,6 @@ +package dev.freya02.botcommands.jda.ktx.retrieve + +/** + * Exception thrown when retrieving a channel by ID from a guild, but their expected guild is incorrect. + */ +class ParentGuildMismatchException(message: String) : IllegalArgumentException(message) From cb2db50891a47879ac1b2fea9b421228c93d3ef6 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:33:36 +0200 Subject: [PATCH 3/5] Throw `GuildNotFoundException` if thread is not from the same shard or guild is not cached yet --- .../jda/ktx/retrieve/GuildNotFoundException.kt | 6 ++++++ .../freya02/botcommands/jda/ktx/retrieve/JDA.kt | 17 ++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/GuildNotFoundException.kt diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/GuildNotFoundException.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/GuildNotFoundException.kt new file mode 100644 index 000000000..2b81730e2 --- /dev/null +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/GuildNotFoundException.kt @@ -0,0 +1,6 @@ +package dev.freya02.botcommands.jda.ktx.retrieve + +/** + * Exception thrown when a guild was expected but not found, typically in sharded applications. + */ +class GuildNotFoundException internal constructor(message: String) : IllegalStateException(message) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt index 2a79d4afc..ffdeba526 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt @@ -18,6 +18,7 @@ import net.dv8tion.jda.api.requests.Route import net.dv8tion.jda.api.requests.restaction.CacheRestAction import net.dv8tion.jda.api.utils.MiscUtil import net.dv8tion.jda.internal.JDAImpl +import net.dv8tion.jda.internal.entities.GuildImpl import net.dv8tion.jda.internal.requests.RestActionImpl /** @@ -123,6 +124,7 @@ suspend fun JDA.retrieveWebhookByIdOrNull(webhookId: Long): Webhook? { * The cached threads are checked first, and then a request is made. * * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. + * If the thread is not owned by the current shard, [GuildNotFoundException] is thrown. * * @see JDA.retrieveThreadChannelByIdOrNull */ @@ -137,7 +139,17 @@ fun JDA.retrieveThreadChannelById(id: Long): CacheRestAction { throw InvalidChannelTypeException("Invalid channel type, expected a thread, got $channelType") val guildId = json.getUnsignedLong("guild_id") - (this as JDAImpl).entityBuilder.createThreadChannel(null, json, guildId, false) + val guild = getGuildById(guildId) ?: run { + val expectedShard = (guildId shr 22) % shardInfo.shardTotal + val actualShard = shardInfo.shardId + if (expectedShard.toInt() == actualShard) { + throw GuildNotFoundException("Thread $id was found but its guild was not found") + } else { + throw GuildNotFoundException("Thread $id was found but its guild was not found as it is from a different shard") + } + } + + (this as JDAImpl).entityBuilder.createThreadChannel(guild as GuildImpl, json, guildId, false) } } ) @@ -149,6 +161,7 @@ fun JDA.retrieveThreadChannelById(id: Long): CacheRestAction { * The cached threads are checked first, and then a request is made. * * The [RestAction] may throw [InvalidChannelTypeException] if a channel with the ID was found, but isn't a thread. + * If the thread is not owned by the current shard, [GuildNotFoundException] is thrown. * * @see JDA.retrieveThreadChannelByIdOrNull */ @@ -165,6 +178,7 @@ fun JDA.retrieveThreadChannelById(id: String): CacheRestAction { * - It doesn't exist * - The bot doesn't have access to it * - The channel isn't a thread + * - The thread isn't owned by a guild of this shard * * @see JDA.retrieveThreadChannelById */ @@ -187,6 +201,7 @@ suspend fun JDA.retrieveThreadChannelByIdOrNull(id: Long): ThreadChannel? { * - It doesn't exist * - The bot doesn't have access to it * - The channel isn't a thread + * - The thread isn't owned by a guild of this shard * * @see JDA.retrieveThreadChannelById */ From 85bc582e047953b03b50eb0c4103f516afe00a0b Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 1 Jul 2026 16:33:47 +0200 Subject: [PATCH 4/5] Make exception constructors internal --- .../botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt | 2 +- .../jda/ktx/retrieve/ParentGuildMismatchException.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt index aac919f87..55a5dbb18 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/InvalidChannelTypeException.kt @@ -3,4 +3,4 @@ package dev.freya02.botcommands.jda.ktx.retrieve /** * Exception thrown when retrieving a channel by ID, but the type is incorrect. */ -class InvalidChannelTypeException(message: String) : IllegalArgumentException(message) +class InvalidChannelTypeException internal constructor(message: String) : IllegalArgumentException(message) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt index 533ebad99..b6b409454 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/ParentGuildMismatchException.kt @@ -3,4 +3,4 @@ package dev.freya02.botcommands.jda.ktx.retrieve /** * Exception thrown when retrieving a channel by ID from a guild, but their expected guild is incorrect. */ -class ParentGuildMismatchException(message: String) : IllegalArgumentException(message) +class ParentGuildMismatchException internal constructor(message: String) : IllegalArgumentException(message) From 0e58de25baf29df7d0c154b573d29f9bd7d2f97a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:58:42 +0200 Subject: [PATCH 5/5] Remove IDs from exception messages --- .../kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt | 2 +- .../kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt index 0791e6692..b77394071 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/Guild.kt @@ -126,7 +126,7 @@ fun Guild.retrieveThreadChannelById(id: Long): CacheRestAction { throw InvalidChannelTypeException("Invalid channel type, expected a thread, got $channelType") if (dataObject.getUnsignedLong("guild_id", 0) != this.idLong) { - throw ParentGuildMismatchException("Thread $id is not from the same guild (expected ${this.id})") + throw ParentGuildMismatchException("Thread is not from the same guild") } (jda as JDAImpl).entityBuilder.createThreadChannel(this as GuildImpl, dataObject, this.idLong, false) diff --git a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt index ffdeba526..bb974b081 100644 --- a/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt +++ b/BotCommands-jda-ktx/src/main/kotlin/dev/freya02/botcommands/jda/ktx/retrieve/JDA.kt @@ -143,9 +143,9 @@ fun JDA.retrieveThreadChannelById(id: Long): CacheRestAction { val expectedShard = (guildId shr 22) % shardInfo.shardTotal val actualShard = shardInfo.shardId if (expectedShard.toInt() == actualShard) { - throw GuildNotFoundException("Thread $id was found but its guild was not found") + throw GuildNotFoundException("Thread was found but its guild was not found") } else { - throw GuildNotFoundException("Thread $id was found but its guild was not found as it is from a different shard") + throw GuildNotFoundException("Thread was found but its guild was not found as it is from a different shard") } }