Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for voice messages #814

Merged
merged 16 commits into from
Apr 20, 2023
Merged
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
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)
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved

/** 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
Loading