Skip to content

Commit

Permalink
Add support for voice messages (#814)
Browse files Browse the repository at this point in the history
see discord/discord-api-docs#6082

---------

Co-authored-by: Lukellmann <lukellmann@gmail.com>
  • Loading branch information
DRSchlaubi and lukellmann committed Apr 20, 2023
1 parent d018a09 commit e9ec786
Show file tree
Hide file tree
Showing 11 changed files with 234 additions and 34 deletions.
28 changes: 23 additions & 5 deletions common/api/common.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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 <init> (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 <init> (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 <init> (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 <init> (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 <init> (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;
Expand All @@ -2431,18 +2433,20 @@ 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;
public final fun getId ()Ldev/kord/common/entity/Snowflake;
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
34 changes: 23 additions & 11 deletions common/src/commonMain/kotlin/entity/DiscordMessage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -502,15 +508,18 @@ public fun MessageFlags(flags: Iterable<MessageFlags>): 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(
Expand All @@ -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<DurationInDoubleSeconds> = Optional.Missing(),
val waveform: Optional<String> = Optional.Missing()
)

/**
Expand Down
7 changes: 6 additions & 1 deletion common/src/commonMain/kotlin/entity/Permission.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -349,7 +353,8 @@ public sealed class Permission(public val code: DiscordBitSet) {
UseEmbeddedActivities,
ModerateMembers,
ViewCreatorMonetizationAnalytics,
UseSoundboard
UseSoundboard,
SendVoiceMessages
)
}
}
20 changes: 20 additions & 0 deletions common/src/commonMain/kotlin/serialization/DurationSerializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<Duration> {
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
Original file line number Diff line number Diff line change
@@ -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<SerializationException> { serialize(Duration.INFINITE) }
assertFailsWith<SerializationException> { 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"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -132,7 +132,7 @@ abstract class DurationSerializerTest(
}


class DurationInNanosecondsSerializerTest : DurationSerializerTest(
class DurationInNanosecondsSerializerTest : DurationAsLongSerializerTest(
json = "84169",
duration = 84169.nanoseconds,
durationToRound = 84169.48.nanoseconds,
Expand All @@ -142,7 +142,7 @@ class DurationInNanosecondsSerializerTest : DurationSerializerTest(
serializer = DurationInNanosecondsSerializer,
)

class DurationInMicrosecondsSerializerTest : DurationSerializerTest(
class DurationInMicrosecondsSerializerTest : DurationAsLongSerializerTest(
json = "25622456",
duration = 25622456.microseconds,
durationToRound = 25622456.4.microseconds,
Expand All @@ -152,39 +152,39 @@ class DurationInMicrosecondsSerializerTest : DurationSerializerTest(
serializer = DurationInMicrosecondsSerializer,
)

class DurationInMillisecondsSerializerTest : DurationSerializerTest(
class DurationInMillisecondsSerializerTest : DurationAsLongSerializerTest(
json = "3495189",
duration = 3495189.milliseconds,
durationToRound = 3495189.24.milliseconds,
largeJson = "4611686018427387903", // the Duration implementation internal `MAX_MILLIS`
serializer = DurationInMillisecondsSerializer,
)

class DurationInSecondsSerializerTest : DurationSerializerTest(
class DurationInSecondsSerializerTest : DurationAsLongSerializerTest(
json = "987465",
duration = 987465.seconds,
durationToRound = 987465.489.seconds,
largeJson = "4611686018427388", // MAX_MILLIS / 1_000 + 1
serializer = DurationInSecondsSerializer,
)

class DurationInMinutesSerializerTest : DurationSerializerTest(
class DurationInMinutesSerializerTest : DurationAsLongSerializerTest(
json = "24905",
duration = 24905.minutes,
durationToRound = 24905.164.minutes,
largeJson = "76861433640457", // MAX_MILLIS / 1_000 / 60 + 1
serializer = DurationInMinutesSerializer,
)

class DurationInHoursSerializerTest : DurationSerializerTest(
class DurationInHoursSerializerTest : DurationAsLongSerializerTest(
json = "7245",
duration = 7245.hours,
durationToRound = 7245.24.hours,
largeJson = "1281023894008", // MAX_MILLIS / 1_000 / 60 / 60 + 1
serializer = DurationInHoursSerializer,
)

class DurationInDaysSerializerTest : DurationSerializerTest(
class DurationInDaysSerializerTest : DurationAsLongSerializerTest(
json = "92",
duration = 92.days,
durationToRound = 92.12.days,
Expand Down

0 comments on commit e9ec786

Please sign in to comment.