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

mod client;
mod entities;
mod error;

pub use client::InnerClient;
pub use entities::activity;
pub use entities::packet;
pub use error::{BadResponseError, Error};
pub type Result<T> = std::result::Result<T, Error>;

use client::CallbackFn;
use packet::Packet;
use std::sync::Arc;

pub trait ClientType {}
impl ClientType for InnerClient {}
impl ClientType for Arc<InnerClient> {}

/// A client for interacting with Discord's IPC (Inter-Process Communication).
///
/// ## Platform-Specific Behavior
/// - On **Windows**, the connection is established using a named pipe (`File`).
/// - On **Unix-based systems**, the connection is handled via a Unix domain socket (`UnixStream`).
///
/// ## Client Types
/// 1. **Simple Client** (created with [`Client::new_simple`]):
///    - [`Client::connect_and_wait`]: Blocking method that sends a connection request, waits for a direct response, and returns the response packet.
///    - [`Client::send_and_wait`]: Blocking method that sends a packet and waits for a direct response.
///    - [`Client::disconnect`]: Closes the IPC connection.
///
/// **Note:** The simple client does not configure any listeners or spawn any threads.
///
/// 2. **Full Client** (created with [`Client::new`]):
///    - [`Client::connect`]: Establishes the IPC connection and starts listening for incoming messages.
///    - [`Client::connect_and_wait`]: Blocking version that waits for the response packet.
///    - [`Client::send`]: Sends a packet to Discord.
///    - [`Client::send_and_wait`]: Blocking method that sends a packet and waits for a direct response.
///    - [`Client::disconnect`]: Closes the IPC connection.
///    - [`Client::on`]: Sets up a callback for an event, nonce or opcode.
///    - [`Client::once`]: Sets up a one-time callback for an event, nonce or opcode.
///
/// **Note:** A listener thread is created every time [`Client::connect`] or [`Client::connect_and_wait`] are called.  
///  
/// The thread terminates if the connection is interrupted.
pub struct Client<T: ClientType>(T);

impl Client<InnerClient> {
    /// Creates a new simple instance of [Client] with the specified client ID.
    pub fn new_simple(client_id: impl Into<String>) -> Self {
        Self(InnerClient::new(client_id))
    }

    /// Attempts to open a pipe and establish a connection to Discord's IPC by sending a handshake and waiting for a response.
    ///
    /// ## Errors
    /// This function returns an error in the following cases:
    /// - **NoAvailablePipes**: No valid IPC pipe or socket was found (it usually means that Discord isn't open).
    /// - **IoError**: An I/O error occurred while sending data.
    /// - **DecodeError**: The response is malformed, incomplete, or cannot be decoded.
    ///
    /// ## Example
    /// ```rust
    /// use discordipc::Client;
    ///
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::new("<application_id>");
    ///
    ///     match client.connect_and_wait()?.filter() {
    ///         Ok(_) => println!("Connected"),
    ///         Err(e) => eprintln!("Couldn't connect: {}", e),
    ///     };
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn connect_and_wait(&self) -> Result<Packet> {
        self.0.connect_and_wait(None)
    }

    /// 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> {
        self.0.send_and_wait(packet)
    }

    /// 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.0.disconnect()
    }
}
impl Client<Arc<InnerClient>> {
    /// Creates a new full instance of [Client] with the specified client ID.
    pub fn new(client_id: impl Into<String>) -> Self {
        Self(Arc::new(InnerClient::new(client_id)))
    }

    /// 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) {
        self.0.set_workers(num_threads)
    }

    /// Attempts to open a pipe and establish a connection to Discord's IPC by sending a handshake.
    ///
    /// ## Errors
    /// This function returns an error in the following cases:
    /// - **NoAvailablePipes**: No valid IPC pipe or socket was found (it usually means that Discord isn't open).
    /// - **IoError**: An I/O error occurred while sending data.
    ///
    /// ## Example
    /// ```rust
    /// use discordipc::Client;
    ///
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::new("<application_id>");
    ///     client.on("READY", |_, _| {
    ///         println!("Connected");
    ///     });
    ///     client.connect()?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn connect(&self) -> Result<()> {
        self.0.connect(Arc::clone(&self.0))
    }

    /// Attempts to open a pipe and establish a connection to Discord's IPC by sending a handshake and waiting for a response.
    ///
    /// ## Errors
    /// This function returns an error in the following cases:
    /// - **NoAvailablePipes**: No valid IPC pipe or socket was found (it usually means that Discord isn't open).
    /// - **IoError**: An I/O error occurred while sending data.
    /// - **DecodeError**: The response is malformed, incomplete, or cannot be decoded.
    ///
    /// ## Example
    /// ```rust
    /// use discordipc::Client;
    ///
    /// fn main() -> Result<(), Box<dyn std::error::Error>> {
    ///     let client = Client::new("<application_id>");
    ///
    ///     match client.connect_and_wait()?.filter() {
    ///         Ok(_) => println!("Connected"),
    ///         Err(e) => eprintln!("Couldn't connect: {}", e),
    ///     };
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn connect_and_wait(&self) -> Result<Packet> {
        let client = Arc::clone(&self.0);
        self.0.connect_and_wait(Some(client))
    }

    /// 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 to the IPC pipe.
    pub fn send(&self, packet: Packet) -> Result<()> {
        self.0.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> {
        self.0.send_and_wait(packet)
    }

    /// 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.0.disconnect()
    }

    /// 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, "")).unwrap(); // Send a ping
    ///     });
    ///
    ///     client.on(Opcode::Pong, |_, _| println!("Pong!"));
    ///
    ///     client.connect_and_wait()?.filter()?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn on(&self, id: impl Into<String>, callback: impl CallbackFn) {
        self.0.on(id, 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::{activity::Activity, packet::Packet, Client};
    ///
    /// 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 sent");
    ///         });
    ///
    ///         client.send(packet).unwrap();
    ///     });
    ///
    ///     client.connect()?;
    ///
    ///     Ok(())
    /// }
    /// ```
    pub fn once(&self, id: impl Into<String>, callback: impl CallbackFn) {
        self.0.once(id, callback)
    }
}