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 5 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
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.DurationInFloatingPointSeconds
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<DurationInFloatingPointSeconds> = 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
)
}
}
27 changes: 27 additions & 0 deletions common/src/commonMain/kotlin/serialization/DurationSerializers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,33 @@ public sealed class DurationAsLongSerializer(
}
}

/** A [Duration] that is [serializable][Serializable] with [DurationInFloatingPointSecondsSerializer]. */
public typealias DurationInFloatingPointSeconds = @Serializable(with = DurationInFloatingPointSecondsSerializer::class) Duration

/** Serializer that encodes and decodes [Duration]s as a [Double] number of seconds */
public object DurationInFloatingPointSecondsSerializer : KSerializer<Duration> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("dev.kord.common.serialization.DurationInFloatingPointSeconds", PrimitiveKind.DOUBLE)

override fun serialize(encoder: Encoder, value: Duration) {
when (val valueAsDouble = value.toDouble(MILLISECONDS)) {
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
Double.MIN_VALUE, Double.MAX_VALUE -> throw SerializationException(
lukellmann marked this conversation as resolved.
Show resolved Hide resolved
if (value.isInfinite()) {
"Infinite Durations cannot be serialized, got $value"
} else {
"The Duration $value expressed as a number of SECONDS does not fit in the range of Long type and therefore cannot be serialized with DurationInFloatingPointSecondsSerializer"
}
)

else -> encoder.encodeDouble(valueAsDouble)
}
}

override fun deserialize(decoder: Decoder): Duration {
return decoder.decodeDouble().div(1000).toDuration(MILLISECONDS)
}
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved
}


// nanoseconds

Expand Down
7 changes: 5 additions & 2 deletions core/src/commonMain/kotlin/cache/data/AttachmentData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.DurationInFloatingPointSeconds
import kotlinx.serialization.Serializable

@Serializable
Expand All @@ -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<DurationInFloatingPointSeconds> = Optional.Missing(),
val waveform: Optional<String> = 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)
}
}
}
19 changes: 17 additions & 2 deletions core/src/commonMain/kotlin/entity/Attachment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -58,12 +60,25 @@ 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? by lazy {
data.waveform.value?.decodeBase64Bytes()
}
DRSchlaubi marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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].
*/
Expand Down