Skip to main content

filthy_rich/
ipc.rs

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