protobuf migration: replace JSON with prost-generated wire format

- Expanded protocol/lnex.proto with full message set for all packet types
- Added prost + prost-build to gateway build pipeline
- Refactored all handlers and framing to use prost encode/decode
- Removed serde payload structs (now generated from .proto)
- Server starts and admin endpoint responds
This commit is contained in:
loki5512344 2026-07-09 13:04:22 +02:00
parent 5534cd01f7
commit 9821814240
Signed by: boba
GPG key ID: 253067914055423B
31 changed files with 719 additions and 661 deletions

145
Cargo.lock generated
View file

@ -564,6 +564,12 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]] [[package]]
name = "fiat-crypto" name = "fiat-crypto"
version = "0.2.9" version = "0.2.9"
@ -576,6 +582,12 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fixedbitset"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]] [[package]]
name = "flume" name = "flume"
version = "0.12.0" version = "0.12.0"
@ -991,6 +1003,15 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@ -1041,6 +1062,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.2" version = "0.8.2"
@ -1110,6 +1137,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "multimap"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084"
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@ -1211,6 +1244,16 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "petgraph"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772"
dependencies = [
"fixedbitset",
"indexmap",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -1268,6 +1311,16 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@ -1277,6 +1330,58 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "prost"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
"prost-derive",
]
[[package]]
name = "prost-build"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
dependencies = [
"heck",
"itertools",
"log",
"multimap",
"once_cell",
"petgraph",
"prettyplease",
"prost",
"prost-types",
"regex",
"syn",
"tempfile",
]
[[package]]
name = "prost-derive"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
dependencies = [
"anyhow",
"itertools",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "prost-types"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [
"prost",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.46" version = "1.0.46"
@ -1396,6 +1501,18 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.14" version = "0.4.14"
@ -1436,6 +1553,19 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.41" version = "0.23.41"
@ -1918,6 +2048,19 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" version = "2.0.18"
@ -2305,6 +2448,8 @@ dependencies = [
"ed25519-dalek", "ed25519-dalek",
"hex", "hex",
"hkdf", "hkdf",
"prost",
"prost-build",
"rand 0.8.6", "rand 0.8.6",
"rcgen", "rcgen",
"rustls", "rustls",

View file

@ -38,9 +38,13 @@ axum = { version = "0.8", features = ["ws"] }
tower = "0.5" tower = "0.5"
sqlx = { version = "0.9", features = ["sqlite", "runtime-tokio", "tls-rustls"] } sqlx = { version = "0.9", features = ["sqlite", "runtime-tokio", "tls-rustls"] }
bitflags = "2" bitflags = "2"
prost = "0.13"
# TLS 1.3 support # TLS 1.3 support
rustls-pki-types = "1" rustls-pki-types = "1"
rustls = "0.23" rustls = "0.23"
tokio-rustls = "0.26" tokio-rustls = "0.26"
rcgen = "0.13" rcgen = "0.13"
[build-dependencies]
prost-build = "0.13"

3
gateway/build.rs Normal file
View file

@ -0,0 +1,3 @@
fn main() {
prost_build::compile_protos(&["../protocol/lnex.proto"], &["../protocol/"]).unwrap();
}

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tracing::{info, warn}; use tracing::{info, warn};
@ -24,7 +25,7 @@ pub async fn handle_channel_create(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: ChannelCreatePayload = serde_json::from_slice(payload)?; let req = ChannelCreatePayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),
@ -178,7 +179,7 @@ pub async fn handle_channel_delete(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: ChannelDeletePayload = serde_json::from_slice(payload)?; let req = ChannelDeletePayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tracing::debug; use tracing::debug;
use crate::{ use crate::{
@ -8,7 +9,7 @@ use crate::{
}; };
pub async fn handle_message_edit(session_id: &str, payload: &[u8], state: &State) -> Result<()> { pub async fn handle_message_edit(session_id: &str, payload: &[u8], state: &State) -> Result<()> {
let edit: crate::proto::MessageEditPayload = serde_json::from_slice(payload)?; let edit = crate::proto::MessageEditPayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),
@ -60,7 +61,7 @@ pub async fn handle_message_edit(session_id: &str, payload: &[u8], state: &State
} }
pub async fn handle_message_delete(session_id: &str, payload: &[u8], state: &State) -> Result<()> { pub async fn handle_message_delete(session_id: &str, payload: &[u8], state: &State) -> Result<()> {
let delete: MessageDeletePayload = serde_json::from_slice(payload)?; let delete = MessageDeletePayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -21,7 +22,7 @@ pub async fn handle_presence_update(
_crypto: &SessionCrypto, _crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: PresenceUpdatePayload = serde_json::from_slice(payload)?; let req = PresenceUpdatePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;

View file

@ -1,9 +1,10 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use crate::{ use crate::{
domain::session, domain::session,
net::state::{BroadcastMsg, State}, net::state::{BroadcastMsg, State},
proto::{PacketId, ReadReceiptPayload, encode_packet, to_payload}, proto::{PacketId, ReadReceiptBroadcastPayload, ReadReceiptPayload, encode_packet, to_payload},
}; };
pub async fn handle_read_receipt( pub async fn handle_read_receipt(
@ -14,7 +15,7 @@ pub async fn handle_read_receipt(
_crypto: &crate::proto::SessionCrypto, _crypto: &crate::proto::SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: ReadReceiptPayload = serde_json::from_slice(payload)?; let req = ReadReceiptPayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),
@ -25,11 +26,11 @@ pub async fn handle_read_receipt(
.update_read_receipt(&req.channel_id, &sess.user_id, &req.last_read_message_id) .update_read_receipt(&req.channel_id, &sess.user_id, &req.last_read_message_id)
.await?; .await?;
let broadcast_data = serde_json::json!({ let broadcast_data = ReadReceiptBroadcastPayload {
"channel_id": req.channel_id, channel_id: req.channel_id.clone(),
"user_id": sess.user_id, user_id: sess.user_id.clone(),
"last_read_message_id": req.last_read_message_id, last_read_message_id: req.last_read_message_id,
}); };
let _ = state.broadcast.send(BroadcastMsg { let _ = state.broadcast.send(BroadcastMsg {
channel_id: Some(req.channel_id), channel_id: Some(req.channel_id),

View file

@ -1,23 +1,24 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use crate::{ use crate::{
domain::session, domain::session,
net::state::{BroadcastMsg, State}, net::state::{BroadcastMsg, State},
proto::{PacketId, TypingStartPayload, encode_packet, to_payload}, proto::{PacketId, TypingBroadcastPayload, TypingStartPayload, encode_packet, to_payload},
}; };
pub async fn handle_typing_start(session_id: &str, payload: &[u8], state: &State) -> Result<()> { pub async fn handle_typing_start(session_id: &str, payload: &[u8], state: &State) -> Result<()> {
let req: TypingStartPayload = serde_json::from_slice(payload)?; let req = TypingStartPayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),
}; };
let data = serde_json::json!({ let data = TypingBroadcastPayload {
"user_id": sess.user_id, user_id: sess.user_id.clone(),
"nickname": sess.nickname, nickname: sess.nickname.clone(),
"channel_id": req.channel_id, channel_id: req.channel_id.clone(),
}); };
let _ = state.broadcast.send(BroadcastMsg { let _ = state.broadcast.send(BroadcastMsg {
channel_id: Some(req.channel_id), channel_id: Some(req.channel_id),

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -15,7 +16,7 @@ pub async fn handle_dm_history(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: DmHistoryPayload = serde_json::from_slice(payload)?; let req = DmHistoryPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tracing::warn; use tracing::warn;
@ -21,7 +22,7 @@ pub async fn handle_dm_message(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let msg: DmMessagePayload = serde_json::from_slice(payload)?; let msg = DmMessagePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -15,7 +16,7 @@ pub async fn handle_dm_start(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: DmStartPayload = serde_json::from_slice(payload)?; let req = DmStartPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
@ -95,10 +96,7 @@ pub async fn handle_dm_read_ack(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: serde_json::Value = serde_json::from_slice(payload)?; let req = crate::proto::DmReadAckPayload::decode(payload)?;
let dm_id = req["dm_id"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("missing dm_id"))?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
@ -106,7 +104,7 @@ pub async fn handle_dm_read_ack(
let my_id = sess.user_id.clone(); let my_id = sess.user_id.clone();
drop(sess); drop(sess);
state.storage.reset_dm_unread(dm_id, &my_id).await?; state.storage.reset_dm_unread(&req.dm_id, &my_id).await?;
io::send_encrypted(stream, PacketId::DmReadAck, seq, b"{}", crypto).await?; io::send_encrypted(stream, PacketId::DmReadAck, seq, b"{}", crypto).await?;
Ok(()) Ok(())

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tracing::debug; use tracing::debug;
@ -21,7 +22,7 @@ pub async fn dispatch<S: AsyncRead + AsyncWrite + Unpin>(
) -> Result<()> { ) -> Result<()> {
match pid { match pid {
PacketId::Ping => { PacketId::Ping => {
let ping: PingPayload = serde_json::from_slice(payload)?; let ping = PingPayload::decode(payload)?;
io::send_encrypted( io::send_encrypted(
ctx.stream, ctx.stream,
PacketId::Pong, PacketId::Pong,
@ -34,12 +35,12 @@ pub async fn dispatch<S: AsyncRead + AsyncWrite + Unpin>(
.await?; .await?;
} }
PacketId::JoinChannel => { PacketId::JoinChannel => {
let m: JoinChannelPayload = serde_json::from_slice(payload)?; let m = JoinChannelPayload::decode(payload)?;
channel::join(ctx.stream, ctx.seq, session_id, &m.channel_id, ctx.crypto, ctx.state) channel::join(ctx.stream, ctx.seq, session_id, &m.channel_id, ctx.crypto, ctx.state)
.await?; .await?;
} }
PacketId::LeaveChannel => { PacketId::LeaveChannel => {
let m: LeaveChannelPayload = serde_json::from_slice(payload)?; let m = LeaveChannelPayload::decode(payload)?;
channel::leave(ctx.stream, ctx.seq, session_id, &m.channel_id, ctx.crypto, ctx.state) channel::leave(ctx.stream, ctx.seq, session_id, &m.channel_id, ctx.crypto, ctx.state)
.await?; .await?;
} }
@ -70,7 +71,7 @@ pub async fn dispatch<S: AsyncRead + AsyncWrite + Unpin>(
.await?; .await?;
} }
PacketId::ChatMessage => { PacketId::ChatMessage => {
let m: ChatMessagePayload = serde_json::from_slice(payload)?; let m = ChatMessagePayload::decode(payload)?;
content::chat::handle(session_id, m, ctx.state).await?; content::chat::handle(session_id, m, ctx.state).await?;
} }
PacketId::DmStart => { PacketId::DmStart => {
@ -382,11 +383,11 @@ pub async fn dispatch<S: AsyncRead + AsyncWrite + Unpin>(
.await?; .await?;
} }
PacketId::MessageReactionAdd => { PacketId::MessageReactionAdd => {
let m: ReactionPayload = serde_json::from_slice(payload)?; let m = ReactionPayload::decode(payload)?;
content::reaction::handle_reaction_add(session_id, m, ctx.state).await?; content::reaction::handle_reaction_add(session_id, m, ctx.state).await?;
} }
PacketId::MessageReactionRemove => { PacketId::MessageReactionRemove => {
let m: ReactionPayload = serde_json::from_slice(payload)?; let m = ReactionPayload::decode(payload)?;
content::reaction::handle_reaction_remove(session_id, m, ctx.state).await?; content::reaction::handle_reaction_remove(session_id, m, ctx.state).await?;
} }
PacketId::MessageEdit => { PacketId::MessageEdit => {

View file

@ -1,10 +1,11 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
domain::session, domain::session,
net::io, net::io,
proto::{FriendDeclinePayload, PacketId, SessionCrypto, to_payload}, proto::{FriendDeclinePayload, PacketId, SessionCrypto, SimpleResponsePayload, to_payload},
}; };
pub async fn handle_friend_decline( pub async fn handle_friend_decline(
@ -15,7 +16,7 @@ pub async fn handle_friend_decline(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &crate::net::state::State, state: &crate::net::state::State,
) -> Result<()> { ) -> Result<()> {
let req: FriendDeclinePayload = serde_json::from_slice(payload)?; let req = FriendDeclinePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -29,7 +30,7 @@ pub async fn handle_friend_decline(
stream, stream,
PacketId::FriendDecline, PacketId::FriendDecline,
seq, seq,
&to_payload(&serde_json::json!({"status": "DECLINED"})), &to_payload(&SimpleResponsePayload { ok: true, user_id: None, role_id: None }),
crypto, crypto,
) )
.await?; .await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -6,7 +7,7 @@ use crate::{
net::{io, state::State}, net::{io, state::State},
proto::{ proto::{
BlockListPayload, BlockUserPayload, FriendRemovePayload, PacketId, SessionCrypto, BlockListPayload, BlockUserPayload, FriendRemovePayload, PacketId, SessionCrypto,
UnblockUserPayload, to_payload, SimpleResponsePayload, UnblockUserPayload, to_payload,
}, },
}; };
@ -18,7 +19,7 @@ pub async fn handle_friend_remove(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: FriendRemovePayload = serde_json::from_slice(payload)?; let req = FriendRemovePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -32,7 +33,7 @@ pub async fn handle_friend_remove(
stream, stream,
PacketId::FriendRemove, PacketId::FriendRemove,
seq, seq,
&to_payload(&serde_json::json!({"removed": true})), &to_payload(&SimpleResponsePayload { ok: true, user_id: None, role_id: None }),
crypto, crypto,
) )
.await?; .await?;
@ -47,7 +48,7 @@ pub async fn handle_block_user(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: BlockUserPayload = serde_json::from_slice(payload)?; let req = BlockUserPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -61,7 +62,7 @@ pub async fn handle_block_user(
stream, stream,
PacketId::BlockUser, PacketId::BlockUser,
seq, seq,
&to_payload(&serde_json::json!({"blocked": true})), &to_payload(&SimpleResponsePayload { ok: true, user_id: None, role_id: None }),
crypto, crypto,
) )
.await?; .await?;
@ -76,7 +77,7 @@ pub async fn handle_unblock_user(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: UnblockUserPayload = serde_json::from_slice(payload)?; let req = UnblockUserPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -90,7 +91,7 @@ pub async fn handle_unblock_user(
stream, stream,
PacketId::UnblockUser, PacketId::UnblockUser,
seq, seq,
&to_payload(&serde_json::json!({"unblocked": true})), &to_payload(&SimpleResponsePayload { ok: true, user_id: None, role_id: None }),
crypto, crypto,
) )
.await?; .await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -23,7 +24,7 @@ pub async fn handle_friend_request(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: FriendRequestPayload = serde_json::from_slice(payload)?; let req = FriendRequestPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -92,7 +93,9 @@ pub async fn handle_friend_request(
stream, stream,
PacketId::FriendRequest, PacketId::FriendRequest,
seq, seq,
&to_payload(&serde_json::json!({"status": "PENDING", "to_user_id": req.to_user_id})), &to_payload(&FriendRequestPayload {
to_user_id: req.to_user_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;
@ -107,7 +110,7 @@ pub async fn handle_friend_accept(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: FriendAcceptPayload = serde_json::from_slice(payload)?; let req = FriendAcceptPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -144,7 +147,9 @@ pub async fn handle_friend_accept(
stream, stream,
PacketId::FriendAccept, PacketId::FriendAccept,
seq, seq,
&to_payload(&serde_json::json!({"status": "ACCEPTED", "from_user_id": req.from_user_id})), &to_payload(&FriendAcceptPayload {
from_user_id: req.from_user_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -19,7 +20,7 @@ pub async fn handle_audit_log_fetch(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: GuildAuditLogFetchPayload = serde_json::from_slice(payload)?; let req = GuildAuditLogFetchPayload::decode(payload)?;
let sess = match crate::domain::session::get(&state.sessions, session_id).await { let sess = match crate::domain::session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tracing::debug; use tracing::debug;
@ -18,7 +19,7 @@ pub async fn handle_guild_create(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: GuildCreatePayload = serde_json::from_slice(payload)?; let req = GuildCreatePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -74,11 +75,7 @@ pub async fn handle_guild_delete(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
#[derive(serde::Deserialize)] let req = crate::proto::GuildDeletePayload::decode(payload)?;
struct Req {
guild_id: String,
}
let req: Req = serde_json::from_slice(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -112,11 +109,14 @@ pub async fn handle_guild_delete(
None, None,
) )
.await?; .await?;
io::send_encrypted( io::send_encrypted(
stream, stream,
PacketId::GuildDelete, PacketId::GuildDelete,
seq, seq,
&to_payload(&serde_json::json!({"guild_id": req.guild_id})), &to_payload(&crate::proto::GuildDeletePayload {
guild_id: req.guild_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -20,7 +21,7 @@ pub async fn handle_invite_create(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: InviteCreatePayload = serde_json::from_slice(payload)?; let req = InviteCreatePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -96,7 +97,7 @@ pub async fn handle_invite_accept(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: InviteAcceptPayload = serde_json::from_slice(payload)?; let req = InviteAcceptPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -156,7 +157,11 @@ pub async fn handle_invite_accept(
stream, stream,
PacketId::InviteAccept, PacketId::InviteAccept,
seq, seq,
&to_payload(&serde_json::json!({"guild_id": inv.guild_id, "guild_name": inv.guild_name})), &to_payload(&InviteAcceptPayload {
code: "".into(),
guild_id: inv.guild_id.clone(),
guild_name: inv.guild_name.clone(),
}),
crypto, crypto,
) )
.await?; .await?;
@ -171,7 +176,7 @@ pub async fn handle_invite_delete(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: InviteDeletePayload = serde_json::from_slice(payload)?; let req = InviteDeletePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -211,7 +216,10 @@ pub async fn handle_invite_delete(
stream, stream,
PacketId::InviteDelete, PacketId::InviteDelete,
seq, seq,
&to_payload(&serde_json::json!({"invite_id": req.invite_id})), &to_payload(&InviteDeletePayload {
guild_id: req.guild_id.clone(),
invite_id: req.invite_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;

View file

@ -4,7 +4,7 @@ use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
domain::session, domain::session,
net::{io, state::State}, net::{io, state::State},
proto::{GuildInfo, GuildListPayload, PacketId, SessionCrypto, to_payload}, proto::{GuildInfo, GuildListPayload, PacketId, SessionCrypto, UserRoleUpdatePayload, to_payload},
}; };
pub async fn handle_guild_list( pub async fn handle_guild_list(
@ -41,9 +41,11 @@ pub async fn handle_guild_list(
stream, stream,
PacketId::UserRoleUpdate, PacketId::UserRoleUpdate,
seq, seq,
&to_payload( &to_payload(&UserRoleUpdatePayload {
&serde_json::json!({"user_id": sess.user_id, "guild_id": g.id, "color": color}), user_id: sess.user_id.clone(),
), guild_id: g.id.clone(),
color,
}),
crypto, crypto,
) )
.await?; .await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -20,7 +21,7 @@ pub async fn handle_guild_member_join(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: GuildMemberJoinPayload = serde_json::from_slice(payload)?; let req = GuildMemberJoinPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -44,7 +45,9 @@ pub async fn handle_guild_member_join(
stream, stream,
PacketId::GuildMemberJoin, PacketId::GuildMemberJoin,
seq, seq,
&to_payload(&serde_json::json!({"guild_id": req.guild_id, "user_id": sess.user_id})), &to_payload(&crate::proto::GuildMemberJoinPayload {
guild_id: req.guild_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;
@ -53,9 +56,11 @@ pub async fn handle_guild_member_join(
stream, stream,
PacketId::UserRoleUpdate, PacketId::UserRoleUpdate,
seq, seq,
&to_payload( &to_payload(&crate::proto::UserRoleUpdatePayload {
&serde_json::json!({"user_id": sess.user_id, "guild_id": req.guild_id, "color": color}), user_id: sess.user_id.clone(),
), guild_id: req.guild_id.clone(),
color,
}),
crypto, crypto,
) )
.await?; .await?;
@ -70,25 +75,28 @@ pub async fn handle_guild_member_leave(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: GuildMemberLeavePayload = serde_json::from_slice(payload)?; let req = GuildMemberLeavePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
let target = if req.user_id.is_empty() { let target = if req.user_id.is_empty() {
&sess.user_id sess.user_id.clone()
} else { } else {
&req.user_id req.user_id.clone()
}; };
state state
.storage .storage
.remove_guild_member(&req.guild_id, target) .remove_guild_member(&req.guild_id, target.as_str())
.await?; .await?;
io::send_encrypted( io::send_encrypted(
stream, stream,
PacketId::GuildMemberLeave, PacketId::GuildMemberLeave,
seq, seq,
&to_payload(&serde_json::json!({"guild_id": req.guild_id, "user_id": target})), &to_payload(&GuildMemberLeavePayload {
guild_id: req.guild_id.clone(),
user_id: target,
}),
crypto, crypto,
) )
.await?; .await?;
@ -103,7 +111,7 @@ pub async fn handle_guild_member_kick(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: GuildMemberKickPayload = serde_json::from_slice(payload)?; let req = GuildMemberKickPayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -146,7 +154,10 @@ pub async fn handle_guild_member_kick(
stream, stream,
PacketId::GuildMemberKick, PacketId::GuildMemberKick,
seq, seq,
&to_payload(&serde_json::json!({"guild_id": req.guild_id, "user_id": req.user_id})), &to_payload(&GuildMemberKickPayload {
guild_id: req.guild_id.clone(),
user_id: req.user_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -22,7 +23,7 @@ pub async fn handle_member_list_fetch(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: GuildMemberListFetchPayload = serde_json::from_slice(payload)?; let req = GuildMemberListFetchPayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),
@ -72,7 +73,7 @@ pub async fn handle_role_assign(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: RoleAssignPayload = serde_json::from_slice(payload)?; let req = RoleAssignPayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),
@ -118,9 +119,11 @@ pub async fn handle_role_assign(
stream, stream,
PacketId::GuildRoleAssign, PacketId::GuildRoleAssign,
seq, seq,
&to_payload( &to_payload(&RoleAssignPayload {
&serde_json::json!({"ok": true, "user_id": req.user_id, "role_id": req.role_id}), guild_id: req.guild_id.clone(),
), user_id: req.user_id.clone(),
role_id: req.role_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;
@ -136,7 +139,7 @@ pub async fn handle_role_unassign(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: RoleAssignPayload = serde_json::from_slice(payload)?; let req = RoleAssignPayload::decode(payload)?;
let sess = match session::get(&state.sessions, session_id).await { let sess = match session::get(&state.sessions, session_id).await {
Some(s) => s, Some(s) => s,
None => return Ok(()), None => return Ok(()),
@ -181,9 +184,11 @@ pub async fn handle_role_unassign(
stream, stream,
PacketId::GuildRoleUnassign, PacketId::GuildRoleUnassign,
seq, seq,
&to_payload( &to_payload(&RoleAssignPayload {
&serde_json::json!({"ok": true, "user_id": req.user_id, "role_id": req.role_id}), guild_id: req.guild_id.clone(),
), user_id: req.user_id.clone(),
role_id: req.role_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;
@ -199,7 +204,7 @@ pub async fn handle_role_list_fetch(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: GuildRoleListFetchPayload = serde_json::from_slice(payload)?; let req = GuildRoleListFetchPayload::decode(payload)?;
let _sess = session::get(&state.sessions, session_id).await; let _sess = session::get(&state.sessions, session_id).await;
let rows = state.storage.list_guild_roles(&req.guild_id).await?; let rows = state.storage.list_guild_roles(&req.guild_id).await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use crate::{ use crate::{
@ -17,7 +18,7 @@ pub async fn handle_role_create(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: RoleCreatePayload = serde_json::from_slice(payload)?; let req = RoleCreatePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -63,9 +64,13 @@ pub async fn handle_role_create(
stream, stream,
PacketId::RoleCreate, PacketId::RoleCreate,
seq, seq,
&to_payload( &to_payload(&RoleCreatePayload {
&serde_json::json!({"id": role_id, "guild_id": req.guild_id, "name": req.name}), guild_id: req.guild_id.clone(),
), name: req.name.clone(),
color: req.color.clone(),
permissions: req.permissions,
id: role_id,
}),
crypto, crypto,
) )
.await?; .await?;
@ -80,7 +85,7 @@ pub async fn handle_role_delete(
crypto: &SessionCrypto, crypto: &SessionCrypto,
state: &State, state: &State,
) -> Result<()> { ) -> Result<()> {
let req: RoleDeletePayload = serde_json::from_slice(payload)?; let req = RoleDeletePayload::decode(payload)?;
let sess = session::get(&state.sessions, session_id) let sess = session::get(&state.sessions, session_id)
.await .await
.ok_or_else(|| anyhow::anyhow!("session not found"))?; .ok_or_else(|| anyhow::anyhow!("session not found"))?;
@ -120,7 +125,10 @@ pub async fn handle_role_delete(
stream, stream,
PacketId::RoleDelete, PacketId::RoleDelete,
seq, seq,
&to_payload(&serde_json::json!({"role_id": req.role_id})), &to_payload(&RoleDeletePayload {
guild_id: req.guild_id.clone(),
role_id: req.role_id.clone(),
}),
crypto, crypto,
) )
.await?; .await?;

View file

@ -1,4 +1,5 @@
use anyhow::Result; use anyhow::Result;
use prost::Message;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
@ -23,17 +24,17 @@ pub async fn run(
) -> Result<(session::Session, SessionCrypto)> { ) -> Result<(session::Session, SessionCrypto)> {
// Generate ephemeral X25519 keypair for forward secrecy // Generate ephemeral X25519 keypair for forward secrecy
let (eph_sk, eph_pk) = SessionCrypto::new_ephemeral(); let (eph_sk, eph_pk) = SessionCrypto::new_ephemeral();
let eph_pk_hex = hex::encode(eph_pk.as_bytes()); let eph_pk_raw = eph_pk.as_bytes();
// HELLO — include server's ephemeral public key + privacy mode // HELLO — include server's ephemeral public key + privacy mode
let challenge = auth::new_challenge(); let challenge = auth::new_challenge();
let private_mode = state.config.is_private(); let private_mode = state.config.is_private();
let hello = HelloPayload { let hello = HelloPayload {
lnex_version: LNEX_VERSION.into(), lnex_version: LNEX_VERSION.into(),
server_pubkey: state.server_identity.pubkey_hex(), server_pubkey: hex::decode(state.server_identity.pubkey_hex())?,
challenge_nonce: hex::encode(challenge), challenge_nonce: challenge.to_vec(),
node_name: state.config.node.name.clone(), node_name: state.config.node.name.clone(),
server_eph_pubkey: eph_pk_hex, server_eph_pubkey: eph_pk_raw.to_vec(),
private_mode, private_mode,
}; };
io::send_packet(stream, PacketId::Hello, seq, &to_payload(&hello)).await?; io::send_packet(stream, PacketId::Hello, seq, &to_payload(&hello)).await?;
@ -52,7 +53,7 @@ pub async fn run(
return Err(anyhow::anyhow!("expected AUTH")); return Err(anyhow::anyhow!("expected AUTH"));
} }
let msg: AuthPayload = serde_json::from_slice(&payload)?; let msg = AuthPayload::decode(payload.as_slice())?;
debug!("{addr} → AUTH nick={}", msg.nickname); debug!("{addr} → AUTH nick={}", msg.nickname);
if msg.lnex_version != LNEX_VERSION { if msg.lnex_version != LNEX_VERSION {
@ -66,9 +67,8 @@ pub async fn run(
return Err(anyhow::anyhow!("version mismatch")); return Err(anyhow::anyhow!("version mismatch"));
} }
let pubkey = let pubkey: [u8; 32] = msg.client_pubkey.as_slice().try_into()?;
hex_to_32(&msg.client_pubkey).ok_or_else(|| anyhow::anyhow!("bad client pubkey"))?; let sig: [u8; 64] = msg.signature.as_slice().try_into()?;
let sig = hex_to_64(&msg.signature).ok_or_else(|| anyhow::anyhow!("bad signature"))?;
if let Err(e) = auth::verify_auth(&challenge, &pubkey, &sig) { if let Err(e) = auth::verify_auth(&challenge, &pubkey, &sig) {
warn!("{addr} auth failed: {e}"); warn!("{addr} auth failed: {e}");
@ -83,7 +83,7 @@ pub async fn run(
return Err(anyhow::anyhow!("auth failed")); return Err(anyhow::anyhow!("auth failed"));
} }
if state.storage.is_banned(&msg.client_pubkey).await? { if state.storage.is_banned(&hex::encode(&msg.client_pubkey)).await? {
state.metrics.inc(&state.metrics.auth_failures); state.metrics.inc(&state.metrics.auth_failures);
io::send_error(stream, seq, proto::ErrorCode::AuthFailed, "banned").await?; io::send_error(stream, seq, proto::ErrorCode::AuthFailed, "banned").await?;
return Err(anyhow::anyhow!("banned")); return Err(anyhow::anyhow!("banned"));
@ -91,19 +91,18 @@ pub async fn run(
state state
.storage .storage
.upsert_user(&msg.client_pubkey, &msg.nickname) .upsert_user(&hex::encode(&msg.client_pubkey), &msg.nickname)
.await?; .await?;
// ECDH: compute shared secret from server's ephemeral sk + client's ephemeral pk // ECDH: compute shared secret from server's ephemeral sk + client's ephemeral pk
let client_eph_raw = hex_to_32(&msg.client_eph_pubkey) let client_eph_raw: [u8; 32] = msg.client_eph_pubkey.as_slice().try_into()?;
.ok_or_else(|| anyhow::anyhow!("bad client eph pubkey"))?;
let client_eph_pk = x25519_dalek::PublicKey::from(client_eph_raw); let client_eph_pk = x25519_dalek::PublicKey::from(client_eph_raw);
let shared_secret = SessionCrypto::ecdh(eph_sk, &client_eph_pk); let shared_secret = SessionCrypto::ecdh(eph_sk, &client_eph_pk);
// SESSION // SESSION
let sess = session::create( let sess = session::create(
&state.sessions, &state.sessions,
msg.client_pubkey.clone(), hex::encode(&msg.client_pubkey),
msg.nickname.clone(), msg.nickname.clone(),
) )
.await; .await;
@ -131,10 +130,3 @@ pub async fn run(
Ok((sess, crypto)) Ok((sess, crypto))
} }
fn hex_to_32(s: &str) -> Option<[u8; 32]> {
hex::decode(s).ok()?.try_into().ok()
}
fn hex_to_64(s: &str) -> Option<[u8; 64]> {
hex::decode(s).ok()?.try_into().ok()
}

View file

@ -1,6 +1,6 @@
use chacha20poly1305::{ use chacha20poly1305::{
ChaCha20Poly1305, Key, KeyInit, Nonce,
aead::{Aead, Payload}, aead::{Aead, Payload},
ChaCha20Poly1305, Key, KeyInit, Nonce,
}; };
pub(super) fn make_nonce(cid: &[u8; 8], seq: u64) -> [u8; 12] { pub(super) fn make_nonce(cid: &[u8; 8], seq: u64) -> [u8; 12] {

View file

@ -1,7 +1,7 @@
use super::packet::{PacketHeader, PacketId}; use super::packet::{PacketHeader, PacketId};
use serde::Serialize; use prost::Message;
/// Encode a packet: header + JSON payload bytes. /// Encode a packet: header + protobuf payload bytes.
pub fn encode_packet(id: PacketId, seq: u32, payload: &[u8]) -> Vec<u8> { pub fn encode_packet(id: PacketId, seq: u32, payload: &[u8]) -> Vec<u8> {
let header = PacketHeader::new(id, seq, payload.len() as u32); let header = PacketHeader::new(id, seq, payload.len() as u32);
let mut out = Vec::with_capacity(PacketHeader::SIZE + payload.len()); let mut out = Vec::with_capacity(PacketHeader::SIZE + payload.len());
@ -10,7 +10,7 @@ pub fn encode_packet(id: PacketId, seq: u32, payload: &[u8]) -> Vec<u8> {
out out
} }
/// Serialize a payload struct to JSON bytes. /// Encode a protobuf payload struct to bytes.
pub fn to_payload<T: Serialize>(v: &T) -> Vec<u8> { pub fn to_payload(msg: &impl Message) -> Vec<u8> {
serde_json::to_vec(v).expect("payload serialization is infallible") msg.encode_to_vec()
} }

View file

@ -1,11 +1,104 @@
mod crypto; mod crypto;
mod framing; mod framing;
mod packet; mod packet;
mod payloads;
pub mod lnex {
include!(concat!(env!("OUT_DIR"), "/lnex.rs"));
}
pub use lnex::*;
pub use crypto::SessionCrypto; pub use crypto::SessionCrypto;
// Re-export everything so `crate::proto::X` works as before // Re-export everything so `crate::proto::X` works as before
pub use framing::{encode_packet, to_payload}; pub use framing::{encode_packet, to_payload};
pub use packet::{ErrorCode, PacketHeader, PacketId, flags}; pub use packet::{flags, ErrorCode, PacketHeader, PacketId};
pub use payloads::*;
// ─── Backward-compat type aliases ────────────────────────────────
// Handshake
pub type HelloPayload = Hello;
pub type AuthPayload = Auth;
pub type SessionPayload = Session;
// Keepalive
pub type PingPayload = Ping;
pub type PongPayload = Pong;
// Error / Disconnect
pub type ErrorPayload = Error;
pub type DisconnectPayload = Disconnect;
// Channels
pub type JoinChannelPayload = JoinChannel;
pub type LeaveChannelPayload = LeaveChannel;
pub type ChannelStatePayload = ChannelState;
pub type ChannelCreatePayload = ChannelCreate;
pub type ChannelDeletePayload = ChannelDelete;
pub type ChannelListPayload = ChannelList;
pub type UserJoinPayload = UserJoin;
pub type UserLeavePayload = UserLeave;
// Chat
pub type ChatMessagePayload = ChatMessage;
pub type ChatHistoryPayload = ChatHistory;
pub type MessageEditPayload = MessageEdit;
pub type MessageDeletePayload = MessageDelete;
// Voice
pub type VoiceStatePayload = VoiceState;
// Guilds
pub type GuildCreatePayload = GuildCreate;
pub type GuildDeletePayload = GuildDelete;
pub type GuildListPayload = GuildList;
pub type GuildMemberJoinPayload = GuildMemberJoin;
pub type GuildMemberLeavePayload = GuildMemberLeave;
pub type GuildMemberKickPayload = GuildMemberKick;
pub type RoleCreatePayload = RoleCreate;
pub type RoleDeletePayload = RoleDelete;
pub type InviteCreatePayload = InviteCreate;
pub type InviteAcceptPayload = InviteAccept;
pub type InviteDeletePayload = InviteDelete;
pub type GuildAuditLogFetchPayload = GuildAuditLogFetch;
pub type AuditLogEntryPayload = AuditLogEntry;
pub type GuildAuditLogPayload = GuildAuditLog;
pub type GuildMemberListFetchPayload = GuildMemberListFetch;
pub type GuildMemberInfoPayload = GuildMemberInfo;
pub type GuildMemberListPayload = GuildMemberList;
pub type RoleAssignPayload = RoleAssign;
pub type GuildRoleListFetchPayload = GuildRoleListFetch;
pub type GuildRoleInfoPayload = GuildRoleInfo;
pub type GuildRoleListPayload = GuildRoleList;
// DMs
pub type DmStartPayload = DmStart;
pub type DmStartResponsePayload = DmStartResponse;
pub type DmMessagePayload = DmMessage;
pub type DmHistoryPayload = DmHistory;
pub type DmReadAckPayload = DmReadAck;
// Friends
pub type FriendRequestPayload = FriendRequest;
pub type FriendAcceptPayload = FriendAccept;
pub type FriendDeclinePayload = FriendDecline;
pub type FriendRemovePayload = FriendRemove;
pub type FriendListPayload = FriendList;
pub type BlockUserPayload = BlockUser;
pub type UnblockUserPayload = UnblockUser;
pub type BlockListPayload = BlockList;
pub type FriendEventPayload = FriendEvent;
// Presence
pub type PresenceUpdatePayload = PresenceUpdate;
pub type PresenceSyncPayload = PresenceSync;
pub type PresenceEventPayload = PresenceEvent;
// Read/typing
pub type ReadReceiptPayload = ReadReceipt;
pub type TypingStartPayload = TypingStart;
// Response types
pub type ReadReceiptBroadcastPayload = ReadReceiptBroadcast;
pub type UserRoleUpdatePayload = UserRoleUpdate;
pub type TypingBroadcastPayload = TypingBroadcast;
pub type SimpleResponsePayload = SimpleResponse;

View file

@ -1,157 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct HelloPayload {
pub lnex_version: String,
pub server_pubkey: String,
pub challenge_nonce: String,
pub node_name: String,
pub server_eph_pubkey: String,
pub private_mode: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthPayload {
pub client_pubkey: String,
pub nickname: String,
pub lnex_version: String,
pub signature: String,
pub client_eph_pubkey: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionPayload {
pub session_id: String,
pub token: String,
pub expires_at: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PingPayload {
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PongPayload {
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct JoinChannelPayload {
pub channel_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LeaveChannelPayload {
pub channel_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChannelStatePayload {
pub channel_id: String,
pub channel_name: String,
pub kind: String,
pub members: Vec<MemberInfo>,
pub voice_endpoint: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChannelCreatePayload {
pub channel_id: String,
pub channel_name: String,
/// "text" or "voice".
pub kind: String,
/// Optional guild_id this channel belongs to (Phase 1.x — unused, future).
#[serde(default)]
pub guild_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChannelDeletePayload {
pub channel_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChannelListPayload {
pub channels: Vec<ChannelListItem>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChannelListItem {
pub channel_id: String,
pub channel_name: String,
pub kind: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MemberInfo {
pub user_id: String,
pub nickname: String,
pub in_voice: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserJoinPayload {
pub channel_id: String,
pub user_id: String,
pub nickname: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserLeavePayload {
pub channel_id: String,
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ChatMessagePayload {
pub message_id: String,
pub channel_id: String,
pub sender_id: String,
pub content: String,
pub timestamp: i64,
#[serde(default)]
pub edited: bool,
/// Optional message_id this message is replying to.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reply_to: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReactionPayload {
pub message_id: String,
pub channel_id: String,
pub emoji: String,
#[serde(default)]
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageEditPayload {
pub message_id: String,
pub channel_id: String,
pub content: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MessageDeletePayload {
pub message_id: String,
pub channel_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ChatHistoryPayload {
pub channel_id: String,
pub messages: Vec<ChatMessagePayload>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorPayload {
pub code: u32,
pub message: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DisconnectPayload {
pub reason: String,
}

View file

@ -1,161 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildCreatePayload {
pub name: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GuildInfo {
pub id: String,
pub owner_id: String,
pub name: String,
pub member_count: i64,
pub created_at: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildListPayload {
pub guilds: Vec<GuildInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildMemberJoinPayload {
pub guild_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildMemberLeavePayload {
pub guild_id: String,
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildMemberKickPayload {
pub guild_id: String,
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RoleCreatePayload {
pub guild_id: String,
pub name: String,
pub color: Option<String>,
pub permissions: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RoleDeletePayload {
pub guild_id: String,
pub role_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteCreatePayload {
pub guild_id: String,
pub max_uses: Option<i64>,
pub expires_in_seconds: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct InviteInfo {
pub id: String,
pub guild_id: String,
pub guild_name: String,
pub code: String,
pub creator_id: String,
pub max_uses: Option<i64>,
pub uses: i64,
pub expires_at: Option<i64>,
pub created_at: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteAcceptPayload {
pub code: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InviteDeletePayload {
pub guild_id: String,
pub invite_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildAuditLogFetchPayload {
pub guild_id: String,
#[serde(default = "default_audit_limit")]
pub limit: i64,
}
fn default_audit_limit() -> i64 {
50
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AuditLogEntryPayload {
pub id: String,
pub guild_id: String,
pub actor_id: String,
pub action: String,
pub target_id: Option<String>,
pub target_type: Option<String>,
pub reason: Option<String>,
pub created_at: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildAuditLogPayload {
pub guild_id: String,
pub entries: Vec<AuditLogEntryPayload>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildMemberListFetchPayload {
pub guild_id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GuildMemberInfoPayload {
pub user_id: String,
pub nickname: String,
pub joined_at: i64,
pub role_color: String,
pub role_name: String,
/// True if this user is the guild owner.
pub is_owner: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildMemberListPayload {
pub guild_id: String,
pub members: Vec<GuildMemberInfoPayload>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RoleAssignPayload {
pub guild_id: String,
pub user_id: String,
pub role_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildRoleListFetchPayload {
pub guild_id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct GuildRoleInfoPayload {
pub id: String,
pub guild_id: String,
pub name: String,
pub color: String,
pub permissions: u64,
pub position: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GuildRoleListPayload {
pub guild_id: String,
pub roles: Vec<GuildRoleInfoPayload>,
}

View file

@ -1,7 +0,0 @@
mod channel;
mod guild;
mod social;
pub use channel::*;
pub use guild::*;
pub use social::*;

View file

@ -1,132 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct DmStartPayload {
pub target_user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DmStartResponsePayload {
pub dm_id: String,
pub other_user_id: String,
pub other_nickname: String,
pub messages: Vec<DmMessagePayload>,
#[serde(default)]
pub unread_count: u32,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct DmMessagePayload {
pub dm_id: String,
pub sender_id: String,
pub content: String,
pub timestamp: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DmHistoryPayload {
pub dm_id: String,
#[serde(default)]
pub messages: Vec<DmMessagePayload>,
#[serde(default)]
pub search_query: Option<String>,
#[serde(default)]
pub limit: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FriendRequestPayload {
pub to_user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FriendAcceptPayload {
pub from_user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FriendDeclinePayload {
pub from_user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FriendRemovePayload {
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FriendInfo {
pub user_id: String,
pub nickname: String,
pub status: String,
pub since: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FriendListPayload {
pub friends: Vec<FriendInfo>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BlockUserPayload {
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UnblockUserPayload {
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BlockListPayload {
pub blocked: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FriendEventPayload {
pub event: String,
pub user_id: String,
pub nickname: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PresenceUpdatePayload {
pub status: String,
pub activity_type: Option<String>,
pub activity_text: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PresenceInfo {
pub user_id: String,
pub nickname: String,
pub status: String,
pub activity_type: Option<String>,
pub activity_text: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PresenceSyncPayload {
pub presences: Vec<PresenceInfo>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PresenceEventPayload {
pub user_id: String,
pub nickname: String,
pub status: String,
pub activity_type: Option<String>,
pub activity_text: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReadReceiptPayload {
pub channel_id: String,
pub last_read_message_id: String,
pub user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TypingStartPayload {
pub channel_id: String,
}

View file

@ -3,129 +3,359 @@ package lnex;
// ─── HANDSHAKE ────────────────────────────────────────────────────────────── // ─── HANDSHAKE ──────────────────────────────────────────────────────────────
// Server → Client on TCP connect (packet_id = 0x0001)
message Hello { message Hello {
string lnex_version = 1; // "v1" string lnex_version = 1;
bytes server_pubkey = 2; // 32-byte Ed25519 public key bytes server_pubkey = 2;
bytes challenge_nonce = 3; // 32-byte random challenge bytes challenge_nonce = 3;
string node_name = 4; string node_name = 4;
bytes server_eph_pubkey = 5;
bool private_mode = 6;
} }
// Client → Server (packet_id = 0x0002)
message Auth { message Auth {
bytes client_pubkey = 1; // 32-byte Ed25519 public key bytes client_pubkey = 1;
string nickname = 2; string nickname = 2;
string lnex_version = 3; string lnex_version = 3;
bytes signature = 4; // Ed25519 sig over challenge_nonce bytes signature = 4;
bytes client_eph_pubkey = 5;
} }
// Server → Client on success (packet_id = 0x0003)
message Session { message Session {
string session_id = 1; string session_id = 1;
string token = 2; string token = 2;
int64 expires_at = 3; // Unix ms, 0 = no expiry int64 expires_at = 3;
} }
// ─── KEEPALIVE ────────────────────────────────────────────────────────────── // ─── KEEPALIVE ──────────────────────────────────────────────────────────────
// Either direction (0x0004 / 0x0005) message Ping { int64 timestamp = 1; }
message Ping { message Pong { int64 timestamp = 1; }
int64 timestamp = 1; // Unix ms
}
message Pong { // ─── ERROR / DISCONNECT ─────────────────────────────────────────────────────
int64 timestamp = 1; // Echo of Ping.timestamp
} message Error { uint32 code = 1; string message = 2; }
message Disconnect { string reason = 1; }
// ─── CHANNELS ─────────────────────────────────────────────────────────────── // ─── CHANNELS ───────────────────────────────────────────────────────────────
// Client → Gateway (0x0030) message JoinChannel { string channel_id = 1; }
message JoinChannel { message LeaveChannel { string channel_id = 1; }
string channel_id = 1;
}
// Client → Gateway (0x0031)
message LeaveChannel {
string channel_id = 1;
}
// Gateway → Client (0x0032) — full channel snapshot on join
message ChannelState { message ChannelState {
string channel_id = 1; string channel_id = 1;
string channel_name = 2; string channel_name = 2;
ChannelKind kind = 3; string kind = 3;
repeated Member members = 4; repeated MemberInfo members = 4;
string voice_endpoint = 5; // host:port of voice-node UDP string voice_endpoint = 5;
}
enum ChannelKind { message MemberInfo {
TEXT = 0;
VOICE = 1;
}
message Member {
string user_id = 1; string user_id = 1;
string nickname = 2; string nickname = 2;
bool in_voice = 3; bool in_voice = 3;
}
} }
// Broadcast → all channel members (0x0040 / 0x0041) message ChannelCreate {
message UserJoin {
string channel_id = 1; string channel_id = 1;
string user_id = 2; string channel_name = 2;
string nickname = 3; string kind = 3;
optional string guild_id = 4;
} }
message UserLeave { message ChannelDelete { string channel_id = 1; }
message ChannelList { repeated ChannelListItem channels = 1; }
message ChannelListItem {
string channel_id = 1; string channel_id = 1;
string user_id = 2; string channel_name = 2;
string kind = 3;
} }
// ─── CHAT ─────────────────────────────────────────────────────────────────── message UserJoin { string channel_id = 1; string user_id = 2; string nickname = 3; }
message UserLeave { string channel_id = 1; string user_id = 2; }
// ─── CHAT ────────────────────────────────────────────────────────────────────
// Client → Gateway, broadcast to channel (0x0020)
message ChatMessage { message ChatMessage {
string message_id = 1; // UUID v4, generated by client string message_id = 1;
string channel_id = 2; string channel_id = 2;
string sender_id = 3; // sender pubkey short form string sender_id = 3;
string content = 4; string content = 4;
int64 timestamp = 5; // Unix ms int64 timestamp = 5;
bool edited = 6;
optional string reply_to = 7;
} }
// Gateway → Client on join (0x0021) — recent history
message ChatHistory { message ChatHistory {
string channel_id = 1; string channel_id = 1;
repeated ChatMessage messages = 2; repeated ChatMessage messages = 2;
} }
// ─── VOICE STATE ──────────────────────────────────────────────────────────── message ReactionPayload {
string message_id = 1;
string channel_id = 2;
string emoji = 3;
string user_id = 4;
}
message MessageEdit {
string message_id = 1;
string channel_id = 2;
string content = 3;
}
message MessageDelete {
string message_id = 1;
string channel_id = 2;
}
// ─── VOICE ──────────────────────────────────────────────────────────────────
// Broadcast on voice state change (0x0011)
message VoiceState { message VoiceState {
string user_id = 1; string user_id = 1;
string channel_id = 2; string channel_id = 2;
string state = 3;
enum State {
SILENT = 0;
SPEAKING = 1;
MUTED = 2;
DEAFENED = 3;
}
State state = 3;
} }
// ─── ERRORS ───────────────────────────────────────────────────────────────── // ─── GUILDS ─────────────────────────────────────────────────────────────────
// Any direction (0x00F0) message GuildCreate { string name = 1; }
message Error {
uint32 code = 1; // ErrorCode enum value message GuildInfo {
string message = 2; string id = 1;
string owner_id = 2;
string name = 3;
int64 member_count = 4;
int64 created_at = 5;
} }
// ─── DISCONNECT ───────────────────────────────────────────────────────────── message GuildDelete { string guild_id = 1; }
message GuildList { repeated GuildInfo guilds = 1; }
// Any direction (0x00FF) — graceful close message GuildMemberJoin { string guild_id = 1; }
message Disconnect { message GuildMemberLeave { string guild_id = 1; string user_id = 2; }
string reason = 1; // optional human-readable reason message GuildMemberKick { string guild_id = 1; string user_id = 2; }
message RoleCreate {
string guild_id = 1;
string name = 2;
optional string color = 3;
optional uint64 permissions = 4;
string id = 5;
}
message RoleDelete {
string guild_id = 1;
string role_id = 2;
}
message InviteCreate {
string guild_id = 1;
optional int64 max_uses = 2;
optional int64 expires_in_seconds = 3;
}
message InviteInfo {
string id = 1;
string guild_id = 2;
string guild_name = 3;
string code = 4;
string creator_id = 5;
optional int64 max_uses = 6;
int64 uses = 7;
optional int64 expires_at = 8;
int64 created_at = 9;
}
message InviteAccept {
string code = 1;
string guild_id = 2;
string guild_name = 3;
}
message InviteDelete {
string guild_id = 1;
string invite_id = 2;
}
message GuildAuditLogFetch {
string guild_id = 1;
int64 limit = 2;
}
message AuditLogEntry {
string id = 1;
string guild_id = 2;
string actor_id = 3;
string action = 4;
optional string target_id = 5;
optional string target_type = 6;
optional string reason = 7;
int64 created_at = 8;
}
message GuildAuditLog {
string guild_id = 1;
repeated AuditLogEntry entries = 2;
}
message GuildMemberListFetch { string guild_id = 1; }
message GuildMemberInfo {
string user_id = 1;
string nickname = 2;
int64 joined_at = 3;
string role_color = 4;
string role_name = 5;
bool is_owner = 6;
}
message GuildMemberList {
string guild_id = 1;
repeated GuildMemberInfo members = 2;
}
message RoleAssign {
string guild_id = 1;
string user_id = 2;
string role_id = 3;
}
message GuildRoleListFetch { string guild_id = 1; }
message GuildRoleInfo {
string id = 1;
string guild_id = 2;
string name = 3;
string color = 4;
uint64 permissions = 5;
int32 position = 6;
}
message GuildRoleList {
string guild_id = 1;
repeated GuildRoleInfo roles = 2;
}
// ─── DIRECT MESSAGES ────────────────────────────────────────────────────────
message DmStart {
string target_user_id = 1;
}
message DmStartResponse {
string dm_id = 1;
string other_user_id = 2;
string other_nickname = 3;
repeated DmMessage messages = 4;
uint32 unread_count = 5;
}
message DmMessage {
string dm_id = 1;
string sender_id = 2;
string content = 3;
int64 timestamp = 4;
}
message DmHistory {
string dm_id = 1;
repeated DmMessage messages = 2;
optional string search_query = 3;
optional int64 limit = 4;
}
message DmReadAck {
string dm_id = 1;
}
// ─── FRIENDS ────────────────────────────────────────────────────────────────
message FriendRequest { string to_user_id = 1; }
message FriendAccept { string from_user_id = 1; }
message FriendDecline { string from_user_id = 1; }
message FriendRemove { string user_id = 1; }
message FriendInfo {
string user_id = 1;
string nickname = 2;
string status = 3;
int64 since = 4;
}
message FriendList { repeated FriendInfo friends = 1; }
message BlockUser { string user_id = 1; }
message UnblockUser { string user_id = 1; }
message BlockList { repeated string blocked = 1; }
message FriendEvent {
string event = 1;
string user_id = 2;
optional string nickname = 3;
}
// ─── PRESENCE ──────────────────────────────────────────────────────────────
message PresenceUpdate {
string status = 1;
optional string activity_type = 2;
optional string activity_text = 3;
}
message PresenceInfo {
string user_id = 1;
string nickname = 2;
string status = 3;
optional string activity_type = 4;
optional string activity_text = 5;
}
message PresenceSync { repeated PresenceInfo presences = 1; }
message PresenceEvent {
string user_id = 1;
string nickname = 2;
string status = 3;
optional string activity_type = 4;
optional string activity_text = 5;
}
// ─── READ RECEIPT & TYPING ─────────────────────────────────────────────────
message ReadReceipt {
string channel_id = 1;
string last_read_message_id = 2;
string user_id = 3;
}
message TypingStart {
string channel_id = 1;
}
// ─── AD-HOC RESPONSE / BROADCAST TYPES ──────────────────────────────────────
message ReadReceiptBroadcast {
string channel_id = 1;
string user_id = 2;
string last_read_message_id = 3;
}
message UserRoleUpdate {
string user_id = 1;
string guild_id = 2;
string color = 3;
}
message TypingBroadcast {
string user_id = 1;
string nickname = 2;
string channel_id = 3;
}
message SimpleResponse {
bool ok = 1;
optional string user_id = 2;
optional string role_id = 3;
} }