Skip to main content

filthy_rich/
ipc.rs

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