discordipc 0.1.1

A Rust crate that enables connection and interaction with Discord's IPC, allowing you to set custom activities for your project.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0

use crate::{
    entities::{
        packet::{Opcode, Packet},
        pipe::Pipe,
    },
    Error, Result,
};
use serde_json::json;
use std::{
    collections::HashMap,
    sync::{Arc, Mutex},
    thread,
    time::Duration,
};
use threadpool::ThreadPool;

pub struct InnerClient {
    client_id: String,
    pipe: Mutex<Pipe>,
    callbacks: Mutex<HashMap<String, Callback>>,
    workers: Mutex<ThreadPool>,
}
impl InnerClient {
    pub fn new(client_id: impl Into<String>) -> Self {
        Self {
            client_id: client_id.into(),
            pipe: Mutex::new(Pipe(None)),
            callbacks: Mutex::new(HashMap::new()),
            workers: Mutex::new(ThreadPool::new(1)),
        }
    }

    /// Sets the number of worker threads processing Discord IPC callbacks (default to 1).
    ///
    /// ## Note
    /// - This function has no effect if `num_threads` is `0` or greater than `10`.
    pub fn set_workers(&self, num_threads: usize) {
        if num_threads != 0 && num_threads <= 10 {
            let mut workers = self.workers.lock().unwrap();
            workers.set_num_threads(num_threads);
        }
    }

    /// Listens for incoming packets from the Discord IPC connection and handles them.
    /// This function spawns a thread that continuously checks for incoming data.
    fn listen(&self, client: Arc<InnerClient>) {
        thread::spawn(move || loop {
            thread::sleep(Duration::from_millis(100));

            match client.pipe.lock().unwrap().try_receive() {
                Ok(incoming_packet) => client.handle(incoming_packet, Arc::clone(&client)),
                Err(e) => {
                    if let Error::IoError(io_err) = &e {
                        match io_err.kind() {
                            std::io::ErrorKind::WouldBlock => continue,
                            std::io::ErrorKind::BrokenPipe => break,
                            _ => (),
                        }
                    } else if let Error::NoPipe = e {
                        break;
                    }
                }
            }
        });
    }

    pub fn connect(&self, client: Arc<InnerClient>) -> Result<()> {
        self.pipe.lock().unwrap().open()?;
        self.listen(client);
        let handshake = json!({"v": 1, "client_id": self.client_id});
        self.send(Packet::new(Opcode::Handshake, handshake))
    }

    pub fn connect_and_wait(&self, client: Option<Arc<InnerClient>>) -> Result<Packet> {
        self.pipe.lock().unwrap().open()?;
        if let Some(client) = client {
            self.listen(client);
        };
        let handshake = json!({"v": 1, "client_id": self.client_id});
        self.send_and_wait(Packet::new(Opcode::Handshake, handshake))
    }

    /// Sends a packet to the active IPC pipe.
    ///
    /// ## Errors
    /// This function can return an error in the following cases:
    /// - **NoPipe**: No active IPC connection exists.
    /// - **IoError**: An I/O error occurred while writing into the IPC pipe.
    pub fn send(&self, packet: Packet) -> Result<()> {
        self.pipe.lock().unwrap().send(packet)
    }

    /// Sends a packet to the active IPC pipe and waits for a response.
    ///
    /// ## Errors
    /// This function can return an error in the following cases:
    /// - **NoPipe**: No active IPC connection exists.
    /// - **IoError**: An I/O error occurred while writing to the IPC pipe.
    /// - **DecodeError**: The response is malformed, incomplete, or cannot be decoded.
    pub fn send_and_wait(&self, packet: Packet) -> Result<Packet> {
        let mut pipe = self.pipe.lock().unwrap();
        pipe.send(packet)?;

        loop {
            thread::sleep(Duration::from_millis(100));
            match pipe.try_receive() {
                Ok(packet) => return Ok(packet),
                Err(Error::IoError(e)) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
                Err(e) => return Err(e),
            };
        }
    }

    /// Closes the Discord IPC connection.
    ///
    /// ## Errors
    /// This function returns an error in the following cases:
    /// - **NoPipe**: No active IPC connection exists.
    /// - **IoError**: An I/O error occurred while flushing or shutting down the connection.
    pub fn disconnect(&self) -> Result<()> {
        self.pipe.lock().unwrap().close()
    }
}

// === Event handler ===
pub trait CallbackFn: Fn(Arc<InnerClient>, Packet) + Send + 'static + Sync {}
impl<T: Fn(Arc<InnerClient>, Packet) + Send + 'static + Sync> CallbackFn for T {}

enum Callback {
    On(Arc<dyn CallbackFn>),
    Once(Box<dyn CallbackFn>),
}

impl InnerClient {
    /// Registers a callback that will be invoked whenever the specified id is triggered.
    /// The callback will be called every time the event occurs.
    ///
    /// ## Parameters
    /// - `id`: A unique identifier for the event, such as:
    ///   - A predefined event like `"READY"`.
    ///   - A nonce.
    ///   - An [Opcode](crate::entities::packet::Opcode).
    /// - `callback`: A function or closure that will be executed when the event is triggered. The callback
    ///   must take two parameters:
    ///   - `client`: A reference to the [`Client`]
    ///   - `packet`: The received [`Packet`]
    ///
    /// ## Note
    /// If the callback `id` was already registered, it will overwrite the existing one.
    ///
    /// ## Example
    /// ```rust
    /// use discordipc::{Client, packet::{Opcode, Packet}};
    ///
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::new("<application_id>");
    ///
    ///     client.on("READY", |client, packet| {
    ///         println!("Connected");    
    ///         client.send(Packet::new(Opcode::Ping, "")); // Send a ping
    ///     });
    ///
    ///     client.on(Opcode::Pong, |_, _| {
    ///         println!("Pong!");    
    ///     });
    ///
    ///     client.connect()?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn on(&self, id: impl Into<String>, callback: impl CallbackFn) {
        let callback = Callback::On(Arc::new(callback));
        self.callbacks.lock().unwrap().insert(id.into(), callback);
    }

    /// Registers a one-time callback that will be invoked only once when the specified id is triggered.
    /// After the callback is invoked, it will be removed from the list.
    ///
    /// ## Parameters
    /// - `id`: A unique identifier for the event, such as:
    ///   - A predefined event like `"READY"`.
    ///   - A nonce.
    ///   - An [Opcode](crate::entities::packet::Opcode).
    /// - `callback`: A function or closure that will be executed when the event is triggered. The callback
    ///   must take two parameters:
    ///   - `client`: A reference to the [`Client`]
    ///   - `packet`: The received [`Packet`]
    ///
    /// ## Note
    /// If the callback `id` was already registered, it will overwrite the existing one.
    ///
    /// ## Example
    /// ```rust
    /// use discordipc::{Client, packet::{Opcode, Packet}, activity::Activity};
    ///
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::new("<application_id>");
    ///
    ///     client.on("READY", move |client, _| {
    ///         let initial_activity = Activity::new().details("test");
    ///         let nonce = Packet::generate_nonce();
    ///         let packet = Packet::new_activity(Some(&initial_activity), Some(&nonce));
    ///
    ///         client.once(nonce, |_, response| {
    ///             // This callback will be triggered only once
    ///             println!("Activity set");
    ///         });
    ///     });
    ///
    ///     client.connect()?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn once(&self, id: impl Into<String>, callback: impl CallbackFn) {
        let callback = Callback::Once(Box::new(callback));
        self.callbacks.lock().unwrap().insert(id.into(), callback);
    }

    /// Processes an incoming packet by extracting its opcode, event, and `nonce`,
    /// then invoking the corresponding callback.
    fn handle(&self, packet: Packet, client: Arc<InnerClient>) {
        if let Some(event) = packet.payload.get("evt").and_then(|v| v.as_str()) {
            self.trigger(event, Arc::clone(&client), packet.clone());
        }
        if let Some(nonce) = packet.payload.get("nonce").and_then(|v| v.as_str()) {
            self.trigger(nonce, Arc::clone(&client), packet.clone());
        }

        let opcode = packet.opcode.to_string();
        self.trigger(&opcode, client, packet);
    }

    /// Triggers the callback associated with the provided callback id, executing it in a worker thread.
    ///  
    /// If the callback is [`Callback::Once`], it runs once and is then removed.
    ///  
    /// If the callback is [`Callback::On`], it remains registered for future packets.
    fn trigger(&self, callback_id: &str, client: Arc<InnerClient>, packet: Packet) {
        let callback = {
            let mut callbacks = self.callbacks.lock().unwrap();
            if let Some(cb) = callbacks.remove(callback_id) {
                cb
            } else {
                match callbacks.remove("_unhandled") {
                    Some(cb) => cb,
                    None => return,
                }
            }
        };

        let workers = self.workers.lock().unwrap();
        match callback {
            Callback::Once(cb) => workers.execute(move || cb(client, packet)),
            Callback::On(cb) => {
                let cb_on = Arc::clone(&cb);
                workers.execute(move || cb_on(client, packet));

                let mut callbacks = self.callbacks.lock().unwrap();
                callbacks.insert(callback_id.to_string(), Callback::On(cb));
            }
        }
    }
}