Skip to main content

filthy_rich/types/
builder.rs

1use std::{collections::HashMap, time::Duration};
2
3use crate::{
4    errors::ActivitySpecBuildError,
5    nf,
6    types::{
7        ActivitySpec, ActivityType, StatusDisplayType,
8        payloads::{AssetsPayload, ButtonPayload},
9    },
10};
11
12/// Represents a Discord Rich Presence activity which is yet to be built. To start building it into a usable [`ActivitySpec`],
13/// initialize a new [`ActivityBuilder`] with [`Activity::new`].
14pub struct Activity;
15
16impl Activity {
17    /// Initializes a new activity builder instance.
18    #[must_use]
19    #[allow(clippy::new_ret_no_self)]
20    pub fn new() -> ActivityBuilder {
21        ActivityBuilder::default()
22    }
23
24    /// Gives out an empty but usable [`ActivitySpec`]. Essentially,
25    /// this only shows the name of the app and the elapsed time for the activity on
26    /// Discord. Useful when you only need a simple rich presence instance.
27    ///
28    /// For building a complete activity, using [`Activity::new`] is suggested instead.
29    ///
30    /// NOTE: This is the same as calling [`ActivitySpec::default`].
31    #[must_use]
32    pub fn empty_spec() -> ActivitySpec {
33        ActivitySpec::default()
34    }
35}
36
37/// A builder for a Rich Presence activity.
38/// To build a [`ActivitySpec`] out of it, use [`ActivityBuilder::build`].
39#[derive(Default)]
40pub struct ActivityBuilder {
41    name: Option<String>,
42    activity_type: Option<ActivityType>,
43    status_display_type: Option<StatusDisplayType>,
44    instance: Option<bool>,
45    details: Option<String>,
46    details_url: Option<String>,
47    state: Option<String>,
48    state_url: Option<String>,
49    duration: Option<Duration>,
50    large_image: Option<String>,
51    large_text: Option<String>,
52    large_url: Option<String>,
53    small_image: Option<String>,
54    small_text: Option<String>,
55    small_url: Option<String>,
56    buttons: Option<HashMap<String, String>>,
57}
58
59impl ActivityBuilder {
60    nf!(name, "Name of the activity.", name);
61    nf!(details, "Top text for your activity.", text);
62    nf!(details_url, "URL for the top text of your activity.", url);
63    nf!(
64        state,
65        "Bottom text (top if field `details` is missing) for your activity.",
66        text
67    );
68    nf!(state_url, "URL for the state of your activity.", url);
69    nf!(
70        large_image,
71        "Large image for your activity (e.g. game icon)",
72        key
73    );
74    nf!(
75        large_text,
76        "Text for the large image of your activity.",
77        text
78    );
79    nf!(large_url, "URL for the large image of your activity.", url);
80    nf!(
81        small_image,
82        "Small image for your activity (e.g. game icon)",
83        key
84    );
85    nf!(
86        small_text,
87        "Text for the small image of your activity.",
88        text
89    );
90    nf!(small_url, "URL for the small image of your activity.", url);
91
92    /// The type of activity you want to create.
93    #[must_use]
94    pub fn activity_type(mut self, r#type: ActivityType) -> Self {
95        self.activity_type = Some(r#type);
96        self
97    }
98
99    /// Sets the activity to be an instance.
100    #[must_use]
101    pub fn set_as_instance(mut self) -> Self {
102        self.instance = Some(true);
103        self
104    }
105
106    /// The status display type for the activity.
107    #[must_use]
108    pub fn status_display_type(mut self, r#type: StatusDisplayType) -> Self {
109        self.status_display_type = Some(r#type);
110        self
111    }
112
113    /// Countdown duration for your activity.
114    #[must_use]
115    pub fn duration(mut self, duration: Duration) -> Self {
116        self.duration = Some(duration);
117        self
118    }
119
120    /// Adds a button to the activity.
121    ///
122    /// NOTE: The Discord desktop client may behave in such a way that the buttons may only be visible from anyone but the
123    /// connected user's side. This is a wonky feature and must be used with care.
124    pub fn add_button(mut self, label: impl Into<String>, url: impl Into<String>) -> Self {
125        if let Some(btns) = &mut self.buttons {
126            btns.insert(label.into(), url.into());
127        } else {
128            let mut btns: HashMap<String, String> = HashMap::new();
129            btns.insert(label.into(), url.into());
130            self.buttons = Some(btns);
131        };
132
133        self
134    }
135
136    /// Parses the state of this builder into a usable [`ActivitySpec`] for you to pass through [`crate::PresenceClient::set_activity`].
137    pub fn build(self) -> Result<ActivitySpec, ActivitySpecBuildError> {
138        if (self.large_image.is_none() && (self.large_text.is_some() || self.large_url.is_some()))
139            || (self.small_image.is_none()
140                && (self.small_text.is_some() || self.small_url.is_some()))
141        {
142            return Err(ActivitySpecBuildError::ImageAssetsTooEarly);
143        }
144
145        if self.details.is_none() && self.details_url.is_some() {
146            return Err(ActivitySpecBuildError::ElementURLProvidedEarly("details"));
147        } else if self.state.is_none() && self.state_url.is_some() {
148            return Err(ActivitySpecBuildError::ElementURLProvidedEarly("state"));
149        }
150
151        if let Some(s) = self.status_display_type {
152            match s {
153                StatusDisplayType::Details if self.details.is_none() => {
154                    return Err(ActivitySpecBuildError::StatusDisplayElementMissing(
155                        "details",
156                    ));
157                }
158                StatusDisplayType::State if self.state.is_none() => {
159                    return Err(ActivitySpecBuildError::StatusDisplayElementMissing("state"));
160                }
161                _ => {}
162            }
163        }
164
165        Ok(ActivitySpec {
166            name: self.name,
167            r#type: self.activity_type,
168            status_display_type: self.status_display_type,
169            details: self.details,
170            details_url: self.details_url,
171            state: self.state,
172            state_url: self.state_url,
173            instance: self.instance,
174            assets: Some(AssetsPayload {
175                large_image: self.large_image,
176                large_url: self.large_url,
177                large_text: self.large_text,
178                small_image: self.small_image,
179                small_text: self.small_text,
180                small_url: self.small_url,
181            }),
182            buttons: self.buttons.map(|btns| {
183                btns.into_iter()
184                    .map(|f| ButtonPayload {
185                        label: f.0,
186                        url: f.1,
187                    })
188                    .collect()
189            }),
190            duration: self.duration,
191        })
192    }
193}