From e9ec786b35e175bbf9e7a9e801268faac37f8f61 Mon Sep 17 00:00:00 2001 From: Michael Rittmeister Date: Thu, 20 Apr 2023 02:31:18 +0200 Subject: [PATCH] Add support for voice messages (#814) see https://github.com/discord/discord-api-docs/pull/6082 --------- Co-authored-by: Lukellmann --- common/api/common.api | 28 ++++- .../kotlin/entity/DiscordMessage.kt | 34 ++++-- .../commonMain/kotlin/entity/Permission.kt | 7 +- .../serialization/DurationSerializers.kt | 20 ++++ .../DurationInDoubleSecondsSerializerTest.kt | 103 ++++++++++++++++++ .../serialization/DurationSerializersTests.kt | 16 +-- core/api/core.api | 16 ++- .../kotlin/cache/data/AttachmentData.kt | 7 +- .../commonMain/kotlin/entity/Attachment.kt | 17 ++- rest/api/rest.api | 5 + .../commonMain/kotlin/json/JsonErrorCode.kt | 15 +++ 11 files changed, 234 insertions(+), 34 deletions(-) create mode 100644 common/src/commonTest/kotlin/serialization/DurationInDoubleSecondsSerializerTest.kt diff --git a/common/api/common.api b/common/api/common.api index 42028ff4097b..28eae8e3fe12 100644 --- a/common/api/common.api +++ b/common/api/common.api @@ -2418,11 +2418,13 @@ public final class dev/kord/common/entity/DiscordApplicationKt { public final class dev/kord/common/entity/DiscordAttachment { public static final field Companion Ldev/kord/common/entity/DiscordAttachment$Companion; - public synthetic fun (ILdev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;)V - public synthetic fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILdev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;)V + public synthetic fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ldev/kord/common/entity/Snowflake; public final fun component10 ()Ldev/kord/common/entity/optional/OptionalBoolean; + public final fun component11 ()Ldev/kord/common/entity/optional/Optional; + public final fun component12 ()Ldev/kord/common/entity/optional/Optional; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ldev/kord/common/entity/optional/Optional; public final fun component4 ()Ldev/kord/common/entity/optional/Optional; @@ -2431,11 +2433,12 @@ public final class dev/kord/common/entity/DiscordAttachment { public final fun component7 ()Ljava/lang/String; public final fun component8 ()Ldev/kord/common/entity/optional/OptionalInt; public final fun component9 ()Ldev/kord/common/entity/optional/OptionalInt; - public final fun copy (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;)Ldev/kord/common/entity/DiscordAttachment; - public static synthetic fun copy$default (Ldev/kord/common/entity/DiscordAttachment;Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;ILjava/lang/Object;)Ldev/kord/common/entity/DiscordAttachment; + public final fun copy (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;)Ldev/kord/common/entity/DiscordAttachment; + public static synthetic fun copy$default (Ldev/kord/common/entity/DiscordAttachment;Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/Object;)Ldev/kord/common/entity/DiscordAttachment; public fun equals (Ljava/lang/Object;)Z public final fun getContentType ()Ldev/kord/common/entity/optional/Optional; public final fun getDescription ()Ldev/kord/common/entity/optional/Optional; + public final fun getDurationSecs ()Ldev/kord/common/entity/optional/Optional; public final fun getEphemeral ()Ldev/kord/common/entity/optional/OptionalBoolean; public final fun getFilename ()Ljava/lang/String; public final fun getHeight ()Ldev/kord/common/entity/optional/OptionalInt; @@ -2443,6 +2446,7 @@ public final class dev/kord/common/entity/DiscordAttachment { public final fun getProxyUrl ()Ljava/lang/String; public final fun getSize ()I public final fun getUrl ()Ljava/lang/String; + public final fun getWaveform ()Ldev/kord/common/entity/optional/Optional; public final fun getWidth ()Ldev/kord/common/entity/optional/OptionalInt; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -7096,6 +7100,7 @@ public final class dev/kord/common/entity/MessageFlag : java/lang/Enum { public static final field FailedToMentionSomeRolesInThread Ldev/kord/common/entity/MessageFlag; public static final field HasThread Ldev/kord/common/entity/MessageFlag; public static final field IsCrossPost Ldev/kord/common/entity/MessageFlag; + public static final field IsVoiceMessage Ldev/kord/common/entity/MessageFlag; public static final field Loading Ldev/kord/common/entity/MessageFlag; public static final field SourceMessageDeleted Ldev/kord/common/entity/MessageFlag; public static final field SuppressEmbeds Ldev/kord/common/entity/MessageFlag; @@ -7679,6 +7684,10 @@ public final class dev/kord/common/entity/Permission$SendTTSMessages : dev/kord/ public static final field INSTANCE Ldev/kord/common/entity/Permission$SendTTSMessages; } +public final class dev/kord/common/entity/Permission$SendVoiceMessages : dev/kord/common/entity/Permission { + public static final field INSTANCE Ldev/kord/common/entity/Permission$SendVoiceMessages; +} + public final class dev/kord/common/entity/Permission$Speak : dev/kord/common/entity/Permission { public static final field INSTANCE Ldev/kord/common/entity/Permission$Speak; } @@ -8705,6 +8714,15 @@ public final class dev/kord/common/serialization/DurationInDaysSerializer : dev/ public static final field INSTANCE Ldev/kord/common/serialization/DurationInDaysSerializer; } +public final class dev/kord/common/serialization/DurationInDoubleSecondsSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Ldev/kord/common/serialization/DurationInDoubleSecondsSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize-5sfh64U (Lkotlinx/serialization/encoding/Decoder;)J + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize-HG0u8IE (Lkotlinx/serialization/encoding/Encoder;J)V +} + public final class dev/kord/common/serialization/DurationInHoursSerializer : dev/kord/common/serialization/DurationAsLongSerializer { public static final field INSTANCE Ldev/kord/common/serialization/DurationInHoursSerializer; } diff --git a/common/src/commonMain/kotlin/entity/DiscordMessage.kt b/common/src/commonMain/kotlin/entity/DiscordMessage.kt index 263a79c080ab..b0d883742ff3 100644 --- a/common/src/commonMain/kotlin/entity/DiscordMessage.kt +++ b/common/src/commonMain/kotlin/entity/DiscordMessage.kt @@ -91,6 +91,7 @@ import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.OptionalInt import dev.kord.common.entity.optional.OptionalSnowflake +import dev.kord.common.serialization.DurationInDoubleSeconds import dev.kord.common.serialization.LongOrStringSerializer import dev.kord.ksp.GenerateKordEnum import dev.kord.ksp.GenerateKordEnum.Entry @@ -406,7 +407,12 @@ public enum class MessageFlag(public val code: Int) { FailedToMentionSomeRolesInThread(1 shl 8), /** This message will not trigger push and desktop notifications. */ - SuppressNotifications(1 shl 12) + SuppressNotifications(1 shl 12), + + /** + * This message is a voice message. + */ + IsVoiceMessage(1 shl 13) } @Serializable(with = MessageFlags.Serializer::class) @@ -502,15 +508,18 @@ public fun MessageFlags(flags: Iterable): MessageFlags = MessageFl /** * A representation of a [Discord Attachment structure](https://discord.com/developers/docs/resources/channel#attachment-object). * - * @param id The attachment id. - * @param filename The name of the attached file. - * @param description The description for the file. - * @param contentType The attachment's [media type](https://en.wikipedia.org/wiki/Media_type). - * @param size The size of the file in bytes. - * @param url The source url of the file. - * @param proxyUrl A proxied url of the field. - * @param height The height of the file (if it is an image). - * @param width The width of the file (if it is an image). + * @property id The attachment id. + * @property filename The name of the attached file. + * @property description The description for the file. + * @property contentType The attachment's [media type](https://en.wikipedia.org/wiki/Media_type). + * @property size The size of the file in bytes. + * @property url The source url of the file. + * @property proxyUrl A proxied url of the field. + * @property height The height of the file (if it is an image). + * @property width The width of the file (if it is an image). + * @property ephemeral Whether this attachment is ephemeral + * @property durationSecs The duration of the audio file (currently for voice messages) + * @property waveform Base64 encoded bytearray representing a sampled waveform (currently for voice messages) */ @Serializable public data class DiscordAttachment( @@ -534,7 +543,10 @@ public data class DiscordAttachment( */ val width: OptionalInt? = OptionalInt.Missing, - val ephemeral: OptionalBoolean = OptionalBoolean.Missing + val ephemeral: OptionalBoolean = OptionalBoolean.Missing, + @SerialName("duration_secs") + val durationSecs: Optional = Optional.Missing(), + val waveform: Optional = Optional.Missing() ) /** diff --git a/common/src/commonMain/kotlin/entity/Permission.kt b/common/src/commonMain/kotlin/entity/Permission.kt index ce1ed2211562..904f0586dd47 100644 --- a/common/src/commonMain/kotlin/entity/Permission.kt +++ b/common/src/commonMain/kotlin/entity/Permission.kt @@ -295,6 +295,10 @@ public sealed class Permission(public val code: DiscordBitSet) { /** Allows for using soundboard in a voice channel. */ public object UseSoundboard : Permission(1L shl 42) + /** + * Allows sending voice messages. + */ + public object SendVoiceMessages : Permission(1L shl 46) /** All [Permission]s combined into one. */ public object All : Permission(buildAll()) @@ -349,7 +353,8 @@ public sealed class Permission(public val code: DiscordBitSet) { UseEmbeddedActivities, ModerateMembers, ViewCreatorMonetizationAnalytics, - UseSoundboard + UseSoundboard, + SendVoiceMessages ) } } diff --git a/common/src/commonMain/kotlin/serialization/DurationSerializers.kt b/common/src/commonMain/kotlin/serialization/DurationSerializers.kt index ea6339c797e6..aad1b7d5ad10 100644 --- a/common/src/commonMain/kotlin/serialization/DurationSerializers.kt +++ b/common/src/commonMain/kotlin/serialization/DurationSerializers.kt @@ -13,6 +13,7 @@ import kotlin.time.DurationUnit import kotlin.time.DurationUnit.* import kotlin.time.toDuration +// -------- as Long -------- /** Serializer that encodes and decodes [Duration]s as a [Long] number of the specified [unit]. */ public sealed class DurationAsLongSerializer( @@ -107,3 +108,22 @@ public object DurationInDaysSerializer : DurationAsLongSerializer(DAYS, "Duratio /** A [Duration] that is [serializable][Serializable] with [DurationInDaysSerializer]. */ public typealias DurationInDays = @Serializable(with = DurationInDaysSerializer::class) Duration + + +// -------- as Double -------- + +/** Serializer that encodes and decodes [Duration]s as a [Double] number of seconds. */ +public object DurationInDoubleSecondsSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("dev.kord.common.serialization.DurationInDoubleSeconds", PrimitiveKind.DOUBLE) + + override fun serialize(encoder: Encoder, value: Duration) { + if (value.isInfinite()) throw SerializationException("Infinite Durations cannot be serialized, got $value") + encoder.encodeDouble(value.toDouble(unit = SECONDS)) + } + + override fun deserialize(decoder: Decoder): Duration = decoder.decodeDouble().toDuration(unit = SECONDS) +} + +/** A [Duration] that is [serializable][Serializable] with [DurationInDoubleSecondsSerializer]. */ +public typealias DurationInDoubleSeconds = @Serializable(with = DurationInDoubleSecondsSerializer::class) Duration diff --git a/common/src/commonTest/kotlin/serialization/DurationInDoubleSecondsSerializerTest.kt b/common/src/commonTest/kotlin/serialization/DurationInDoubleSecondsSerializerTest.kt new file mode 100644 index 000000000000..1e1bbd47481c --- /dev/null +++ b/common/src/commonTest/kotlin/serialization/DurationInDoubleSecondsSerializerTest.kt @@ -0,0 +1,103 @@ +package dev.kord.common.serialization + +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds + +class DurationInDoubleSecondsSerializerTest { + private fun serialize(duration: Duration) = Json.encodeToString(DurationInDoubleSecondsSerializer, duration) + private fun deserialize(json: String) = Json.decodeFromString(DurationInDoubleSecondsSerializer, json) + + + @Test + fun zero_Duration_can_be_serialized() { + assertEquals(expected = 0.0.toString(), actual = serialize(Duration.ZERO)) + } + + @Test + fun zero_Duration_can_be_deserialized() { + for (jsonZero in listOf("0", "0.0", "0.0000", "0.00e-0864", "0E+456")) { + assertEquals(expected = Duration.ZERO, actual = deserialize(jsonZero)) + } + } + + + @Test + fun infinite_Durations_cannot_be_serialized() { + assertFailsWith { serialize(Duration.INFINITE) } + assertFailsWith { serialize(-Duration.INFINITE) } + } + + private val largestFiniteDuration = (Long.MAX_VALUE / 2 - 1).milliseconds + + init { + check(largestFiniteDuration.isFinite()) + check(largestFiniteDuration + (1.milliseconds - 1.nanoseconds) == largestFiniteDuration) + check((largestFiniteDuration + 1.milliseconds).isInfinite()) + } + + @Test + fun largest_finite_Durations_can_be_serialized() { + assertEquals(4.611686018427388e+15.toString(), serialize(largestFiniteDuration)) + assertEquals((-4.611686018427388e+15).toString(), serialize(-largestFiniteDuration)) + } + + + private val duration2Jsons = listOf( + 123.seconds to listOf(123.0.toString(), "123", "0.1230E+3", "1230.0e-1"), + 5646.876456.seconds to listOf("5646.876456", "5646.87645600", "5.646876456e003"), + 4631.89.seconds to listOf("4631.89", "4631.890000000", "46.3189000000E2"), + 4.595632e+1.seconds to listOf("45.95632", "4.595632e+1"), + ) + + @Test + fun positive_Durations_can_be_serialized() { + for ((duration, jsons) in duration2Jsons) { + assertEquals(expected = jsons.first(), actual = serialize(duration)) + } + } + + @Test + fun positive_Durations_can_be_deserialized() { + for ((duration, jsons) in duration2Jsons) { + for (json in jsons) { + assertEquals(expected = duration, actual = deserialize(json)) + } + } + } + + @Test + fun negative_Durations_can_be_serialized() { + for ((duration, jsons) in duration2Jsons) { + assertEquals(expected = "-${jsons.first()}", actual = serialize(-duration)) + } + } + + @Test + fun negative_Durations_can_be_deserialized() { + for ((duration, jsons) in duration2Jsons) { + for (json in jsons) { + assertEquals(expected = -duration, actual = deserialize("-$json")) + } + } + } + + + private val largeJson = "4611686018427388" // MAX_MILLIS / 1_000 + 1 + + @Test + fun large_positive_Duration_gets_deserialized_as_Infinity() { + assertEquals(expected = Duration.INFINITE, deserialize(largeJson)) + } + + @Test + fun large_negative_Duration_gets_deserialized_as_negative_Infinity() { + assertEquals(expected = -Duration.INFINITE, deserialize("-$largeJson")) + } +} diff --git a/common/src/commonTest/kotlin/serialization/DurationSerializersTests.kt b/common/src/commonTest/kotlin/serialization/DurationSerializersTests.kt index 5a5e5761bdbc..438ae90955ba 100644 --- a/common/src/commonTest/kotlin/serialization/DurationSerializersTests.kt +++ b/common/src/commonTest/kotlin/serialization/DurationSerializersTests.kt @@ -16,7 +16,7 @@ import kotlin.time.Duration.Companion.nanoseconds import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit.MILLISECONDS -abstract class DurationSerializerTest( +abstract class DurationAsLongSerializerTest( private val json: String, private val duration: Duration, private val durationToRound: Duration, @@ -132,7 +132,7 @@ abstract class DurationSerializerTest( } -class DurationInNanosecondsSerializerTest : DurationSerializerTest( +class DurationInNanosecondsSerializerTest : DurationAsLongSerializerTest( json = "84169", duration = 84169.nanoseconds, durationToRound = 84169.48.nanoseconds, @@ -142,7 +142,7 @@ class DurationInNanosecondsSerializerTest : DurationSerializerTest( serializer = DurationInNanosecondsSerializer, ) -class DurationInMicrosecondsSerializerTest : DurationSerializerTest( +class DurationInMicrosecondsSerializerTest : DurationAsLongSerializerTest( json = "25622456", duration = 25622456.microseconds, durationToRound = 25622456.4.microseconds, @@ -152,7 +152,7 @@ class DurationInMicrosecondsSerializerTest : DurationSerializerTest( serializer = DurationInMicrosecondsSerializer, ) -class DurationInMillisecondsSerializerTest : DurationSerializerTest( +class DurationInMillisecondsSerializerTest : DurationAsLongSerializerTest( json = "3495189", duration = 3495189.milliseconds, durationToRound = 3495189.24.milliseconds, @@ -160,7 +160,7 @@ class DurationInMillisecondsSerializerTest : DurationSerializerTest( serializer = DurationInMillisecondsSerializer, ) -class DurationInSecondsSerializerTest : DurationSerializerTest( +class DurationInSecondsSerializerTest : DurationAsLongSerializerTest( json = "987465", duration = 987465.seconds, durationToRound = 987465.489.seconds, @@ -168,7 +168,7 @@ class DurationInSecondsSerializerTest : DurationSerializerTest( serializer = DurationInSecondsSerializer, ) -class DurationInMinutesSerializerTest : DurationSerializerTest( +class DurationInMinutesSerializerTest : DurationAsLongSerializerTest( json = "24905", duration = 24905.minutes, durationToRound = 24905.164.minutes, @@ -176,7 +176,7 @@ class DurationInMinutesSerializerTest : DurationSerializerTest( serializer = DurationInMinutesSerializer, ) -class DurationInHoursSerializerTest : DurationSerializerTest( +class DurationInHoursSerializerTest : DurationAsLongSerializerTest( json = "7245", duration = 7245.hours, durationToRound = 7245.24.hours, @@ -184,7 +184,7 @@ class DurationInHoursSerializerTest : DurationSerializerTest( serializer = DurationInHoursSerializer, ) -class DurationInDaysSerializerTest : DurationSerializerTest( +class DurationInDaysSerializerTest : DurationAsLongSerializerTest( json = "92", duration = 92.days, durationToRound = 92.12.days, diff --git a/core/api/core.api b/core/api/core.api index ea4c2c2cd140..13833efbcee4 100644 --- a/core/api/core.api +++ b/core/api/core.api @@ -2661,11 +2661,13 @@ public final class dev/kord/core/cache/data/ApplicationInteractionData$Companion public final class dev/kord/core/cache/data/AttachmentData { public static final field Companion Ldev/kord/core/cache/data/AttachmentData$Companion; - public synthetic fun (ILdev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V - public fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;)V - public synthetic fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (ILdev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;Lkotlinx/serialization/internal/SerializationConstructorMarker;)V + public fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;)V + public synthetic fun (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ldev/kord/common/entity/Snowflake; public final fun component10 ()Ldev/kord/common/entity/optional/OptionalBoolean; + public final fun component11 ()Ldev/kord/common/entity/optional/Optional; + public final fun component12 ()Ldev/kord/common/entity/optional/Optional; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ldev/kord/common/entity/optional/Optional; public final fun component4 ()Ldev/kord/common/entity/optional/Optional; @@ -2674,11 +2676,12 @@ public final class dev/kord/core/cache/data/AttachmentData { public final fun component7 ()Ljava/lang/String; public final fun component8 ()Ldev/kord/common/entity/optional/OptionalInt; public final fun component9 ()Ldev/kord/common/entity/optional/OptionalInt; - public final fun copy (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;)Ldev/kord/core/cache/data/AttachmentData; - public static synthetic fun copy$default (Ldev/kord/core/cache/data/AttachmentData;Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;ILjava/lang/Object;)Ldev/kord/core/cache/data/AttachmentData; + public final fun copy (Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;)Ldev/kord/core/cache/data/AttachmentData; + public static synthetic fun copy$default (Ldev/kord/core/cache/data/AttachmentData;Ldev/kord/common/entity/Snowflake;Ljava/lang/String;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/String;Ljava/lang/String;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalInt;Ldev/kord/common/entity/optional/OptionalBoolean;Ldev/kord/common/entity/optional/Optional;Ldev/kord/common/entity/optional/Optional;ILjava/lang/Object;)Ldev/kord/core/cache/data/AttachmentData; public fun equals (Ljava/lang/Object;)Z public final fun getContentType ()Ldev/kord/common/entity/optional/Optional; public final fun getDescription ()Ldev/kord/common/entity/optional/Optional; + public final fun getDurationSecs ()Ldev/kord/common/entity/optional/Optional; public final fun getEphemeral ()Ldev/kord/common/entity/optional/OptionalBoolean; public final fun getFilename ()Ljava/lang/String; public final fun getHeight ()Ldev/kord/common/entity/optional/OptionalInt; @@ -2686,6 +2689,7 @@ public final class dev/kord/core/cache/data/AttachmentData { public final fun getProxyUrl ()Ljava/lang/String; public final fun getSize ()I public final fun getUrl ()Ljava/lang/String; + public final fun getWaveform ()Ldev/kord/common/entity/optional/Optional; public final fun getWidth ()Ldev/kord/common/entity/optional/OptionalInt; public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -5815,6 +5819,7 @@ public final class dev/kord/core/entity/Attachment : dev/kord/core/entity/KordEn public final fun getContentType ()Ljava/lang/String; public final fun getData ()Ldev/kord/core/cache/data/AttachmentData; public final fun getDescription ()Ljava/lang/String; + public final fun getDuration-FghU774 ()Lkotlin/time/Duration; public final fun getFilename ()Ljava/lang/String; public final fun getHeight ()Ljava/lang/Integer; public fun getId ()Ldev/kord/common/entity/Snowflake; @@ -5822,6 +5827,7 @@ public final class dev/kord/core/entity/Attachment : dev/kord/core/entity/KordEn public final fun getProxyUrl ()Ljava/lang/String; public final fun getSize ()I public final fun getUrl ()Ljava/lang/String; + public final fun getWaveform ()[B public final fun getWidth ()Ljava/lang/Integer; public fun hashCode ()I public final fun isEphemeral ()Z diff --git a/core/src/commonMain/kotlin/cache/data/AttachmentData.kt b/core/src/commonMain/kotlin/cache/data/AttachmentData.kt index cca121971a22..509117004fe3 100644 --- a/core/src/commonMain/kotlin/cache/data/AttachmentData.kt +++ b/core/src/commonMain/kotlin/cache/data/AttachmentData.kt @@ -5,6 +5,7 @@ import dev.kord.common.entity.Snowflake import dev.kord.common.entity.optional.Optional import dev.kord.common.entity.optional.OptionalBoolean import dev.kord.common.entity.optional.OptionalInt +import dev.kord.common.serialization.DurationInDoubleSeconds import kotlinx.serialization.Serializable @Serializable @@ -18,11 +19,13 @@ public data class AttachmentData( val proxyUrl: String, val height: OptionalInt? = OptionalInt.Missing, val width: OptionalInt? = OptionalInt.Missing, - val ephemeral: OptionalBoolean = OptionalBoolean.Missing + val ephemeral: OptionalBoolean = OptionalBoolean.Missing, + val durationSecs: Optional = Optional.Missing(), + val waveform: Optional = Optional.Missing() ) { public companion object { public fun from(entity: DiscordAttachment): AttachmentData = with(entity) { - AttachmentData(id, filename, description, contentType, size, url, proxyUrl, height, width, ephemeral) + AttachmentData(id, filename, description, contentType, size, url, proxyUrl, height, width, ephemeral, durationSecs, waveform) } } } diff --git a/core/src/commonMain/kotlin/entity/Attachment.kt b/core/src/commonMain/kotlin/entity/Attachment.kt index 6bd5b83254cc..5c0022bde374 100644 --- a/core/src/commonMain/kotlin/entity/Attachment.kt +++ b/core/src/commonMain/kotlin/entity/Attachment.kt @@ -5,8 +5,10 @@ import dev.kord.common.entity.Snowflake import dev.kord.common.entity.optional.value import dev.kord.core.Kord import dev.kord.core.cache.data.AttachmentData -import dev.kord.rest.Image import dev.kord.core.hash +import dev.kord.rest.Image +import io.ktor.util.* +import kotlin.time.Duration /** * An instance of a [Discord Attachment](https://discord.com/developers/docs/resources/channel#attachment-object). @@ -58,12 +60,23 @@ public data class Attachment(val data: AttachmentData, override val kord: Kord) */ val width: Int? get() = data.width.value + /** + * The duration of the audio file (currently for voice messages). + */ + val duration: Duration? get() = data.durationSecs.value + + /** + * A sampled waveform (currently for voice messages). + */ + val waveform: ByteArray? get() = data.waveform.value?.decodeBase64Bytes() + /** * If this file is displayed as a spoiler. Denoted by the `SPOILER_` prefix in the name. */ val isSpoiler: Boolean get() = filename.startsWith("SPOILER_") - val isEphemeral: Boolean get() = data.ephemeral.discordBoolean + val isEphemeral: Boolean get() = data.ephemeral.discordBoolean + /** * If this file is an image. Denoted by the presence of a [width] and [height]. */ diff --git a/rest/api/rest.api b/rest/api/rest.api index 170c99885d1a..fd88e0baf7bd 100644 --- a/rest/api/rest.api +++ b/rest/api/rest.api @@ -2433,6 +2433,7 @@ public final class dev/kord/rest/json/JsonErrorCode : java/lang/Enum { public static final field CannotSendMessageInForumChannel Ldev/kord/rest/json/JsonErrorCode; public static final field CannotSendMessagesInNonTextChannel Ldev/kord/rest/json/JsonErrorCode; public static final field CannotSendMessagesToUser Ldev/kord/rest/json/JsonErrorCode; + public static final field CannotSendVoiceMessagesInThisChannel Ldev/kord/rest/json/JsonErrorCode; public static final field CannotUpdateFinishedEvent Ldev/kord/rest/json/JsonErrorCode; public static final field ChannelVerificationTooHigh Ldev/kord/rest/json/JsonErrorCode; public static final field ChannelWriteRateLimit Ldev/kord/rest/json/JsonErrorCode; @@ -2595,6 +2596,10 @@ public final class dev/kord/rest/json/JsonErrorCode : java/lang/Enum { public static final field UserBannedFromGuild Ldev/kord/rest/json/JsonErrorCode; public static final field UserNotInVoice Ldev/kord/rest/json/JsonErrorCode; public static final field VerifyAccount Ldev/kord/rest/json/JsonErrorCode; + public static final field VoiceMessagesCannotBeEdited Ldev/kord/rest/json/JsonErrorCode; + public static final field VoiceMessagesDoNotSupportAdditionalContent Ldev/kord/rest/json/JsonErrorCode; + public static final field VoiceMessagesMustHaveASingleAudioAttachment Ldev/kord/rest/json/JsonErrorCode; + public static final field VoiceMessagesMustHaveSupportingMetadata Ldev/kord/rest/json/JsonErrorCode; public static final field WebhookCannotHaveThreadNameAndThreadId Ldev/kord/rest/json/JsonErrorCode; public static final field WebhookMissingThreadNameOrThreadId Ldev/kord/rest/json/JsonErrorCode; public static final field WebhookServicesCannotBeUsedInForumChannels Ldev/kord/rest/json/JsonErrorCode; diff --git a/rest/src/commonMain/kotlin/json/JsonErrorCode.kt b/rest/src/commonMain/kotlin/json/JsonErrorCode.kt index af73e0df2581..35bdde062196 100644 --- a/rest/src/commonMain/kotlin/json/JsonErrorCode.kt +++ b/rest/src/commonMain/kotlin/json/JsonErrorCode.kt @@ -512,9 +512,24 @@ public enum class JsonErrorCode(public val code: Int) { /** Uploaded file not found. */ UnknownUpload(50146), + /** Voice messages do not support additional content. */ + VoiceMessagesDoNotSupportAdditionalContent(50159), + + /** Voice messages must have a single audio attachment. */ + VoiceMessagesMustHaveASingleAudioAttachment(50160), + + /** Voice messages must have supporting metadata. */ + VoiceMessagesMustHaveSupportingMetadata(50161), + + /** Voice messages cannot be edited. */ + VoiceMessagesCannotBeEdited(50162), + /** Cannot delete guild subscription integration. */ CannotDeleteGuildSubscriptionIntegration(50163), + /** You cannot send voice messages in this channel. */ + CannotSendVoiceMessagesInThisChannel(50173), + /** You do not have permission to send this sticker. */ StickerPermissionLack(50600),