Skip to main content

filthy_rich/
ipc.rs

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