Skip to main content

filthy_rich/
ipc.rs

1use anyhow::Result;
2use tokio::task::JoinHandle;
3use uuid::Uuid;
4
5use crate::{socket::DiscordIPCSocket, utils::pack};
6
7/// Basic Discord rich presence IPC implementation.
8/// See the docs: https://docs.rs/crate/filthy-rich/latest
9pub struct DiscordIPC {
10    sock: DiscordIPCSocket,
11    ipc_task: Option<JoinHandle<Result<()>>>,
12    client_id: String,
13}
14
15impl DiscordIPC {
16    /// Given a client ID, create a new DiscordIPC instance.
17    /// Needs to have Discord running for successful execution.
18    pub async fn new_from(client_id: &str) -> Result<Self> {
19        let sock = DiscordIPCSocket::new().await?;
20
21        Ok(Self {
22            sock,
23            ipc_task: None,
24            client_id: client_id.to_string(),
25        })
26    }
27
28    /// Bare-bones implementation of handshake with the Discord IPC.
29    /// Use `.run()` instead.
30    pub async fn handshake(&mut self) -> Result<()> {
31        let json = format!(r#"{{"v":1,"client_id":"{}"}}"#, self.client_id);
32        let bytes = json.as_bytes();
33
34        let pack = pack(0u32, bytes.len() as u32)?;
35        self.sock.write(&pack).await?;
36        self.sock.write(bytes).await?;
37
38        Ok(())
39    }
40
41    /// Look out for READY in socket frames. Use `.run()` instead.
42    pub async fn wait_for_ready(&mut self) -> Result<()> {
43        loop {
44            let frame = self.sock.read_frame().await?;
45
46            if frame.opcode == 1 && frame.body.windows(5).any(|w| w == b"READY") {
47                break;
48            }
49        }
50        Ok(())
51    }
52
53    /// Convenience function for performing handshake, waiting for READY opcode
54    /// and handling the IPC response loop.
55    pub async fn run(&mut self) -> Result<()> {
56        if self.ipc_task.is_some() {
57            return Ok(());
58        }
59
60        self.handshake().await?;
61        self.wait_for_ready().await?;
62
63        let mut sock = self.sock.clone();
64        self.ipc_task = Some(tokio::spawn(async move { sock.handle_ipc().await }));
65
66        Ok(())
67    }
68
69    /// Waits for response from IPC task; can be used to run the client indefinitely.
70    pub async fn wait(&mut self) -> Result<()> {
71        if let Some(handle) = &mut self.ipc_task {
72            match handle.await {
73                Ok(res) => res?,
74                Err(e) if e.is_cancelled() => {}
75                Err(e) => return Err(e.into()),
76            }
77        }
78        Ok(())
79    }
80
81    /// Sets a tiny Discord rich presence activity.
82    pub async fn set_activity(&mut self, top: &str, bottom: &str) -> Result<()> {
83        let pid = std::process::id();
84        let uuid = Uuid::new_v4();
85
86        let json = format!(
87            r#"
88{{
89    "cmd":"SET_ACTIVITY",
90    "args": {{
91        "pid": {},
92        "activity": {{
93            "details":"{}",
94            "state":"{}"
95        }}
96    }},
97    "nonce":"{}"
98}}
99"#,
100            pid, top, bottom, uuid
101        );
102
103        let bytes = json.as_bytes();
104        let pack = pack(1u32, bytes.len() as u32)?;
105
106        self.sock.write(&pack).await?;
107        self.sock.write(bytes).await?;
108        Ok(())
109    }
110}