Skip to main content

filthy_rich/
types.rs

1//! Core types related to filthy-rich. Used in conjunction with the core imports.
2//!
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize, ser::SerializeStruct};
5use std::{collections::HashMap, time::Duration};
6use uuid::Uuid;
7
8use crate::utils::filter_none_string;
9
10#[derive(Debug, Serialize)]
11pub(crate) struct ActivityCommand {
12    cmd: String,
13    args: ActivityCommandArgs,
14    nonce: String,
15}
16
17impl ActivityCommand {
18    pub fn new_with(activity: Option<ActivityPayload>) -> Self {
19        Self {
20            cmd: "SET_ACTIVITY".to_string(),
21            args: ActivityCommandArgs {
22                pid: std::process::id(),
23                activity,
24            },
25            nonce: Uuid::new_v4().to_string(),
26        }
27    }
28
29    pub fn to_json(&self) -> Result<String> {
30        serde_json::to_string(self).context("Failed to serialize IPC activity command.")
31    }
32}
33
34#[derive(Debug, Serialize)]
35struct ActivityCommandArgs {
36    pid: u32,
37    activity: Option<ActivityPayload>,
38}
39
40/// Payload that actually gets serialized for setting a rich presence activity.
41///
42/// Reference: https://docs.discord.com/developers/events/gateway-events#activity-object
43#[derive(Debug, Serialize)]
44pub(crate) struct ActivityPayload {
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub name: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub r#type: Option<u8>,
49    pub created_at: u64,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub instance: Option<bool>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub status_display_type: Option<u8>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub details: Option<String>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub details_url: Option<String>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub state: Option<String>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub state_url: Option<String>,
62    pub timestamps: TimestampPayload,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub assets: Option<AssetsPayload>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub buttons: Option<Vec<ButtonPayload>>,
67}
68
69#[derive(Debug)]
70pub(crate) struct AssetsPayload {
71    pub large_image: Option<String>,
72    pub large_url: Option<String>,
73    pub large_text: Option<String>,
74    pub small_image: Option<String>,
75    pub small_text: Option<String>,
76    pub small_url: Option<String>,
77}
78
79// This is redundant as [`ActivityBuilder`] already accepts large_image/small_image fields mandatorily before receiving any of their
80// corresponding url/text fields to ensure safety.
81impl Serialize for AssetsPayload {
82    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
83    where
84        S: serde::Serializer,
85    {
86        let mut state = serializer.serialize_struct("AssetsPayload", 6)?;
87
88        if let Some(v) = &self.large_image {
89            state.serialize_field("large_image", v)?;
90
91            if let Some(v) = &self.large_text {
92                state.serialize_field("large_text", v)?;
93            }
94            if let Some(v) = &self.large_url {
95                state.serialize_field("large_url", v)?;
96            }
97        }
98
99        if let Some(v) = &self.small_image {
100            state.serialize_field("small_image", v)?;
101
102            if let Some(v) = &self.small_text {
103                state.serialize_field("small_text", v)?;
104            }
105            if let Some(v) = &self.small_url {
106                state.serialize_field("small_url", v)?;
107            }
108        }
109
110        state.end()
111    }
112}
113
114#[derive(Debug, Serialize)]
115pub(crate) struct ButtonPayload {
116    pub label: String,
117    pub url: String,
118}
119
120#[derive(Debug, Serialize)]
121pub(crate) struct TimestampPayload {
122    pub start: u64,
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub end: Option<u64>,
125}
126
127/// An iteration of a frame primarily for interpreting received READY events.
128#[derive(Debug, Deserialize)]
129pub(crate) struct ReadyRPCFrame {
130    pub cmd: Option<String>,
131    pub evt: Option<String>,
132    pub data: Option<ReadyData>,
133}
134
135/// An iteration of a frame for using throughout the generic read_frame loop.
136#[derive(Debug, Deserialize)]
137pub(crate) struct DynamicRPCFrame {
138    #[allow(unused)]
139    pub cmd: Option<String>,
140    pub evt: Option<String>,
141    #[allow(unused)]
142    pub nonce: Option<String>,
143    pub data: Option<serde_json::Value>,
144}
145
146#[derive(Debug)]
147pub(crate) enum IPCCommand {
148    SetActivity {
149        activity: Box<Activity>,
150    },
151    ClearActivity,
152    Close {
153        done: tokio::sync::oneshot::Sender<()>,
154    },
155}
156
157/// Data received in response from the server after sending a SET_ACTIVITY command.
158///
159/// Note that this struct doesn't fully cover the schema of the actual response since most of the fields
160/// that are found are the same as the actual activity that is sent.
161#[derive(Debug, Clone, Deserialize)]
162pub struct ActivityResponseData {
163    pub application_id: String,
164    pub platform: String,
165    pub name: String,
166    pub metadata: serde_json::Value,
167}
168
169/// Data received from READY event.
170#[derive(Debug, Clone, Deserialize)]
171pub struct ReadyData {
172    pub user: DiscordUser,
173}
174
175/// Represents a Discord user.
176#[derive(Debug, Clone, Deserialize)]
177pub struct DiscordUser {
178    pub id: String,
179    pub username: String,
180    pub global_name: Option<String>,
181    pub discriminator: Option<String>,
182    pub avatar: Option<String>,
183    // TODO: extend this into a deserializable struct (probably?)
184    pub avatar_decoration_data: Option<serde_json::Value>,
185    pub bot: bool,
186    pub flags: Option<u64>,
187    pub premium_type: Option<u64>,
188}
189
190/// Enum indicating the activity type.
191#[repr(u8)]
192#[derive(Clone, Debug, Eq, PartialEq, Copy)]
193pub enum ActivityType {
194    Playing = 0,
195    Listening = 2,
196    Watching = 3,
197    Competing = 5,
198}
199
200impl From<ActivityType> for u8 {
201    fn from(value: ActivityType) -> Self {
202        value as u8
203    }
204}
205
206/// Enum indicating which mode to use for indicating the status of an activity.
207#[repr(u8)]
208#[derive(Clone, Debug, Eq, PartialEq, Copy)]
209pub enum StatusDisplayType {
210    Name = 0,
211    Details = 2,
212    State = 1,
213}
214
215impl From<StatusDisplayType> for u8 {
216    fn from(value: StatusDisplayType) -> Self {
217        value as u8
218    }
219}
220
221/// Represents a Discord Rich Presence activity.
222#[derive(Debug, Clone)]
223pub struct Activity {
224    pub(crate) name: Option<String>,
225    pub(crate) activity_type: Option<ActivityType>,
226    pub(crate) status_display_type: Option<StatusDisplayType>,
227    pub(crate) details: Option<String>,
228    pub(crate) details_url: Option<String>,
229    pub(crate) state: Option<String>,
230    pub(crate) state_url: Option<String>,
231    pub(crate) instance: Option<bool>,
232    pub(crate) duration: Option<Duration>,
233    pub(crate) large_image: Option<String>,
234    pub(crate) large_text: Option<String>,
235    pub(crate) large_url: Option<String>,
236    pub(crate) small_image: Option<String>,
237    pub(crate) small_text: Option<String>,
238    pub(crate) small_url: Option<String>,
239    pub(crate) buttons: Option<HashMap<String, String>>,
240}
241
242impl Activity {
243    /// Initializes a new activity builder instance.
244    #[must_use]
245    #[allow(clippy::new_ret_no_self)]
246    pub fn new() -> ActivityBuilder {
247        ActivityBuilder::default()
248    }
249}
250
251impl Default for Activity {
252    /// Gives out an empty [`Activity`] with all of the default values. Essentially,
253    /// this only shows the name of the app and the elapsed time for the activity on
254    /// Discord. Useful when you only need a simple rich presence instance.
255    ///
256    /// For building a complete activity, using [`Activity::new`] is suggested instead.
257    fn default() -> Self {
258        Self {
259            name: None,
260            activity_type: None,
261            status_display_type: None,
262            details: None,
263            details_url: None,
264            state: None,
265            state_url: None,
266            instance: None,
267            duration: None,
268            large_image: None,
269            large_text: None,
270            large_url: None,
271            small_image: None,
272            small_text: None,
273            small_url: None,
274            buttons: None,
275        }
276    }
277}
278
279/// A builder for a Rich Presence activity.
280/// To build an [`Activity`] out of it, use [`ActivityBuilder::build`].
281#[derive(Default)]
282pub struct ActivityBuilder {
283    name: Option<String>,
284    activity_type: Option<ActivityType>,
285    status_display_type: Option<StatusDisplayType>,
286    instance: Option<bool>,
287    details: Option<String>,
288    details_url: Option<String>,
289    state: Option<String>,
290    state_url: Option<String>,
291    duration: Option<Duration>,
292    large_image: Option<String>,
293    large_text: Option<String>,
294    large_url: Option<String>,
295    small_image: Option<String>,
296    small_text: Option<String>,
297    small_url: Option<String>,
298    buttons: Option<HashMap<String, String>>,
299}
300
301impl ActivityBuilder {
302    /// Name of the activity.
303    pub fn name(mut self, text: impl Into<String>) -> Self {
304        self.name = filter_none_string(text);
305        self
306    }
307
308    /// The type of activity you want to create.
309    #[must_use]
310    pub fn activity_type(mut self, r#type: ActivityType) -> Self {
311        self.activity_type = Some(r#type);
312        self
313    }
314
315    /// Top text for your activity.
316    pub fn details(mut self, text: impl Into<String>) -> Self {
317        self.details = filter_none_string(text);
318        self
319    }
320
321    /// URL for the top text of your activity.
322    pub fn details_url(mut self, url: impl Into<String>) -> Self {
323        self.details_url = filter_none_string(url);
324        self
325    }
326
327    /// Bottom text for your activity.
328    pub fn state(mut self, text: impl Into<String>) -> Self {
329        self.state = filter_none_string(text);
330        self
331    }
332
333    /// URL for the bottom text of your activity.
334    pub fn state_url(mut self, url: impl Into<String>) -> Self {
335        self.state_url = filter_none_string(url);
336        self
337    }
338
339    /// Sets the activity to be an instance.
340    #[must_use]
341    pub fn set_as_instance(mut self) -> Self {
342        self.instance = Some(true);
343        self
344    }
345
346    /// The status display type for the activity.
347    #[must_use]
348    pub fn status_display_type(mut self, r#type: StatusDisplayType) -> Self {
349        self.status_display_type = Some(r#type);
350        self
351    }
352
353    /// Countdown duration for your activity.
354    #[must_use]
355    pub fn duration(mut self, duration: Duration) -> Self {
356        self.duration = Some(duration);
357        self
358    }
359
360    /// Add a button to the activity.
361    pub fn add_button(mut self, label: impl Into<String>, url: impl Into<String>) -> Self {
362        if let Some(btns) = &mut self.buttons {
363            btns.insert(label.into(), url.into());
364        } else {
365            let mut btns: HashMap<String, String> = HashMap::new();
366            btns.insert(label.into(), url.into());
367            self.buttons = Some(btns);
368        };
369
370        self
371    }
372
373    /// Large image for your activity (e.g., game icon).
374    pub fn large_image(mut self, key: impl Into<String>) -> Self {
375        self.large_image = Some(key.into());
376        self
377    }
378
379    /// Text for the large image of your activity.
380    pub fn large_text(mut self, text: impl Into<String>) -> Self {
381        self.large_text = Some(text.into());
382        self
383    }
384
385    /// URL for the large image of your activity.
386    pub fn large_url(mut self, url: impl Into<String>) -> Self {
387        self.large_url = Some(url.into());
388        self
389    }
390
391    /// Small image for your activity (e.g., game icon).
392    pub fn small_image(mut self, key: impl Into<String>) -> Self {
393        self.small_image = Some(key.into());
394        self
395    }
396
397    /// Text for the small image of your activity.
398    pub fn small_text(mut self, text: impl Into<String>) -> Self {
399        self.small_text = Some(text.into());
400        self
401    }
402
403    /// URL for the small image of your activity.
404    pub fn small_url(mut self, url: impl Into<String>) -> Self {
405        self.small_url = Some(url.into());
406        self
407    }
408
409    /// Parses the state of this builder into a usable [`Activity`] for you to pass through [`super::PresenceClient::set_activity`].
410    #[must_use]
411    pub fn build(self) -> Activity {
412        Activity {
413            name: self.name,
414            activity_type: self.activity_type,
415            status_display_type: self.status_display_type,
416            details: self.details,
417            details_url: self.details_url,
418            state: self.state,
419            state_url: self.state_url,
420            instance: self.instance,
421            duration: self.duration,
422            large_image: self.large_image,
423            large_text: self.large_text,
424            large_url: self.large_url,
425            small_image: self.small_image,
426            small_text: self.small_text,
427            small_url: self.small_url,
428            buttons: self.buttons,
429        }
430    }
431}