734 lines
23 KiB
Rust
734 lines
23 KiB
Rust
//! Server messages (`server.slsknet.org:2242`), u32 codes.
|
|
|
|
use bytes::Bytes;
|
|
use md5::{Digest, Md5};
|
|
use std::net::{Ipv4Addr, SocketAddrV4};
|
|
|
|
use crate::wire::{Reader, Writer};
|
|
use crate::Result;
|
|
|
|
/// Client version fields sent at login. 160 is the protocol version spoken
|
|
/// by current clients (Nicotine+ uses the same).
|
|
pub const VERSION: u32 = 160;
|
|
pub const MINOR_VERSION: u32 = 1;
|
|
|
|
pub mod code {
|
|
pub const LOGIN: u32 = 1;
|
|
pub const SET_LISTEN_PORT: u32 = 2;
|
|
pub const GET_PEER_ADDRESS: u32 = 3;
|
|
pub const WATCH_USER: u32 = 5;
|
|
pub const UNWATCH_USER: u32 = 6;
|
|
pub const GET_USER_STATUS: u32 = 7;
|
|
pub const SAY_CHATROOM: u32 = 13;
|
|
pub const JOIN_ROOM: u32 = 14;
|
|
pub const LEAVE_ROOM: u32 = 15;
|
|
pub const USER_JOINED_ROOM: u32 = 16;
|
|
pub const USER_LEFT_ROOM: u32 = 17;
|
|
pub const CONNECT_TO_PEER: u32 = 18;
|
|
pub const MESSAGE_USER: u32 = 22;
|
|
pub const MESSAGE_ACKED: u32 = 23;
|
|
pub const FILE_SEARCH: u32 = 26;
|
|
pub const PING: u32 = 32;
|
|
pub const SHARED_FOLDERS_AND_FILES: u32 = 35;
|
|
pub const GET_USER_STATS: u32 = 36;
|
|
pub const ADD_THING_I_LIKE: u32 = 51;
|
|
pub const REMOVE_THING_I_LIKE: u32 = 52;
|
|
pub const USER_INTERESTS: u32 = 57;
|
|
pub const ADD_THING_I_HATE: u32 = 117;
|
|
pub const REMOVE_THING_I_HATE: u32 = 118;
|
|
pub const SIMILAR_USERS: u32 = 110;
|
|
pub const ROOM_LIST: u32 = 64;
|
|
pub const HAVE_NO_PARENTS: u32 = 71;
|
|
pub const PARENT_MIN_SPEED: u32 = 83;
|
|
pub const PARENT_SPEED_RATIO: u32 = 84;
|
|
pub const EMBEDDED_MESSAGE: u32 = 93; // wraps a distributed search
|
|
pub const ACCEPT_CHILDREN: u32 = 100;
|
|
pub const NET_INFO: u32 = 102;
|
|
pub const WISHLIST_INTERVAL: u32 = 104;
|
|
pub const BRANCH_LEVEL: u32 = 126;
|
|
pub const BRANCH_ROOT: u32 = 127;
|
|
pub const EXCLUDED_SEARCH_PHRASES: u32 = 160;
|
|
pub const CANNOT_CONNECT: u32 = 1001;
|
|
}
|
|
|
|
// ---- outgoing ----
|
|
|
|
pub fn login(username: &str, password: &str) -> Vec<u8> {
|
|
let hash = hex(Md5::digest(format!("{username}{password}").as_bytes()));
|
|
Writer::new()
|
|
.string(username)
|
|
.string(password)
|
|
.u32(VERSION)
|
|
.string(&hash)
|
|
.u32(MINOR_VERSION)
|
|
.finish_u32(code::LOGIN)
|
|
}
|
|
|
|
pub fn set_listen_port(port: u32) -> Vec<u8> {
|
|
Writer::new().u32(port).finish_u32(code::SET_LISTEN_PORT)
|
|
}
|
|
|
|
pub fn file_search(token: u32, query: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.u32(token)
|
|
.string(query)
|
|
.finish_u32(code::FILE_SEARCH)
|
|
}
|
|
|
|
pub fn shared_folders_and_files(dirs: u32, files: u32) -> Vec<u8> {
|
|
Writer::new()
|
|
.u32(dirs)
|
|
.u32(files)
|
|
.finish_u32(code::SHARED_FOLDERS_AND_FILES)
|
|
}
|
|
|
|
/// Tell the server whether we need a distributed parent.
|
|
pub fn accept_children(accept: bool) -> Vec<u8> {
|
|
Writer::new().bool(accept).finish_u32(code::ACCEPT_CHILDREN)
|
|
}
|
|
|
|
pub fn branch_level(level: u32) -> Vec<u8> {
|
|
Writer::new().u32(level).finish_u32(code::BRANCH_LEVEL)
|
|
}
|
|
|
|
pub fn branch_root(root: &str) -> Vec<u8> {
|
|
Writer::new().string(root).finish_u32(code::BRANCH_ROOT)
|
|
}
|
|
|
|
pub fn ping() -> Vec<u8> {
|
|
Writer::new().finish_u32(code::PING)
|
|
}
|
|
|
|
pub fn get_peer_address(username: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.string(username)
|
|
.finish_u32(code::GET_PEER_ADDRESS)
|
|
}
|
|
|
|
/// Ask the server to have `username` connect to us (indirect connection).
|
|
pub fn connect_to_peer(token: u32, username: &str, conn_type: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.u32(token)
|
|
.string(username)
|
|
.string(conn_type)
|
|
.finish_u32(code::CONNECT_TO_PEER)
|
|
}
|
|
|
|
pub fn have_no_parents(value: bool) -> Vec<u8> {
|
|
Writer::new().bool(value).finish_u32(code::HAVE_NO_PARENTS)
|
|
}
|
|
|
|
// ---- rooms / chat / users (outgoing) ----
|
|
|
|
/// Watch a user: the server answers with their stats and then pushes status
|
|
/// changes until unwatched. The buddy list rides on this.
|
|
pub fn watch_user(username: &str) -> Vec<u8> {
|
|
Writer::new().string(username).finish_u32(code::WATCH_USER)
|
|
}
|
|
|
|
pub fn unwatch_user(username: &str) -> Vec<u8> {
|
|
Writer::new().string(username).finish_u32(code::UNWATCH_USER)
|
|
}
|
|
|
|
pub fn get_user_status(username: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.string(username)
|
|
.finish_u32(code::GET_USER_STATUS)
|
|
}
|
|
|
|
pub fn get_user_stats(username: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.string(username)
|
|
.finish_u32(code::GET_USER_STATS)
|
|
}
|
|
|
|
/// Declare a like (code 51). These are what the server matches on when it
|
|
/// ranks similar users, so a client that never sends one is invisible to that
|
|
/// ranking however much it reads.
|
|
pub fn add_thing_i_like(thing: &str) -> Vec<u8> {
|
|
Writer::new().string(thing).finish_u32(code::ADD_THING_I_LIKE)
|
|
}
|
|
|
|
/// Take a like back (code 52).
|
|
pub fn remove_thing_i_like(thing: &str) -> Vec<u8> {
|
|
Writer::new().string(thing).finish_u32(code::REMOVE_THING_I_LIKE)
|
|
}
|
|
|
|
/// Declare a dislike (code 117).
|
|
pub fn add_thing_i_hate(thing: &str) -> Vec<u8> {
|
|
Writer::new().string(thing).finish_u32(code::ADD_THING_I_HATE)
|
|
}
|
|
|
|
/// Take a dislike back (code 118).
|
|
pub fn remove_thing_i_hate(thing: &str) -> Vec<u8> {
|
|
Writer::new().string(thing).finish_u32(code::REMOVE_THING_I_HATE)
|
|
}
|
|
|
|
/// A user's declared likes and hates (code 57).
|
|
pub fn user_interests(username: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.string(username)
|
|
.finish_u32(code::USER_INTERESTS)
|
|
}
|
|
|
|
/// Users the server ranks as similar to us by shared interests (code 110).
|
|
pub fn similar_users() -> Vec<u8> {
|
|
Writer::new().finish_u32(code::SIMILAR_USERS)
|
|
}
|
|
|
|
fn string_list(r: &mut Reader) -> Result<Vec<String>> {
|
|
let n = r.u32()?.min(1024) as usize;
|
|
let mut v = Vec::with_capacity(n.min(64));
|
|
for _ in 0..n {
|
|
v.push(r.string()?);
|
|
}
|
|
Ok(v)
|
|
}
|
|
|
|
pub fn say_chatroom(room: &str, message: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.string(room)
|
|
.string(message)
|
|
.finish_u32(code::SAY_CHATROOM)
|
|
}
|
|
|
|
/// The trailing u32 is the private flag; 0 joins/creates a public room.
|
|
pub fn join_room(room: &str) -> Vec<u8> {
|
|
Writer::new().string(room).u32(0).finish_u32(code::JOIN_ROOM)
|
|
}
|
|
|
|
pub fn leave_room(room: &str) -> Vec<u8> {
|
|
Writer::new().string(room).finish_u32(code::LEAVE_ROOM)
|
|
}
|
|
|
|
pub fn message_user(username: &str, message: &str) -> Vec<u8> {
|
|
Writer::new()
|
|
.string(username)
|
|
.string(message)
|
|
.finish_u32(code::MESSAGE_USER)
|
|
}
|
|
|
|
/// Acknowledge a private message so the server stops redelivering it.
|
|
pub fn message_acked(id: u32) -> Vec<u8> {
|
|
Writer::new().u32(id).finish_u32(code::MESSAGE_ACKED)
|
|
}
|
|
|
|
/// Ask for the room list (the server also pushes one after login).
|
|
pub fn room_list() -> Vec<u8> {
|
|
Writer::new().finish_u32(code::ROOM_LIST)
|
|
}
|
|
|
|
// ---- incoming ----
|
|
|
|
/// A user as a room roster reports them (join replies and join notices).
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct RoomUser {
|
|
pub username: String,
|
|
/// 0 offline · 1 away · 2 online.
|
|
pub status: u32,
|
|
pub avg_speed: u32,
|
|
pub file_count: u32,
|
|
pub dir_count: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ServerEvent {
|
|
LoginOk {
|
|
greeting: String,
|
|
own_ip: Ipv4Addr,
|
|
is_supporter: bool,
|
|
},
|
|
LoginFailed {
|
|
reason: String,
|
|
},
|
|
/// Another client wants a connection with us (or is answering our
|
|
/// indirect request): connect to them and send PierceFirewall(token).
|
|
ConnectToPeer {
|
|
username: String,
|
|
conn_type: String,
|
|
addr: SocketAddrV4,
|
|
token: u32,
|
|
privileged: bool,
|
|
},
|
|
/// Our indirect connection request to `token` failed.
|
|
CannotConnect {
|
|
token: u32,
|
|
username: Option<String>,
|
|
},
|
|
/// Answer to GetPeerAddress. A 0.0.0.0/port-0 address means offline.
|
|
UserAddress {
|
|
username: String,
|
|
addr: SocketAddrV4,
|
|
},
|
|
ExcludedSearchPhrases(Vec<String>),
|
|
/// Candidate distributed parents to connect to.
|
|
NetInfo(Vec<(String, SocketAddrV4)>),
|
|
/// A search relayed directly by the server (older path).
|
|
ServerSearch {
|
|
username: String,
|
|
token: u32,
|
|
query: String,
|
|
},
|
|
/// A distributed message wrapped by the server (code 93). The inner
|
|
/// bytes are a distributed frame starting with its code byte — decode
|
|
/// with `dist::decode`.
|
|
Embedded(Bytes),
|
|
/// Public rooms and their user counts (pushed after login, and on
|
|
/// request). Private-room sections in the payload are ignored.
|
|
/// Answer to user_interests: what they declared, verbatim.
|
|
UserInterests {
|
|
username: String,
|
|
likes: Vec<String>,
|
|
hates: Vec<String>,
|
|
},
|
|
/// Answer to similar_users: (username, rating) as the server ranks them.
|
|
SimilarUsers {
|
|
users: Vec<(String, u32)>,
|
|
},
|
|
RoomList {
|
|
rooms: Vec<(String, u32)>,
|
|
},
|
|
/// We are in the room now; the full roster rides along.
|
|
RoomJoined {
|
|
room: String,
|
|
users: Vec<RoomUser>,
|
|
},
|
|
RoomLeft {
|
|
room: String,
|
|
},
|
|
RoomUserJoined {
|
|
room: String,
|
|
user: RoomUser,
|
|
},
|
|
RoomUserLeft {
|
|
room: String,
|
|
username: String,
|
|
},
|
|
/// A line said in a room we are in.
|
|
RoomChat {
|
|
room: String,
|
|
username: String,
|
|
message: String,
|
|
},
|
|
/// A private message. Ack with `message_acked(id)` or the server
|
|
/// redelivers it on every login.
|
|
PrivateMessage {
|
|
id: u32,
|
|
timestamp: u32,
|
|
username: String,
|
|
message: String,
|
|
},
|
|
/// Answer to watch_user.
|
|
UserWatched {
|
|
username: String,
|
|
exists: bool,
|
|
status: u32,
|
|
avg_speed: u32,
|
|
file_count: u32,
|
|
dir_count: u32,
|
|
},
|
|
/// Pushed for watched users, and the answer to get_user_status.
|
|
UserStatus {
|
|
username: String,
|
|
status: u32,
|
|
privileged: bool,
|
|
},
|
|
/// Answer to get_user_stats.
|
|
UserStats {
|
|
username: String,
|
|
avg_speed: u32,
|
|
file_count: u32,
|
|
dir_count: u32,
|
|
},
|
|
Unhandled {
|
|
code: u32,
|
|
},
|
|
}
|
|
|
|
pub fn decode(code: u32, payload: Bytes) -> Result<ServerEvent> {
|
|
let mut r = Reader::new(payload);
|
|
Ok(match code {
|
|
code::LOGIN => {
|
|
if r.bool()? {
|
|
let greeting = r.string()?;
|
|
let own_ip = r.ipv4()?;
|
|
// Trailing hash + supporter flag are absent on some servers.
|
|
let _hash = r.string().unwrap_or_default();
|
|
let is_supporter = r.bool().unwrap_or(false);
|
|
ServerEvent::LoginOk {
|
|
greeting,
|
|
own_ip,
|
|
is_supporter,
|
|
}
|
|
} else {
|
|
ServerEvent::LoginFailed {
|
|
reason: r.string()?,
|
|
}
|
|
}
|
|
}
|
|
code::CONNECT_TO_PEER => {
|
|
let username = r.string()?;
|
|
let conn_type = r.string()?;
|
|
let ip = r.ipv4()?;
|
|
let port = r.u32()?;
|
|
let token = r.u32()?;
|
|
let privileged = r.bool().unwrap_or(false);
|
|
ServerEvent::ConnectToPeer {
|
|
username,
|
|
conn_type,
|
|
addr: SocketAddrV4::new(ip, port as u16),
|
|
token,
|
|
privileged,
|
|
}
|
|
}
|
|
code::GET_PEER_ADDRESS => {
|
|
let username = r.string()?;
|
|
let ip = r.ipv4()?;
|
|
let port = r.u32()?;
|
|
// Trailing obfuscation fields ignored.
|
|
ServerEvent::UserAddress {
|
|
username,
|
|
addr: SocketAddrV4::new(ip, port as u16),
|
|
}
|
|
}
|
|
code::CANNOT_CONNECT => {
|
|
let token = r.u32()?;
|
|
let username = r.string().ok();
|
|
ServerEvent::CannotConnect { token, username }
|
|
}
|
|
code::NET_INFO => {
|
|
let count = r.u32()?.min(1000);
|
|
let mut parents = Vec::with_capacity(count as usize);
|
|
for _ in 0..count {
|
|
let username = r.string()?;
|
|
let ip = r.ipv4()?;
|
|
let port = r.u32()?;
|
|
parents.push((username, SocketAddrV4::new(ip, port as u16)));
|
|
}
|
|
ServerEvent::NetInfo(parents)
|
|
}
|
|
code::FILE_SEARCH => {
|
|
// Server-relayed search (same code we send): username, token, query.
|
|
let username = r.string()?;
|
|
let token = r.u32()?;
|
|
let query = r.string()?;
|
|
ServerEvent::ServerSearch {
|
|
username,
|
|
token,
|
|
query,
|
|
}
|
|
}
|
|
code::EMBEDDED_MESSAGE => ServerEvent::Embedded(r.rest()),
|
|
code::SAY_CHATROOM => ServerEvent::RoomChat {
|
|
room: r.string()?,
|
|
username: r.string()?,
|
|
message: r.string()?,
|
|
},
|
|
code::JOIN_ROOM => {
|
|
// room; N names; N statuses; N stats (speed u32, uploads u64,
|
|
// files u32, dirs u32); then slots-free and country sections we
|
|
// don't need. Sections beyond the names are tolerated missing —
|
|
// rosters from older servers stop early.
|
|
let room = r.string()?;
|
|
let count = r.u32()?.min(10_000) as usize;
|
|
let mut users: Vec<RoomUser> = Vec::with_capacity(count.min(4096));
|
|
for _ in 0..count {
|
|
users.push(RoomUser {
|
|
username: r.string()?,
|
|
..Default::default()
|
|
});
|
|
}
|
|
// Each section carries its own count; consume EVERY entry it
|
|
// declares (misaligned reads poison everything after), assign
|
|
// only the ones with a roster slot.
|
|
if r.has_more() {
|
|
let n = r.u32()?.min(10_000) as usize;
|
|
for i in 0..n {
|
|
let status = r.u32()?;
|
|
if let Some(user) = users.get_mut(i) {
|
|
user.status = status;
|
|
}
|
|
}
|
|
}
|
|
if r.has_more() {
|
|
let n = r.u32()?.min(10_000) as usize;
|
|
for i in 0..n {
|
|
let avg_speed = r.u32()?;
|
|
let _upload_count = r.u64()?;
|
|
let file_count = r.u32()?;
|
|
let dir_count = r.u32()?;
|
|
if let Some(user) = users.get_mut(i) {
|
|
user.avg_speed = avg_speed;
|
|
user.file_count = file_count;
|
|
user.dir_count = dir_count;
|
|
}
|
|
}
|
|
}
|
|
ServerEvent::RoomJoined { room, users }
|
|
}
|
|
code::LEAVE_ROOM => ServerEvent::RoomLeft { room: r.string()? },
|
|
code::USER_JOINED_ROOM => {
|
|
let room = r.string()?;
|
|
let username = r.string()?;
|
|
let status = r.u32().unwrap_or(2);
|
|
let avg_speed = r.u32().unwrap_or(0);
|
|
let _upload_count = r.u64().unwrap_or(0);
|
|
let file_count = r.u32().unwrap_or(0);
|
|
let dir_count = r.u32().unwrap_or(0);
|
|
ServerEvent::RoomUserJoined {
|
|
room,
|
|
user: RoomUser {
|
|
username,
|
|
status,
|
|
avg_speed,
|
|
file_count,
|
|
dir_count,
|
|
},
|
|
}
|
|
}
|
|
code::USER_LEFT_ROOM => ServerEvent::RoomUserLeft {
|
|
room: r.string()?,
|
|
username: r.string()?,
|
|
},
|
|
code::MESSAGE_USER => ServerEvent::PrivateMessage {
|
|
id: r.u32()?,
|
|
timestamp: r.u32()?,
|
|
username: r.string()?,
|
|
message: r.string()?,
|
|
},
|
|
code::WATCH_USER => {
|
|
let username = r.string()?;
|
|
let exists = r.bool()?;
|
|
let (mut status, mut avg_speed, mut file_count, mut dir_count) = (0, 0, 0, 0);
|
|
if exists && r.has_more() {
|
|
status = r.u32()?;
|
|
avg_speed = r.u32()?;
|
|
let _upload_count = r.u64()?;
|
|
file_count = r.u32()?;
|
|
dir_count = r.u32()?;
|
|
}
|
|
ServerEvent::UserWatched {
|
|
username,
|
|
exists,
|
|
status,
|
|
avg_speed,
|
|
file_count,
|
|
dir_count,
|
|
}
|
|
}
|
|
code::GET_USER_STATUS => ServerEvent::UserStatus {
|
|
username: r.string()?,
|
|
status: r.u32()?,
|
|
privileged: r.bool().unwrap_or(false),
|
|
},
|
|
code::GET_USER_STATS => {
|
|
let username = r.string()?;
|
|
let avg_speed = r.u32()?;
|
|
let _upload_count = r.u64()?;
|
|
ServerEvent::UserStats {
|
|
username,
|
|
avg_speed,
|
|
file_count: r.u32()?,
|
|
dir_count: r.u32()?,
|
|
}
|
|
}
|
|
code::USER_INTERESTS => {
|
|
let username = r.string()?;
|
|
let likes = string_list(&mut r)?;
|
|
let hates = string_list(&mut r)?;
|
|
ServerEvent::UserInterests { username, likes, hates }
|
|
}
|
|
code::SIMILAR_USERS => {
|
|
let n = r.u32()?.min(4096) as usize;
|
|
let mut users = Vec::with_capacity(n.min(256));
|
|
for _ in 0..n {
|
|
users.push((r.string()?, r.u32()?));
|
|
}
|
|
ServerEvent::SimilarUsers { users }
|
|
}
|
|
code::ROOM_LIST => {
|
|
// Public names + counts; the private-room sections that follow
|
|
// are deliberately not parsed.
|
|
let count = r.u32()?.min(50_000) as usize;
|
|
let mut names = Vec::with_capacity(count.min(8192));
|
|
for _ in 0..count {
|
|
names.push(r.string()?);
|
|
}
|
|
let ccount = if r.has_more() {
|
|
r.u32()?.min(50_000) as usize
|
|
} else {
|
|
0
|
|
};
|
|
let mut counts = Vec::with_capacity(ccount.min(8192));
|
|
for _ in 0..ccount {
|
|
counts.push(r.u32()?);
|
|
}
|
|
let rooms = names
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, name)| (name, counts.get(i).copied().unwrap_or(0)))
|
|
.collect();
|
|
ServerEvent::RoomList { rooms }
|
|
}
|
|
code::EXCLUDED_SEARCH_PHRASES => {
|
|
let count = r.u32()?.min(10_000);
|
|
let mut phrases = Vec::with_capacity(count as usize);
|
|
for _ in 0..count {
|
|
phrases.push(r.string()?);
|
|
}
|
|
ServerEvent::ExcludedSearchPhrases(phrases)
|
|
}
|
|
other => ServerEvent::Unhandled { code: other },
|
|
})
|
|
}
|
|
|
|
fn hex(digest: impl AsRef<[u8]>) -> String {
|
|
digest.as_ref().iter().map(|b| format!("{b:02x}")).collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn login_message_layout() {
|
|
let msg = login("user", "pass");
|
|
let mut r = Reader::new(Bytes::copy_from_slice(&msg[8..]));
|
|
assert_eq!(&msg[4..8], &1u32.to_le_bytes());
|
|
assert_eq!(r.string().unwrap(), "user");
|
|
assert_eq!(r.string().unwrap(), "pass");
|
|
assert_eq!(r.u32().unwrap(), VERSION);
|
|
// md5("userpass")
|
|
assert_eq!(r.string().unwrap(), "63e780c3f321d13109c71bf81805476e");
|
|
assert_eq!(r.u32().unwrap(), MINOR_VERSION);
|
|
}
|
|
|
|
#[test]
|
|
fn decode_room_traffic() {
|
|
// A said line.
|
|
let payload = Writer::new()
|
|
.string("indie")
|
|
.string("nikac")
|
|
.string("browse me")
|
|
.finish_u32(13);
|
|
match decode(13, Bytes::copy_from_slice(&payload[8..])).unwrap() {
|
|
ServerEvent::RoomChat {
|
|
room,
|
|
username,
|
|
message,
|
|
} => {
|
|
assert_eq!((room.as_str(), username.as_str()), ("indie", "nikac"));
|
|
assert_eq!(message, "browse me");
|
|
}
|
|
other => panic!("wrong event: {other:?}"),
|
|
}
|
|
|
|
// A join reply: 2 users, statuses and stats sections.
|
|
let payload = Writer::new()
|
|
.string("indie")
|
|
.u32(2)
|
|
.string("alice")
|
|
.string("bob")
|
|
.u32(2)
|
|
.u32(2)
|
|
.u32(1)
|
|
.u32(2)
|
|
.u32(500_000)
|
|
.u64(3)
|
|
.u32(1200)
|
|
.u32(80)
|
|
.u32(90_000)
|
|
.u64(0)
|
|
.u32(64)
|
|
.u32(7)
|
|
.finish_u32(14);
|
|
match decode(14, Bytes::copy_from_slice(&payload[8..])).unwrap() {
|
|
ServerEvent::RoomJoined { room, users } => {
|
|
assert_eq!(room, "indie");
|
|
assert_eq!(users.len(), 2);
|
|
assert_eq!(users[0].username, "alice");
|
|
assert_eq!(users[0].status, 2);
|
|
assert_eq!(users[0].avg_speed, 500_000);
|
|
assert_eq!(users[0].file_count, 1200);
|
|
assert_eq!(users[1].username, "bob");
|
|
assert_eq!(users[1].status, 1);
|
|
assert_eq!(users[1].dir_count, 7);
|
|
}
|
|
other => panic!("wrong event: {other:?}"),
|
|
}
|
|
|
|
// A private message.
|
|
let payload = Writer::new()
|
|
.u32(77)
|
|
.u32(1_788_000_000)
|
|
.string("seaside58")
|
|
.string("queued you up")
|
|
.bool(true)
|
|
.finish_u32(22);
|
|
match decode(22, Bytes::copy_from_slice(&payload[8..])).unwrap() {
|
|
ServerEvent::PrivateMessage {
|
|
id,
|
|
username,
|
|
message,
|
|
..
|
|
} => {
|
|
assert_eq!(id, 77);
|
|
assert_eq!(username, "seaside58");
|
|
assert_eq!(message, "queued you up");
|
|
}
|
|
other => panic!("wrong event: {other:?}"),
|
|
}
|
|
|
|
// A room list with a private tail we ignore.
|
|
let payload = Writer::new()
|
|
.u32(2)
|
|
.string("indie")
|
|
.string("electronic")
|
|
.u32(2)
|
|
.u32(214)
|
|
.u32(642)
|
|
.u32(0) // owned-private names, ignored
|
|
.u32(0) // owned-private counts, ignored
|
|
.finish_u32(64);
|
|
match decode(64, Bytes::copy_from_slice(&payload[8..])).unwrap() {
|
|
ServerEvent::RoomList { rooms } => {
|
|
assert_eq!(rooms.len(), 2);
|
|
assert_eq!(rooms[0], ("indie".to_string(), 214));
|
|
assert_eq!(rooms[1], ("electronic".to_string(), 642));
|
|
}
|
|
other => panic!("wrong event: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_connect_to_peer() {
|
|
let payload = Writer::new()
|
|
.string("bob")
|
|
.string("P")
|
|
.u8(4)
|
|
.u8(3)
|
|
.u8(2)
|
|
.u8(1) // 1.2.3.4 reversed on wire
|
|
.u32(2234)
|
|
.u32(99)
|
|
.bool(false)
|
|
.finish_u32(18);
|
|
let ev = decode(18, Bytes::copy_from_slice(&payload[8..])).unwrap();
|
|
match ev {
|
|
ServerEvent::ConnectToPeer {
|
|
username,
|
|
conn_type,
|
|
addr,
|
|
token,
|
|
..
|
|
} => {
|
|
assert_eq!(username, "bob");
|
|
assert_eq!(conn_type, "P");
|
|
assert_eq!(addr, "1.2.3.4:2234".parse().unwrap());
|
|
assert_eq!(token, 99);
|
|
}
|
|
other => panic!("wrong event: {other:?}"),
|
|
}
|
|
}
|
|
}
|