1use 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#[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
79impl 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#[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#[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#[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#[derive(Debug, Clone, Deserialize)]
171pub struct ReadyData {
172 pub user: DiscordUser,
173}
174
175#[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<serde_json::Value>,
185 pub bot: bool,
186 pub flags: Option<u64>,
187 pub premium_type: Option<u64>,
188}
189
190#[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#[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#[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 #[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 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#[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 pub fn name(mut self, text: impl Into<String>) -> Self {
304 self.name = filter_none_string(text);
305 self
306 }
307
308 #[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 pub fn details(mut self, text: impl Into<String>) -> Self {
317 self.details = filter_none_string(text);
318 self
319 }
320
321 pub fn details_url(mut self, url: impl Into<String>) -> Self {
323 self.details_url = filter_none_string(url);
324 self
325 }
326
327 pub fn state(mut self, text: impl Into<String>) -> Self {
329 self.state = filter_none_string(text);
330 self
331 }
332
333 pub fn state_url(mut self, url: impl Into<String>) -> Self {
335 self.state_url = filter_none_string(url);
336 self
337 }
338
339 #[must_use]
341 pub fn set_as_instance(mut self) -> Self {
342 self.instance = Some(true);
343 self
344 }
345
346 #[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 #[must_use]
355 pub fn duration(mut self, duration: Duration) -> Self {
356 self.duration = Some(duration);
357 self
358 }
359
360 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 pub fn large_image(mut self, key: impl Into<String>) -> Self {
375 self.large_image = Some(key.into());
376 self
377 }
378
379 pub fn large_text(mut self, text: impl Into<String>) -> Self {
381 self.large_text = Some(text.into());
382 self
383 }
384
385 pub fn large_url(mut self, url: impl Into<String>) -> Self {
387 self.large_url = Some(url.into());
388 self
389 }
390
391 pub fn small_image(mut self, key: impl Into<String>) -> Self {
393 self.small_image = Some(key.into());
394 self
395 }
396
397 pub fn small_text(mut self, text: impl Into<String>) -> Self {
399 self.small_text = Some(text.into());
400 self
401 }
402
403 pub fn small_url(mut self, url: impl Into<String>) -> Self {
405 self.small_url = Some(url.into());
406 self
407 }
408
409 #[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}