filthy_rich/
ipc.rs

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