mirror of
https://github.com/shiroyashik/sculptor.git
synced 2025-12-06 13:01:12 +03:00
Начало положено...
This commit is contained in:
commit
3fd49300db
12 changed files with 2361 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
/Assets-main
|
||||||
|
output.log
|
||||||
1801
Cargo.lock
generated
Normal file
1801
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
36
Cargo.toml
Normal file
36
Cargo.toml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
[package]
|
||||||
|
name = "sculptor"
|
||||||
|
authors = ["Shiroyashik <shiroyashik@shsr.ru>"]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[workspace]
|
||||||
|
members = ["elyby-api"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Logging
|
||||||
|
log = "0.4.21"
|
||||||
|
fern = { version = "0.6.2", features = ["colored"] }
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
anyhow = "1.0.83"
|
||||||
|
|
||||||
|
# Serialization
|
||||||
|
chrono = { version = "0.4.38", features = ["now"] }
|
||||||
|
serde = { version = "1.0.201", features = ["derive"] }
|
||||||
|
|
||||||
|
# Other
|
||||||
|
elyby-api = { path = "./elyby-api" }
|
||||||
|
dashmap = "5.5.3"
|
||||||
|
|
||||||
|
# Crypto
|
||||||
|
ring = "0.17.8"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
||||||
|
# Web framework
|
||||||
|
axum = { version = "0.7.5", features = ["ws", "macros"] }
|
||||||
|
tower-http = { version = "0.5.2", features = ["trace"] }
|
||||||
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
14
elyby-api/Cargo.toml
Normal file
14
elyby-api/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "elyby-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.83"
|
||||||
|
log = "0.4.21"
|
||||||
|
reqwest = "0.12.4"
|
||||||
|
serde = { version = "1.0.201", features = ["derive"] }
|
||||||
|
serde_json = "1.0.117"
|
||||||
|
tokio = { version = "1.37.0", features = ["full"] }
|
||||||
31
elyby-api/src/lib.rs
Normal file
31
elyby-api/src/lib.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
pub async fn has_joined(server_id: &str, username: &str) -> Result<bool> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client.get(
|
||||||
|
format!("http://minecraft.ely.by/session/hasJoined?serverId={server_id}&username={username}")).send().await?;
|
||||||
|
debug!("{res:?}");
|
||||||
|
match res.status().as_u16() {
|
||||||
|
200 => Ok(true),
|
||||||
|
401 => Ok(false),
|
||||||
|
_ => Err(anyhow!("Unknown code: {}", res.status().as_u16()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_has_joined() {
|
||||||
|
let result = has_joined("0f8fef917f1f62b963804d822b67fe6f59aad7d", "test").await.unwrap();
|
||||||
|
assert_eq!(result, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[cfg(test)]
|
||||||
|
// mod tests {
|
||||||
|
// use super::*;
|
||||||
|
|
||||||
|
// #[test]
|
||||||
|
// fn it_works() {
|
||||||
|
// let result = add(2, 2);
|
||||||
|
// assert_eq!(result, 4);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
63
src/auth.rs
Normal file
63
src/auth.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use axum::{extract::{Query, State}, routing::get, Router, debug_handler};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use ring::digest::{self, digest};
|
||||||
|
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
pub fn router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/id", get(id))
|
||||||
|
.route("/verify", get(verify))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Id {username: String}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
async fn id(
|
||||||
|
Query(query): Query<Id>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> String {
|
||||||
|
let server_id = bytes_into_string(&digest(&digest::SHA1_FOR_LEGACY_USE_ONLY, &rand()).as_ref()[0 .. 20]);
|
||||||
|
let state = state.pending.lock().expect("Mutex poisoned!");
|
||||||
|
state.insert(server_id.clone(), query.username);
|
||||||
|
server_id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct Verify {id: String}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
async fn verify(
|
||||||
|
Query(query): Query<Verify>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> String {
|
||||||
|
let server_id = query.id.clone();
|
||||||
|
let username = state.pending.lock().expect("Mutex poisoned!").remove(&server_id).unwrap().1;
|
||||||
|
if !elyby_api::has_joined(&server_id, &username).await.unwrap() {
|
||||||
|
return String::from("failed to verify")
|
||||||
|
}
|
||||||
|
let authenticated = state.authenticated.lock().expect("Mutex poisoned!");
|
||||||
|
authenticated.insert(server_id.clone(), username);
|
||||||
|
format!("{server_id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rand() -> [u8; 50] {
|
||||||
|
use rand::{Rng, thread_rng};
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
let distr = rand::distributions::Uniform::new_inclusive(0, 255);
|
||||||
|
let mut nums: [u8; 50] = [0u8; 50];
|
||||||
|
for x in &mut nums {
|
||||||
|
*x = rng.sample(distr);
|
||||||
|
}
|
||||||
|
nums
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bytes_into_string(code: &[u8]) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut result = String::new();
|
||||||
|
for byte in code {
|
||||||
|
write!(result, "{:02x}", byte).unwrap();
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
137
src/main.rs
Normal file
137
src/main.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::{
|
||||||
|
extract::Path,
|
||||||
|
routing::{delete, get, post, put},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use chrono::prelude::*;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use fern::colors::{Color, ColoredLevelConfig};
|
||||||
|
use log::info;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
|
// WebSocket worker
|
||||||
|
mod ws;
|
||||||
|
use ws::handler;
|
||||||
|
|
||||||
|
// API
|
||||||
|
mod auth;
|
||||||
|
use auth as api_auth;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Userinfo {
|
||||||
|
id: usize
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
authenticated: Arc<Mutex<DashMap<String, String>>>, // <SHA1, USERNAME>
|
||||||
|
pending: Arc<Mutex<DashMap<String, String>>>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
println!("The Sculptor");
|
||||||
|
let colors = ColoredLevelConfig::new()
|
||||||
|
.info(Color::Green)
|
||||||
|
.debug(Color::Magenta)
|
||||||
|
.trace(Color::Cyan)
|
||||||
|
.warn(Color::Yellow);
|
||||||
|
fern::Dispatch::new()
|
||||||
|
.format(move |out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"[{} {} {}] {}",
|
||||||
|
Local::now().to_rfc3339_opts(SecondsFormat::Millis, true),
|
||||||
|
colors.color(record.level()),
|
||||||
|
record.target(),
|
||||||
|
message
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.level(log::LevelFilter::Debug)
|
||||||
|
// .level_for("hyper", log::LevelFilter::Info)
|
||||||
|
.chain(std::io::stdout())
|
||||||
|
.chain(fern::log_file("output.log")?)
|
||||||
|
.apply()?;
|
||||||
|
|
||||||
|
// Config init here
|
||||||
|
let listen = "0.0.0.0:6665";
|
||||||
|
|
||||||
|
// State init here
|
||||||
|
let state = AppState {
|
||||||
|
authenticated: Arc::new(Mutex::new(DashMap::new())),
|
||||||
|
pending: Arc::new(Mutex::new(DashMap::new()))
|
||||||
|
};
|
||||||
|
|
||||||
|
let api = Router::new()
|
||||||
|
.nest(
|
||||||
|
"//auth",
|
||||||
|
api_auth::router()
|
||||||
|
) // check Auth; return 200 OK if token valid
|
||||||
|
.route(
|
||||||
|
"/limits",
|
||||||
|
get(|| async { "@toomanylimits" })
|
||||||
|
) // Need more info :( TODO:
|
||||||
|
.route(
|
||||||
|
"/version",
|
||||||
|
get(|| async { "{\"release\":\"2.7.1\",\"prerelease\":\"2.7.1\"}" }),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/motd",
|
||||||
|
get(|| async { "\"written by an black cat :3 mew\"" }),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/equip",
|
||||||
|
post(|| async { "Do it! NOW!" })
|
||||||
|
) // set Equipped; TODO:
|
||||||
|
.route(
|
||||||
|
"/:owner/:id",
|
||||||
|
get(|Path((owner, id)): Path<(String, String)>| async move {
|
||||||
|
format!("getting user {id}, owner {owner}")
|
||||||
|
}),
|
||||||
|
) // get Avatar
|
||||||
|
.route(
|
||||||
|
"/:avatar",
|
||||||
|
put(|Path(avatar): Path<String>| async move { format!("put {avatar}") }),
|
||||||
|
) // put Avatar
|
||||||
|
.route(
|
||||||
|
"/:avatar",
|
||||||
|
delete(|Path(avatar): Path<String>| async move { format!("delete {avatar}") }),
|
||||||
|
); // delete Avatar
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.nest("/api", api)
|
||||||
|
.route("/ws", get(handler))
|
||||||
|
.layer(TraceLayer::new_for_http().on_request(()))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind(listen).await?;
|
||||||
|
info!("Listening on {}", listener.local_addr()?);
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.with_graceful_shutdown(shutdown_signal())
|
||||||
|
.await?;
|
||||||
|
info!("Serve stopped. Closing...");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown_signal() {
|
||||||
|
let ctrl_c = async {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("failed to install Ctrl+C handler");
|
||||||
|
};
|
||||||
|
#[cfg(unix)]
|
||||||
|
let terminate = async {
|
||||||
|
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||||
|
.expect("failed to install signal handler")
|
||||||
|
.recv()
|
||||||
|
.await;
|
||||||
|
};
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
let terminate = std::future::pending::<()>();
|
||||||
|
tokio::select! {
|
||||||
|
() = ctrl_c => {},
|
||||||
|
() = terminate => {},
|
||||||
|
}
|
||||||
|
info!("Terminate signal received");
|
||||||
|
}
|
||||||
89
src/ws/c2s.rs
Normal file
89
src/ws/c2s.rs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
use super::MessageLoadError;
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum C2SMessage<'a> {
|
||||||
|
Token(&'a [u8]) = 0,
|
||||||
|
Ping(u32, bool, &'a [u8]) = 1,
|
||||||
|
Sub(u128) = 2, // owo
|
||||||
|
Unsub(u128) = 3,
|
||||||
|
}
|
||||||
|
// 6 - 6
|
||||||
|
impl<'a> TryFrom<&'a [u8]> for C2SMessage<'a> {
|
||||||
|
type Error = MessageLoadError;
|
||||||
|
fn try_from(buf: &'a [u8]) -> Result<Self, <Self as TryFrom<&'a [u8]>>::Error> {
|
||||||
|
if buf.len() == 0 {
|
||||||
|
Err(MessageLoadError::BadLength("C2SMessage", 1, false, 0))
|
||||||
|
} else {
|
||||||
|
match buf[0] {
|
||||||
|
0 => Ok(C2SMessage::Token(&buf[1..])),
|
||||||
|
1 => {
|
||||||
|
if buf.len() >= 6 {
|
||||||
|
Ok(C2SMessage::Ping(
|
||||||
|
u32::from_be_bytes((&buf[1..5]).try_into().unwrap()),
|
||||||
|
buf[5] != 0,
|
||||||
|
&buf[6..],
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(MessageLoadError::BadLength(
|
||||||
|
"C2SMessage::Ping",
|
||||||
|
6,
|
||||||
|
false,
|
||||||
|
buf.len(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
if buf.len() == 17 {
|
||||||
|
Ok(C2SMessage::Sub(u128::from_be_bytes(
|
||||||
|
(&buf[1..]).try_into().unwrap(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Err(MessageLoadError::BadLength(
|
||||||
|
"C2SMessage::Sub",
|
||||||
|
17,
|
||||||
|
true,
|
||||||
|
buf.len(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => {
|
||||||
|
if buf.len() == 17 {
|
||||||
|
Ok(C2SMessage::Unsub(u128::from_be_bytes(
|
||||||
|
(&buf[1..]).try_into().unwrap(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Err(MessageLoadError::BadLength(
|
||||||
|
"C2SMessage::Unsub",
|
||||||
|
17,
|
||||||
|
true,
|
||||||
|
buf.len(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a => Err(MessageLoadError::BadEnum(
|
||||||
|
"C2SMessage.type",
|
||||||
|
0..=3,
|
||||||
|
a.into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Into<Box<[u8]>> for C2SMessage<'a> {
|
||||||
|
fn into(self) -> Box<[u8]> {
|
||||||
|
use std::iter;
|
||||||
|
let a: Box<[u8]> = match self {
|
||||||
|
C2SMessage::Token(t) => iter::once(0).chain(t.into_iter().copied()).collect(),
|
||||||
|
C2SMessage::Ping(p, s, d) => iter::once(1)
|
||||||
|
.chain(p.to_be_bytes())
|
||||||
|
.chain(iter::once(s.into()))
|
||||||
|
.chain(d.into_iter().copied())
|
||||||
|
.collect(),
|
||||||
|
C2SMessage::Sub(s) => iter::once(2).chain(s.to_be_bytes()).collect(),
|
||||||
|
C2SMessage::Unsub(s) => iter::once(3).chain(s.to_be_bytes()).collect(),
|
||||||
|
};
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/ws/errors.rs
Normal file
42
src/ws/errors.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use std::fmt::*;
|
||||||
|
use std::ops::RangeInclusive;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum MessageLoadError {
|
||||||
|
BadEnum(&'static str, RangeInclusive<usize>, usize),
|
||||||
|
BadLength(&'static str, usize, bool, usize),
|
||||||
|
}
|
||||||
|
impl Display for MessageLoadError {
|
||||||
|
fn fmt(&self, fmt: &mut Formatter) -> Result {
|
||||||
|
match self {
|
||||||
|
Self::BadEnum(f, r, c) => write!(
|
||||||
|
fmt,
|
||||||
|
"invalid value of {f}: must be {} to {} inclusive, got {c}",
|
||||||
|
r.start(),
|
||||||
|
r.end()
|
||||||
|
),
|
||||||
|
Self::BadLength(f, n, e, c) => write!(
|
||||||
|
fmt,
|
||||||
|
"buffer wrong size for {f}: must be {} {n} bytes, got {c}",
|
||||||
|
if *e { "exactly" } else { "at least" }
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
#[test]
|
||||||
|
fn message_load_error_display() {
|
||||||
|
use MessageLoadError::*;
|
||||||
|
assert_eq!(
|
||||||
|
BadEnum("foo", 3..=5, 7).to_string(),
|
||||||
|
"invalid value of foo: must be 3 to 5 inclusive, got 7"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BadLength("bar", 17, false, 12).to_string(),
|
||||||
|
"buffer wrong size for bar: must be at least 17 bytes, got 12"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BadLength("bar", 17, true, 19).to_string(),
|
||||||
|
"buffer wrong size for bar: must be exactly 17 bytes, got 19"
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/ws/handler.rs
Normal file
51
src/ws/handler.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use axum::{extract::{ws::{Message, WebSocket}, WebSocketUpgrade}, response::Response};
|
||||||
|
use log::{error, info, warn};
|
||||||
|
|
||||||
|
use crate::ws::C2SMessage;
|
||||||
|
|
||||||
|
pub async fn handler(ws: WebSocketUpgrade) -> Response {
|
||||||
|
ws.on_upgrade(handle_socket)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_socket(mut socket: WebSocket) {
|
||||||
|
while let Some(msg) = socket.recv().await {
|
||||||
|
info!("{msg:?}");
|
||||||
|
let mut msg = if let Ok(msg) = msg {
|
||||||
|
msg
|
||||||
|
} else {
|
||||||
|
// if reached here - client disconnected
|
||||||
|
warn!("ws disconnected!");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Work with code here
|
||||||
|
let msg_array = msg.clone().into_data();
|
||||||
|
let msg_array = msg_array.as_slice();
|
||||||
|
|
||||||
|
let newmsg = match C2SMessage::try_from(msg_array) {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
error!("MessageLoadError: {e:?}");
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
match newmsg {
|
||||||
|
C2SMessage::Token(token) => {
|
||||||
|
// TODO: Authenticated check
|
||||||
|
msg = Message::Binary(vec![0])
|
||||||
|
},
|
||||||
|
// C2SMessage::Ping(_, _, _) => todo!(),
|
||||||
|
// C2SMessage::Sub(_) => todo!(),
|
||||||
|
// C2SMessage::Unsub(_) => todo!(),
|
||||||
|
_ => ()
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("{newmsg:?}");
|
||||||
|
|
||||||
|
if socket.send(msg).await.is_err() {
|
||||||
|
// if reached here - client disconnected
|
||||||
|
warn!("ws disconnected!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/ws/mod.rs
Normal file
9
src/ws/mod.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
mod c2s;
|
||||||
|
mod s2c;
|
||||||
|
mod handler;
|
||||||
|
mod errors;
|
||||||
|
|
||||||
|
pub use c2s::C2SMessage;
|
||||||
|
pub use s2c::S2CMessage;
|
||||||
|
pub use handler::handler;
|
||||||
|
pub use errors::MessageLoadError;
|
||||||
85
src/ws/s2c.rs
Normal file
85
src/ws/s2c.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
use super::MessageLoadError;
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum S2CMessage<'a> {
|
||||||
|
Auth = 0,
|
||||||
|
Ping(u128, u32, bool, &'a [u8]) = 1,
|
||||||
|
Event(u128) = 2,
|
||||||
|
Toast(u8, &'a str, Option<&'a str>) = 3,
|
||||||
|
Chat(&'a str) = 4,
|
||||||
|
Notice(u8) = 5,
|
||||||
|
}
|
||||||
|
impl<'a> TryFrom<&'a [u8]> for S2CMessage<'a> {
|
||||||
|
type Error = MessageLoadError;
|
||||||
|
fn try_from(buf: &'a [u8]) -> Result<Self, <Self as TryFrom<&'a [u8]>>::Error> {
|
||||||
|
if buf.len() == 0 {
|
||||||
|
Err(MessageLoadError::BadLength("S2CMessage", 1, false, 0))
|
||||||
|
} else {
|
||||||
|
use MessageLoadError::*;
|
||||||
|
use S2CMessage::*;
|
||||||
|
match buf[0] {
|
||||||
|
0 => {
|
||||||
|
if buf.len() == 1 {
|
||||||
|
Ok(Auth)
|
||||||
|
} else {
|
||||||
|
Err(BadLength("S2CMessage::Auth", 1, true, buf.len()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => {
|
||||||
|
if buf.len() >= 22 {
|
||||||
|
Ok(Ping(
|
||||||
|
u128::from_be_bytes((&buf[1..17]).try_into().unwrap()),
|
||||||
|
u32::from_be_bytes((&buf[17..21]).try_into().unwrap()),
|
||||||
|
buf[21] != 0,
|
||||||
|
&buf[22..],
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Err(BadLength("S2CMessage::Ping", 22, false, buf.len()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
if buf.len() == 17 {
|
||||||
|
Ok(Event(u128::from_be_bytes(
|
||||||
|
(&buf[1..17]).try_into().unwrap(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
Err(BadLength("S2CMessage::Event", 17, true, buf.len()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
3 => todo!(),
|
||||||
|
4 => todo!(),
|
||||||
|
5 => todo!(),
|
||||||
|
a => Err(BadEnum("S2CMessage.type", 0..=5, a.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> Into<Box<[u8]>> for S2CMessage<'a> {
|
||||||
|
fn into(self) -> Box<[u8]> {
|
||||||
|
use std::iter::once;
|
||||||
|
use S2CMessage::*;
|
||||||
|
match self {
|
||||||
|
Auth => Box::new([0]),
|
||||||
|
Ping(u, i, s, d) => once(1)
|
||||||
|
.chain(u.to_be_bytes().iter().copied())
|
||||||
|
.chain(i.to_be_bytes().iter().copied())
|
||||||
|
.chain(once(if s { 1 } else { 0 }))
|
||||||
|
.chain(d.into_iter().copied())
|
||||||
|
.collect(),
|
||||||
|
Event(u) => once(2).chain(u.to_be_bytes().iter().copied()).collect(),
|
||||||
|
Toast(t, h, d) => once(3)
|
||||||
|
.chain(once(t))
|
||||||
|
.chain(h.as_bytes().into_iter().copied())
|
||||||
|
.chain(
|
||||||
|
d.into_iter()
|
||||||
|
.map(|s| once(0).chain(s.as_bytes().into_iter().copied()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.collect(),
|
||||||
|
Chat(c) => once(4).chain(c.as_bytes().iter().copied()).collect(),
|
||||||
|
Notice(t) => Box::new([5, t]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue