Skip to main content

wavyzon_plugin/
event.rs

1//! Player events, as sent over the WebSocket (`op: "event"`) and delivered to
2//! plugin [`EventListener`](crate::EventListener)s.
3
4use serde::{Deserialize, Serialize};
5
6use crate::{Failure, Track};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub enum EventType {
10    TrackLoadingEvent,
11    TrackStartEvent,
12    TrackAudibleEvent,
13    TrackEndEvent,
14    TrackExceptionEvent,
15    TrackStuckEvent,
16    MixArmedEvent,
17    MixStartedEvent,
18    MixPromotedEvent,
19    MixDroppedEvent,
20    HostStartedEvent,
21    HostEndedEvent,
22    SendStallEvent,
23    #[serde(rename = "PositionEvent")]
24    PositionJump,
25    WebSocketClosedEvent,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub enum EndReason {
31    Finished,
32    LoadFailed,
33    Stopped,
34    Replaced,
35    Cleanup,
36}
37
38impl EndReason {
39    /// Whether a client may start the next queued track (Lavalink semantics).
40    pub fn may_start_next(self) -> bool {
41        matches!(self, EndReason::Finished | EndReason::LoadFailed)
42    }
43}
44
45/// One event. Optional fields are omitted from JSON when unset.
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct Event {
49    pub op: String,
50    #[serde(rename = "type")]
51    pub kind: EventType,
52    pub guild_id: String,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub track: Option<Track>,
55    #[serde(default, skip_serializing_if = "String::is_empty")]
56    pub encoded: String,
57    #[serde(default, skip_serializing_if = "is_zero_u64")]
58    pub instance: u64,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub reason: Option<EndReason>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub exception: Option<Failure>,
63    #[serde(default, skip_serializing_if = "is_zero")]
64    pub position_ms: i64,
65    #[serde(default, skip_serializing_if = "is_zero")]
66    pub sent_at: i64,
67    #[serde(default, skip_serializing_if = "is_zero")]
68    pub audible_at: i64,
69    #[serde(default, skip_serializing_if = "is_zero")]
70    pub duration_ms: i64,
71    #[serde(default, skip_serializing_if = "is_zero")]
72    pub stall_ms: i64,
73    #[serde(default, skip_serializing_if = "is_zero")]
74    pub threshold_ms: i64,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub into: Option<Track>,
77    #[serde(default, rename = "intoPositionMs", skip_serializing_if = "is_zero")]
78    pub into_ms: i64,
79    /// Voice gateway close code (`WebSocketClosedEvent`).
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub code: Option<u16>,
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub terminal: Option<bool>,
84    #[serde(default, rename = "closeReason", skip_serializing_if = "String::is_empty")]
85    pub close_reason: String,
86}
87
88fn is_zero(v: &i64) -> bool {
89    *v == 0
90}
91fn is_zero_u64(v: &u64) -> bool {
92    *v == 0
93}
94
95impl Event {
96    /// A blank event of `kind` for `guild_id`; fill the relevant fields after.
97    pub fn new(kind: EventType, guild_id: impl Into<String>) -> Self {
98        Self {
99            op: "event".into(),
100            kind,
101            guild_id: guild_id.into(),
102            track: None,
103            encoded: String::new(),
104            instance: 0,
105            reason: None,
106            exception: None,
107            position_ms: 0,
108            sent_at: 0,
109            audible_at: 0,
110            duration_ms: 0,
111            stall_ms: 0,
112            threshold_ms: 0,
113            into: None,
114            into_ms: 0,
115            code: None,
116            terminal: None,
117            close_reason: String::new(),
118        }
119    }
120
121    /// Attach a track (and its encoded form).
122    pub fn with_track(mut self, t: &Track) -> Self {
123        self.encoded = t.encode();
124        self.track = Some(t.clone());
125        self
126    }
127
128    pub fn with_instance(mut self, instance: u64) -> Self {
129        self.instance = instance;
130        self
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn serializes_compactly() {
140        let e = Event::new(EventType::TrackEndEvent, "1").with_instance(3);
141        let mut e = e;
142        e.reason = Some(EndReason::LoadFailed);
143        let v = serde_json::to_value(&e).unwrap();
144        assert_eq!(v["type"], "TrackEndEvent");
145        assert_eq!(v["reason"], "loadFailed");
146        assert_eq!(v["op"], "event");
147        assert!(v.get("positionMs").is_none());
148        let p = serde_json::to_value(Event::new(EventType::PositionJump, "1")).unwrap();
149        assert_eq!(p["type"], "PositionEvent");
150    }
151}