Skip to main content

filthy_rich/
ipc.rs

1// SPDX-License-Identifier: MIT
2
3use anyhow::Result;
4use tokio::{
5    runtime::{Builder, Runtime},
6    task::JoinHandle,
7};
8use uuid::Uuid;
9
10use crate::{
11    socket::DiscordIPCSocket,
12    utils::{get_current_timestamp, pack},
13};
14
15/// Blocking representation of DiscordIPC.
16pub struct DiscordIPCSync {
17    inner: DiscordIPC,
18    rt: Runtime,
19}
20
21impl DiscordIPCSync {
22    /// Given a client ID, create a new `DiscordIPCSync` instance.
23    /// Needs to have Discord running for successful execution.
24    ///
25    /// NOTE: Essentially a `DiscordIPC` instance but with blocking I/O.
26    pub fn new(client_id: &str) -> Result<Self> {
27        let rt = Builder::new_multi_thread().enable_all().build()?;
28        let inner = rt.block_on(DiscordIPC::new(client_id))?;
29        Ok(Self { inner, rt })
30    }
31
32    /// Blocking iteration of `DiscordIPC::run`
33    pub fn run(&mut self) -> Result<()> {
34        self.rt.block_on(self.inner.run())
35    }
36
37    /// Blocking iteration of `DiscordIPC::set_activity`
38    pub fn set_activity(&mut self, details: &str, state: &str) -> Result<()> {
39        self.rt.block_on(self.inner.set_activity(details, state))
40    }
41
42    /// Blocking iteration of `DiscordIPC::wait`
43    pub fn wait(&mut self) -> Result<()> {
44        self.rt.block_on(self.inner.wait())
45    }
46}
47
48/// Basic Discord rich presence IPC implementation.
49#[derive(Debug)]
50pub struct DiscordIPC {
51    sock: DiscordIPCSocket,
52    ipc_task: Option<JoinHandle<Result<()>>>,
53    timestamp: u64,
54    client_id: String,
55}
56
57impl DiscordIPC {
58    async fn send_json(&mut self, json: String, opcode: u32) -> Result<()> {
59        let bytes = json.as_bytes();
60
61        let packed = pack(opcode, bytes.len() as u32)?;
62        self.sock.write(&packed).await?;
63        self.sock.write(bytes).await?;
64
65        Ok(())
66    }
67
68    /// Given a client ID, create a new `DiscordIPC` instance.
69    /// Needs to have Discord running for successful execution.
70    pub async fn new(client_id: &str) -> Result<Self> {
71        let sock = DiscordIPCSocket::new().await?;
72
73        Ok(Self {
74            sock,
75            ipc_task: None,
76            timestamp: get_current_timestamp()?,
77            client_id: client_id.to_string(),
78        })
79    }
80
81    async fn handshake(&mut self) -> Result<()> {
82        let json = format!(r#"{{"v":1,"client_id":"{}"}}"#, self.client_id);
83        self.send_json(json, 0u32).await?;
84
85        Ok(())
86    }
87
88    async fn wait_for_ready(&mut self) -> Result<()> {
89        loop {
90            let frame = self.sock.read_frame().await?;
91
92            if frame.opcode == 1 && frame.body.windows(5).any(|w| w == b"READY") {
93                break;
94            }
95        }
96        Ok(())
97    }
98
99    /// Starts off the connection with Discord. This includes performing a handshake, waiting for READY and
100    /// starting the IPC response loop.
101    pub async fn run(&mut self) -> Result<()> {
102        if self.ipc_task.is_some() {
103            return Ok(());
104        }
105
106        self.handshake().await?;
107        self.wait_for_ready().await?;
108
109        let mut sock = self.sock.clone();
110        self.ipc_task = Some(tokio::spawn(async move { sock.handle_ipc().await }));
111
112        Ok(())
113    }
114
115    /// Waits for response from IPC task; can be used to run the client indefinitely.
116    pub async fn wait(&mut self) -> Result<()> {
117        if let Some(handle) = &mut self.ipc_task {
118            match handle.await {
119                Ok(res) => res?,
120                Err(e) if e.is_cancelled() => {}
121                Err(e) => return Err(e.into()),
122            }
123        }
124        Ok(())
125    }
126
127    /// Sets a tiny Discord rich presence activity.
128    pub async fn set_activity(&mut self, details: &str, state: &str) -> Result<()> {
129        let pid = std::process::id();
130        let uuid = Uuid::new_v4();
131
132        let json = format!(
133            r#"
134{{
135    "cmd":"SET_ACTIVITY",
136    "args": {{
137        "pid": {},
138        "activity": {{
139            "details":"{}",
140            "state":"{}",
141            "timestamps": {{
142                "start": {}
143            }}
144        }}
145    }},
146    "nonce":"{}"
147}}
148"#,
149            pid, details, state, self.timestamp, uuid
150        );
151
152        self.send_json(json, 1u32).await?;
153        Ok(())
154    }
155}