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_unix, 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.
49pub struct DiscordIPC {
50    sock: DiscordIPCSocket,
51    ipc_task: Option<JoinHandle<Result<()>>>,
52    timestamp: u64,
53    client_id: String,
54}
55
56impl DiscordIPC {
57    async fn send_json(&mut self, json: String, opcode: u32) -> Result<()> {
58        let bytes = json.as_bytes();
59
60        let packed = pack(opcode, bytes.len() as u32)?;
61        self.sock.write(&packed).await?;
62        self.sock.write(bytes).await?;
63
64        Ok(())
65    }
66
67    /// Given a client ID, create a new `DiscordIPC` instance.
68    /// Needs to have Discord running for successful execution.
69    pub async fn new(client_id: &str) -> Result<Self> {
70        let sock = DiscordIPCSocket::new().await?;
71
72        Ok(Self {
73            sock,
74            ipc_task: None,
75            timestamp: get_current_timestamp_unix()?,
76            client_id: client_id.to_string(),
77        })
78    }
79
80    async fn handshake(&mut self) -> Result<()> {
81        let json = format!(r#"{{"v":1,"client_id":"{}"}}"#, self.client_id);
82        self.send_json(json, 0u32).await?;
83
84        Ok(())
85    }
86
87    async fn wait_for_ready(&mut self) -> Result<()> {
88        loop {
89            let frame = self.sock.read_frame().await?;
90
91            if frame.opcode == 1 && frame.body.windows(5).any(|w| w == b"READY") {
92                break;
93            }
94        }
95        Ok(())
96    }
97
98    /// Starts off the connection with Discord. This includes performing a handshake, waiting for READY and
99    /// starting the IPC response loop.
100    pub async fn run(&mut self) -> Result<()> {
101        if self.ipc_task.is_some() {
102            return Ok(());
103        }
104
105        self.handshake().await?;
106        self.wait_for_ready().await?;
107
108        let mut sock = self.sock.clone();
109        self.ipc_task = Some(tokio::spawn(async move { sock.handle_ipc().await }));
110
111        Ok(())
112    }
113
114    /// Waits for response from IPC task; can be used to run the client indefinitely.
115    pub async fn wait(&mut self) -> Result<()> {
116        if let Some(handle) = &mut self.ipc_task {
117            match handle.await {
118                Ok(res) => res?,
119                Err(e) if e.is_cancelled() => {}
120                Err(e) => return Err(e.into()),
121            }
122        }
123        Ok(())
124    }
125
126    /// Sets a tiny Discord rich presence activity.
127    pub async fn set_activity(&mut self, details: &str, state: &str) -> Result<()> {
128        let pid = std::process::id();
129        let uuid = Uuid::new_v4();
130
131        let json = format!(
132            r#"
133{{
134    "cmd":"SET_ACTIVITY",
135    "args": {{
136        "pid": {},
137        "activity": {{
138            "details":"{}",
139            "state":"{}",
140            "timestamps": {{
141                "start": {}
142            }}
143        }}
144    }},
145    "nonce":"{}"
146}}
147"#,
148            pid, details, state, self.timestamp, uuid
149        );
150
151        self.send_json(json, 1u32).await?;
152        Ok(())
153    }
154}