Skip to main content

filthy_rich/
types.rs

1//! Core types related to filthy-rich. Used in conjunction with the core imports.
2//!
3use serde::{Deserialize, Serialize, ser::SerializeStruct};
4use serde_json::Value;
5use std::{collections::HashMap, time::Duration};
6use uuid::Uuid;
7
8use crate::{errors::InnerParsingError, 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, InnerParsingError> {
30        serde_json::to_string(self).map_err(InnerParsingError::SerializeFailed)
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<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: 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    pub avatar_decoration_data: Option<Value>,
184    pub bot: bool,
185    pub flags: Option<u64>,
186    pub premium_type: Option<u64>,
187}
188
189/// Enum indicating the activity type.
190#[repr(u8)]
191#[derive(Clone, Debug, Eq, PartialEq, Copy)]
192pub enum ActivityType {
193    Playing = 0,
194    Listening = 2,
195    Watching = 3,
196    Competing = 5,
197}
198
199impl From<ActivityType> for u8 {
200    fn from(value: ActivityType) -> Self {
201        value as u8
202    }
203}
204
205/// Enum indicating which mode to use for indicating the status of an activity.
206#[repr(u8)]
207#[derive(Clone, Debug, Eq, PartialEq, Copy)]
208pub enum StatusDisplayType {
209    Name = 0,
210    Details = 2,
211    State = 1,
212}
213
214impl From<StatusDisplayType> for u8 {
215    fn from(value: StatusDisplayType) -> Self {
216        value as u8
217    }
218}
219
220/// Represents a Discord Rich Presence activity.
221#[derive(Debug, Clone)]
222pub struct Activity {
223    pub(crate) name: Option<String>,
224    pub(crate) activity_type: Option<ActivityType>,
225    pub(crate) status_display_type: Option<StatusDisplayType>,
226    pub(crate) details: Option<String>,
227    pub(crate) details_url: Option<String>,
228    pub(crate) state: Option<String>,
229    pub(crate) state_url: Option<String>,
230    pub(crate) instance: Option<bool>,
231    pub(crate) duration: Option<Duration>,
232    pub(crate) large_image: Option<String>,
233    pub(crate) large_text: Option<String>,
234    pub(crate) large_url: Option<String>,
235    pub(crate) small_image: Option<String>,
236    pub(crate) small_text: Option<String>,
237    pub(crate) small_url: Option<String>,
238    pub(crate) buttons: Option<HashMap<String, String>>,
239}
240
241impl Activity {
242    /// Initializes a new activity builder instance.
243    #[must_use]
244    #[allow(clippy::new_ret_no_self)]
245    pub fn new() -> ActivityBuilder {
246        ActivityBuilder::default()
247    }
248}
249
250impl Default for Activity {
251    /// Gives out an empty [`Activity`] with all of the default values. Essentially,
252    /// this only shows the name of the app and the elapsed time for the activity on
253    /// Discord. Useful when you only need a simple rich presence instance.
254    ///
255    /// For building a complete activity, using [`Activity::new`] is suggested instead.
256    fn default() -> Self {
257        Self {
258            name: None,
259            activity_type: None,
260            status_display_type: None,
261            details: None,
262            details_url: None,
263            state: None,
264            state_url: None,
265            instance: None,
266            duration: None,
267            large_image: None,
268            large_text: None,
269            large_url: None,
270            small_image: None,
271            small_text: None,
272            small_url: None,
273            buttons: None,
274        }
275    }
276}
277
278/// A builder for a Rich Presence activity.
279/// To build an [`Activity`] out of it, use [`ActivityBuilder::build`].
280#[derive(Default)]
281pub struct ActivityBuilder {
282    name: Option<String>,
283    activity_type: Option<ActivityType>,
284    status_display_type: Option<StatusDisplayType>,
285    instance: Option<bool>,
286    details: Option<String>,
287    details_url: Option<String>,
288    state: Option<String>,
289    state_url: Option<String>,
290    duration: Option<Duration>,
291    large_image: Option<String>,
292    large_text: Option<String>,
293    large_url: Option<String>,
294    small_image: Option<String>,
295    small_text: Option<String>,
296    small_url: Option<String>,
297    buttons: Option<HashMap<String, String>>,
298}
299
300impl ActivityBuilder {
301    /// Name of the activity.
302    pub fn name(mut self, text: impl Into<String>) -> Self {
303        self.name = filter_none_string(text);
304        self
305    }
306
307    /// The type of activity you want to create.
308    #[must_use]
309    pub fn activity_type(mut self, r#type: ActivityType) -> Self {
310        self.activity_type = Some(r#type);
311        self
312    }
313
314    /// Top text for your activity.
315    pub fn details(mut self, text: impl Into<String>) -> Self {
316        self.details = filter_none_string(text);
317        self
318    }
319
320    /// URL for the top text of your activity.
321    pub fn details_url(mut self, url: impl Into<String>) -> Self {
322        self.details_url = filter_none_string(url);
323        self
324    }
325
326    /// Bottom text for your activity.
327    pub fn state(mut self, text: impl Into<String>) -> Self {
328        self.state = filter_none_string(text);
329        self
330    }
331
332    /// URL for the bottom text of your activity.
333    pub fn state_url(mut self, url: impl Into<String>) -> Self {
334        self.state_url = filter_none_string(url);
335        self
336    }
337
338    /// Sets the activity to be an instance.
339    #[must_use]
340    pub fn set_as_instance(mut self) -> Self {
341        self.instance = Some(true);
342        self
343    }
344
345    /// The status display type for the activity.
346    #[must_use]
347    pub fn status_display_type(mut self, r#type: StatusDisplayType) -> Self {
348        self.status_display_type = Some(r#type);
349        self
350    }
351
352    /// Countdown duration for your activity.
353    #[must_use]
354    pub fn duration(mut self, duration: Duration) -> Self {
355        self.duration = Some(duration);
356        self
357    }
358
359    /// Adds a button to the activity.
360    ///
361    /// NOTE: The Discord desktop client may behave in such a way that the buttons may only be visible from anyone but the
362    /// connected user's side. This is a wonky feature and must be used with care.
363    pub fn add_button(mut self, label: impl Into<String>, url: impl Into<String>) -> Self {
364        if let Some(btns) = &mut self.buttons {
365            btns.insert(label.into(), url.into());
366        } else {
367            let mut btns: HashMap<String, String> = HashMap::new();
368            btns.insert(label.into(), url.into());
369            self.buttons = Some(btns);
370        };
371
372        self
373    }
374
375    /// Large image for your activity (e.g., game icon).
376    pub fn large_image(mut self, key: impl Into<String>) -> Self {
377        self.large_image = Some(key.into());
378        self
379    }
380
381    /// Text for the large image of your activity.
382    pub fn large_text(mut self, text: impl Into<String>) -> Self {
383        self.large_text = Some(text.into());
384        self
385    }
386
387    /// URL for the large image of your activity.
388    pub fn large_url(mut self, url: impl Into<String>) -> Self {
389        self.large_url = Some(url.into());
390        self
391    }
392
393    /// Small image for your activity (e.g., game icon).
394    pub fn small_image(mut self, key: impl Into<String>) -> Self {
395        self.small_image = Some(key.into());
396        self
397    }
398
399    /// Text for the small image of your activity.
400    pub fn small_text(mut self, text: impl Into<String>) -> Self {
401        self.small_text = Some(text.into());
402        self
403    }
404
405    /// URL for the small image of your activity.
406    pub fn small_url(mut self, url: impl Into<String>) -> Self {
407        self.small_url = Some(url.into());
408        self
409    }
410
411    /// Parses the state of this builder into a usable [`Activity`] for you to pass through [`super::PresenceClient::set_activity`].
412    #[must_use]
413    pub fn build(self) -> Activity {
414        Activity {
415            name: self.name,
416            activity_type: self.activity_type,
417            status_display_type: self.status_display_type,
418            details: self.details,
419            details_url: self.details_url,
420            state: self.state,
421            state_url: self.state_url,
422            instance: self.instance,
423            duration: self.duration,
424            large_image: self.large_image,
425            large_text: self.large_text,
426            large_url: self.large_url,
427            small_image: self.small_image,
428            small_text: self.small_text,
429            small_url: self.small_url,
430            buttons: self.buttons,
431        }
432    }
433}