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::{activity::Activity, BadResponseError, Error, Result};
use serde::Serialize;
use serde_json::{json, Value};

const MAX_PAYLOAD_SIZE: u32 = 2048; //2KB

/// Represents a Discord IPC message packet.
#[derive(Debug, Clone, Serialize)]
pub struct Packet {
    pub opcode: Opcode,
    pub payload: Value,
}
impl Packet {
    /// Creates a new packet with the given opcode and payload.
    ///
    /// ## Arguments
    /// - `opcode`: The [Opcode] for the packet.
    /// - `payload`: Data that can be converted into a JSON `Value`.
    pub fn new(opcode: Opcode, payload: impl Into<Value>) -> Self {
        Self {
            opcode,
            payload: payload.into(),
        }
    }

    /// Creates a new activity packet for Discord IPC.
    ///
    /// ## Arguments
    /// - `activity`: Optional [Activity]. Pass `None` to clear.
    /// - `nonce`: Optional nonce string.
    pub fn new_activity(activity: Option<&Activity>, nonce: Option<&str>) -> Self {
        let activity = json!({
            "cmd": "SET_ACTIVITY",
            "args": {
                "pid": std::process::id(),
                "activity": activity
            },
            "nonce": nonce.map(|v| v.to_string()).unwrap_or_else(Self::generate_nonce)
        });

        Self::new(Opcode::Frame, activity)
    }

    /// Generates a unique nonce using UUID v4.
    pub fn generate_nonce() -> String {
        uuid::Uuid::new_v4().to_string()
    }

    /// Decodes a Discord IPC response header.
    ///
    /// ## Errors
    /// - **DecodeError**: If the header is malformed, incomplete, or cannot be decoded.
    pub fn decode_header(header: &[u8; 8]) -> Result<(u32, u32)> {
        let opcode = u32::from_le_bytes(
            header[0..4]
                .try_into()
                .map_err(|_| Error::DecodeError("Invalid header length".to_string()))?,
        );

        let payload_len = u32::from_le_bytes(
            header[4..8]
                .try_into()
                .map_err(|_| Error::DecodeError("Invalid payload length".to_string()))?,
        );

        if payload_len > MAX_PAYLOAD_SIZE {
            return Err(Error::DecodeError(format!(
                "Payload exceeds {} bytes: {}",
                MAX_PAYLOAD_SIZE, payload_len
            )));
        }

        Ok((opcode, payload_len))
    }

    /// Checks for errors in a Discord IPC response payload.
    ///
    /// ## Returns
    /// - `Ok(Packet)` if no error is found.
    /// - `Err(BadResponseError)` if there is an error.
    ///
    /// See [BadResponseError]
    pub fn filter(self) -> std::result::Result<Packet, BadResponseError> {
        let get_err_message = |v: &Value| -> Option<String> {
            let code = v.get("code")?;
            let message = v.get("message")?;
            Some(format!("({}) {}", code, message))
        };

        if self.payload.get("evt") == Some(&Value::from("ERROR")) {
            if let Some(data) = self.payload.get("data") {
                if let Some(message) = get_err_message(data) {
                    return Err(BadResponseError {
                        packet: self,
                        message,
                    });
                }
            }
        }

        if let Some(message) = get_err_message(&self.payload) {
            return Err(BadResponseError {
                packet: self,
                message,
            });
        }

        Ok(self)
    }
}
impl std::fmt::Display for Packet {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:#?}", self)
    }
}

/// Represents the different opcodes used in Discord IPC communication.
#[derive(Debug, Clone, Serialize)]
#[repr(u32)]
pub enum Opcode {
    Handshake = 0,
    Frame = 1,
    Close = 2,
    Ping = 3,
    Pong = 4,
}
impl TryFrom<u32> for Opcode {
    type Error = Error;
    fn try_from(value: u32) -> Result<Self> {
        match value {
            0 => Ok(Opcode::Handshake),
            1 => Ok(Opcode::Frame),
            2 => Ok(Opcode::Close),
            3 => Ok(Opcode::Ping),
            4 => Ok(Opcode::Pong),
            op => Err(Error::DecodeError(format!("Unknown opcode: {}", op))),
        }
    }
}
impl std::fmt::Display for Opcode {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Opcode::Handshake => write!(f, "Handshake"),
            Opcode::Frame => write!(f, "Frame"),
            Opcode::Close => write!(f, "Close"),
            Opcode::Ping => write!(f, "Ping"),
            Opcode::Pong => write!(f, "Pong"),
        }
    }
}
impl From<Opcode> for String {
    fn from(opcode: Opcode) -> Self {
        opcode.to_string()
    }
}