use crate::error::{DiscordIpcError, ProtocolContext};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[repr(u32)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Opcode {
Handshake = 0,
Frame = 1,
Close = 2,
Ping = 3,
Pong = 4,
}
impl Opcode {
pub fn is_handshake_response(&self) -> bool {
*self == Opcode::Frame
}
pub fn is_frame_response(&self) -> bool {
*self == Opcode::Frame
}
}
impl TryFrom<u32> for Opcode {
type Error = DiscordIpcError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Opcode::Handshake),
1 => Ok(Opcode::Frame),
2 => Ok(Opcode::Close),
3 => Ok(Opcode::Ping),
4 => Ok(Opcode::Pong),
_ => {
let context = ProtocolContext {
expected_opcode: None,
received_opcode: Some(value),
payload_size: None,
};
Err(DiscordIpcError::protocol_violation(
format!("Invalid opcode value: {}", value),
context,
))
}
}
}
}
impl From<Opcode> for u32 {
fn from(opcode: Opcode) -> Self {
opcode as u32
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Command {
SetActivity,
Subscribe,
Unsubscribe,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpcMessage {
pub cmd: Command,
pub args: Value,
pub nonce: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HandshakePayload {
pub v: u32,
pub client_id: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct IpcResponse {
pub cmd: Option<String>,
pub data: Option<Value>,
pub evt: Option<String>,
pub nonce: Option<String>,
}
pub mod constants {
pub const IPC_VERSION: u32 = 1;
pub const MAX_IPC_SOCKETS: u8 = 10;
pub const IPC_SOCKET_PREFIX: &str = "discord-ipc-";
pub const DEFAULT_RETRY_INTERVAL_MS: u64 = 100;
pub const MAX_PAYLOAD_SIZE: u32 = 16 * 1024 * 1024;
pub const IPC_HEADER_SIZE: usize = 8;
}
#[derive(Debug, Clone)]
pub struct IpcConfig {
pub max_sockets: u8,
pub retry_interval_ms: u64,
pub max_payload_size: u32,
pub ipc_version: u32,
}
impl Default for IpcConfig {
fn default() -> Self {
Self {
max_sockets: constants::MAX_IPC_SOCKETS,
retry_interval_ms: constants::DEFAULT_RETRY_INTERVAL_MS,
max_payload_size: constants::MAX_PAYLOAD_SIZE,
ipc_version: constants::IPC_VERSION,
}
}
}
impl IpcConfig {
pub fn new() -> Self {
Self::default()
}
pub fn fast_connect() -> Self {
Self {
max_sockets: 3, retry_interval_ms: 50, ..Default::default()
}
}
pub fn extended() -> Self {
Self {
max_sockets: 10,
retry_interval_ms: 200,
..Default::default()
}
}
pub fn with_max_sockets(mut self, max_sockets: u8) -> Self {
self.max_sockets = max_sockets;
self
}
pub fn with_retry_interval(mut self, retry_interval_ms: u64) -> Self {
self.retry_interval_ms = retry_interval_ms;
self
}
pub fn with_max_payload_size(mut self, max_payload_size: u32) -> Self {
self.max_payload_size = max_payload_size;
self
}
pub fn validate(&self) -> Result<(), &'static str> {
if self.max_sockets == 0 {
return Err("max_sockets must be greater than 0");
}
if self.max_sockets > 100 {
return Err("max_sockets exceeds reasonable limit (100)");
}
if self.retry_interval_ms == 0 {
return Err("retry_interval_ms must be greater than 0");
}
if self.retry_interval_ms > 10_000 {
return Err("retry_interval_ms exceeds reasonable limit (10 seconds)");
}
if self.max_payload_size < 1024 {
return Err("max_payload_size too small (minimum 1 KB)");
}
if self.max_payload_size > 100 * 1024 * 1024 {
return Err("max_payload_size too large (maximum 100 MB)");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opcode_try_from_valid_and_invalid_values() {
assert_eq!(Opcode::try_from(0).unwrap(), Opcode::Handshake);
assert_eq!(Opcode::try_from(4).unwrap(), Opcode::Pong);
let err = Opcode::try_from(42).unwrap_err();
assert!(matches!(err, DiscordIpcError::ProtocolViolation { .. }));
}
#[test]
fn opcode_response_checks_match_protocol() {
assert!(Opcode::Frame.is_handshake_response());
assert!(Opcode::Frame.is_frame_response());
assert!(!Opcode::Handshake.is_frame_response());
}
#[test]
fn ipc_config_validate_bounds() {
let valid = IpcConfig::default();
assert!(valid.validate().is_ok());
let too_many = IpcConfig::default().with_max_sockets(0);
assert!(too_many.validate().is_err());
let huge_payload = IpcConfig::default().with_max_payload_size(200 * 1024 * 1024);
assert!(huge_payload.validate().is_err());
}
#[test]
fn ipc_message_roundtrips_through_json() {
let message = IpcMessage {
cmd: Command::SetActivity,
args: serde_json::json!({"foo": "bar"}),
nonce: "1234".to_string(),
};
let json = serde_json::to_string(&message).expect("serialize message");
let deserialized: IpcMessage = serde_json::from_str(&json).expect("deserialize message");
assert_eq!(deserialized.nonce, "1234");
assert!(matches!(deserialized.cmd, Command::SetActivity));
assert_eq!(
deserialized.args.get("foo").and_then(Value::as_str),
Some("bar")
);
}
}