Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54eb7d4261 | |||
| 17eaf3e324 | |||
| 3c242a7b77 | |||
| 6fad5b1852 | |||
| 1c6b6de8f6 | |||
| 97fdbbf968 | |||
| f9d8fccf40 | |||
| 227a622adc | |||
| 61e9c2ac5c | |||
| 79da5e1146 | |||
| d6c98bcf10 | |||
| 8be13e14bc | |||
| a8b55f669f | |||
| 53ed4775ae | |||
| e1eff49be9 | |||
| b56b71b70c | |||
| bcbd6cb6c8 | |||
| ace356381a | |||
| 6821e546db | |||
| e67c55d67d | |||
| 040b70e1c5 |
3528
Cargo.lock
generated
3528
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,4 +1,4 @@
|
||||
cargo-features = ["codegen-backend"]
|
||||
# cargo-features = ["codegen-backend"]
|
||||
|
||||
[package]
|
||||
name = "openworm"
|
||||
@@ -17,14 +17,20 @@ tracing = "0.1.41"
|
||||
iris = { path = "./iris" }
|
||||
wgpu = "27.0.1"
|
||||
winit = "0.30.12"
|
||||
bincode = "2.0.1"
|
||||
zstd = "0.13.3"
|
||||
ron = "0.12.0"
|
||||
sled = "0.34.7"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
scrypt = "0.11.0"
|
||||
ed25519-dalek = { version = "3.0.0-pre.2", features = ["rand_core"] }
|
||||
rand = { version = "0.10.0-rc.5", features = ["chacha"] }
|
||||
rand = { version = "0.10.0-rc.5", features = ["chacha", "sys_rng"] }
|
||||
keyring = { version = "4.0.0-rc.3" }
|
||||
bitcode = "0.6.9"
|
||||
dashmap = "6.1.0"
|
||||
fjall = "3.0.1"
|
||||
time = { version = "0.3.47", features = ["local-offset"] }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
derive_more = { version = "2.1.1", features = ["deref", "deref_mut"] }
|
||||
keyring-core = "0.7.2"
|
||||
|
||||
[[bin]]
|
||||
name = "openworm-client"
|
||||
@@ -34,8 +40,8 @@ path = "src/bin/client/main.rs"
|
||||
name = "openworm-server"
|
||||
path = "src/bin/server/main.rs"
|
||||
|
||||
[profile.dev]
|
||||
codegen-backend = "cranelift"
|
||||
# [profile.dev]
|
||||
# codegen-backend = "cranelift"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 1
|
||||
# [profile.dev.package."*"]
|
||||
# opt-level = 1
|
||||
|
||||
2
iris
2
iris
Submodule iris updated: 7f4846a2d3...1102dc7338
@@ -1,22 +0,0 @@
|
||||
use ed25519_dalek::SigningKey;
|
||||
use rand::{
|
||||
SeedableRng,
|
||||
rngs::{OsRng, StdRng},
|
||||
};
|
||||
|
||||
pub struct Account {
|
||||
device_key: SigningKey,
|
||||
account_key: SigningKey,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new() -> Account {
|
||||
let mut csprng = StdRng::try_from_rng(&mut OsRng).unwrap();
|
||||
let device_key = SigningKey::generate(&mut csprng);
|
||||
let account_key = SigningKey::generate(&mut csprng);
|
||||
Account {
|
||||
device_key,
|
||||
account_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/bin/client/assets/orage.jpg
Normal file
BIN
src/bin/client/assets/orage.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
42
src/bin/client/data/mod.rs
Normal file
42
src/bin/client/data/mod.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use openworm::rsc::{DataDir, DataGuard};
|
||||
|
||||
mod ver;
|
||||
|
||||
pub struct ClientData {
|
||||
pub data: DataDir,
|
||||
}
|
||||
|
||||
pub type AccountList = ver::AccountListV0;
|
||||
pub type AccountInfo = ver::AccountInfoV0;
|
||||
pub type ServerList = ver::ServerListV0;
|
||||
pub type ServerInfo = ver::ServerInfoV0;
|
||||
|
||||
impl ClientData {
|
||||
pub fn load() -> Self {
|
||||
let data = DataDir::new(None);
|
||||
Self { data }
|
||||
}
|
||||
|
||||
pub fn create_account(&self, server: ServerInfo, info: AccountInfo, password: &str) {
|
||||
keyring::use_native_store(true).unwrap();
|
||||
let user_path = info.path();
|
||||
let entry =
|
||||
keyring_core::Entry::new("openworm", &user_path).expect("failed to open keyring entry");
|
||||
entry.set_password(password).unwrap();
|
||||
self.data
|
||||
.load::<ServerList>()
|
||||
.insert(info.url.clone(), server);
|
||||
self.data.load::<AccountList>().push(info);
|
||||
}
|
||||
|
||||
pub fn password(&self, info: &AccountInfo) -> String {
|
||||
let user_path = info.path();
|
||||
let entry =
|
||||
keyring_core::Entry::new("openworm", &user_path).expect("failed to open keyring entry");
|
||||
entry.get_password().unwrap()
|
||||
}
|
||||
|
||||
pub fn accounts(&self) -> DataGuard<AccountList> {
|
||||
self.data.load::<AccountList>()
|
||||
}
|
||||
}
|
||||
58
src/bin/client/data/ver.rs
Normal file
58
src/bin/client/data/ver.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use iris::core::util::HashMap;
|
||||
use openworm::rsc::DataRsc;
|
||||
|
||||
#[derive(Default, serde::Serialize, serde::Deserialize, Deref, DerefMut)]
|
||||
pub struct AccountListV0(Vec<AccountInfoV0>);
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct AccountInfoV0 {
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
impl AccountInfoV0 {
|
||||
pub fn path(&self) -> String {
|
||||
self.username.clone() + "@" + &self.url
|
||||
}
|
||||
}
|
||||
|
||||
impl DataRsc for AccountListV0 {
|
||||
fn name() -> &'static str {
|
||||
"account_list"
|
||||
}
|
||||
|
||||
fn parse_version(text: &str, version: u32) -> Result<Self, String> {
|
||||
match version {
|
||||
0 => ron::from_str(text).map_err(|e| e.to_string()),
|
||||
_ => Err(format!("unknown version {version}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn version() -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Serialize, serde::Deserialize, Deref, DerefMut)]
|
||||
pub struct ServerListV0(HashMap<String, ServerInfoV0>);
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ServerInfoV0 {
|
||||
pub cert_hex: String,
|
||||
}
|
||||
|
||||
impl DataRsc for ServerListV0 {
|
||||
fn name() -> &'static str {
|
||||
"server_list"
|
||||
}
|
||||
|
||||
fn parse_version(text: &str, version: u32) -> Result<Self, String> {
|
||||
match version {
|
||||
0 => ron::from_str(text).map_err(|e| e.to_string()),
|
||||
_ => Err(format!("unknown version {version}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn version() -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,8 @@
|
||||
use crate::Client;
|
||||
use iris::prelude::*;
|
||||
use crate::{Client, Rsc};
|
||||
use iris::core::UiRenderState;
|
||||
|
||||
pub fn debug(_client: &mut Client, ui: &mut Ui, _state: &UiState) {
|
||||
ui.debug_layers();
|
||||
// let mut file = std::fs::OpenOptions::new()
|
||||
// .write(true)
|
||||
// .create(true)
|
||||
// .truncate(true)
|
||||
// .open("./old_msgs")
|
||||
// .unwrap();
|
||||
// bincode::encode_into_std_write(
|
||||
// self.msgs.clone(),
|
||||
// &mut file,
|
||||
// openworm::net::BINCODE_CONFIG,
|
||||
// )
|
||||
// .unwrap();
|
||||
// let mut file = std::fs::OpenOptions::new()
|
||||
// .read(true)
|
||||
// .open("./old_msgs")
|
||||
// .unwrap();
|
||||
// let msgs: Vec<NetMsg> =
|
||||
// bincode::decode_from_std_read(&mut file, openworm::net::BINCODE_CONFIG).unwrap();
|
||||
// for msg in msgs {
|
||||
// println!("{msg:?}");
|
||||
// }
|
||||
// client.ui.debug_layers();
|
||||
impl Client {
|
||||
pub fn debug(&mut self, _rsc: &mut Rsc, render: &mut UiRenderState) {
|
||||
render.debug_layers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
#![feature(async_fn_traits)]
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
use crate::{
|
||||
net::{NetHandle, NetSender},
|
||||
rsc::{CLIENT_DATA, ClientData},
|
||||
state::{ClientState, LoggedIn, Login},
|
||||
ui::*,
|
||||
};
|
||||
use crate::data::ClientData;
|
||||
use iris::prelude::*;
|
||||
use openworm::{
|
||||
net::{ClientMsg, ServerMsg, install_crypto_provider},
|
||||
rsc::DataDir,
|
||||
};
|
||||
use openworm::net::{ServerMsg, install_crypto_provider};
|
||||
use winit::{
|
||||
event::{ElementState, MouseButton, WindowEvent},
|
||||
window::WindowAttributes,
|
||||
};
|
||||
|
||||
mod account;
|
||||
mod data;
|
||||
mod debug;
|
||||
mod net;
|
||||
mod rsc;
|
||||
mod state;
|
||||
mod session;
|
||||
mod ui;
|
||||
|
||||
fn main() {
|
||||
@@ -28,17 +20,19 @@ fn main() {
|
||||
DefaultApp::<Client>::run();
|
||||
}
|
||||
|
||||
#[derive(DefaultUiState)]
|
||||
pub struct Client {
|
||||
dir: DataDir,
|
||||
ui_state: DefaultUiState,
|
||||
data: ClientData,
|
||||
state: ClientState,
|
||||
main_ui: WidgetRef<WidgetPtr>,
|
||||
notif: WidgetRef<WidgetPtr>,
|
||||
proxy: Proxy<ClientEvent>,
|
||||
main_ui: WeakWidget<WidgetPtr>,
|
||||
notif: WeakWidget<WidgetPtr>,
|
||||
}
|
||||
|
||||
pub type Rsc = DefaultRsc<Client>;
|
||||
pub type ClientSender = EventSender<Rsc>;
|
||||
|
||||
pub enum ClientEvent {
|
||||
Connect { send: NetSender },
|
||||
CacheUpdate,
|
||||
ServerMsg(ServerMsg),
|
||||
Err(String),
|
||||
}
|
||||
@@ -46,14 +40,10 @@ pub enum ClientEvent {
|
||||
impl DefaultAppState for Client {
|
||||
type Event = ClientEvent;
|
||||
|
||||
fn window_attrs() -> WindowAttributes {
|
||||
WindowAttributes::default().with_title("OPENWORM")
|
||||
}
|
||||
|
||||
fn new(ui: &mut Ui, _state: &UiState, proxy: Proxy<Self::Event>) -> Self {
|
||||
let dir = DataDir::default();
|
||||
let notif = WidgetPtr::default().add(ui);
|
||||
let main_ui = WidgetPtr::default().add(ui);
|
||||
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
|
||||
let notif = WidgetPtr::default().add(rsc);
|
||||
// let popup = WidgetPtr::default().add(rsc);
|
||||
let main_ui = WidgetPtr::default().add(rsc);
|
||||
let bg = (
|
||||
image(include_bytes!("./assets/fuit.jpg")),
|
||||
rect(Color::BLACK.alpha((0.8 * 255.0) as u8)),
|
||||
@@ -62,96 +52,55 @@ impl DefaultAppState for Client {
|
||||
|
||||
(
|
||||
bg,
|
||||
main_ui.clone(),
|
||||
notif.clone().pad(Padding::top(10)).align(Align::TOP_CENTER),
|
||||
main_ui,
|
||||
notif.pad(Padding::top(10)).align(Align::TOP_CENTER),
|
||||
)
|
||||
.stack()
|
||||
.set_root(ui);
|
||||
.set_root(rsc, &mut ui_state);
|
||||
|
||||
let mut s = Self {
|
||||
data: dir.load(CLIENT_DATA),
|
||||
state: Default::default(),
|
||||
dir,
|
||||
main_ui: main_ui.clone(),
|
||||
let data = ClientData::load();
|
||||
ui::start(rsc, &data).set_ptr(main_ui, rsc);
|
||||
|
||||
Self {
|
||||
ui_state,
|
||||
data,
|
||||
main_ui,
|
||||
notif,
|
||||
proxy,
|
||||
};
|
||||
start_ui(&mut s, ui).set_ptr(&s.main_ui, ui);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, event: ClientEvent, ui: &mut Ui, _state: &UiState) {
|
||||
fn event(
|
||||
&mut self,
|
||||
event: Self::Event,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
_render: &mut UiRenderState,
|
||||
) {
|
||||
match event {
|
||||
ClientEvent::Connect { send } => {
|
||||
let ClientState::Connect(connect) = self.state.take() else {
|
||||
panic!("invalid state");
|
||||
};
|
||||
let th = connect.handle.unwrap();
|
||||
self.state = ClientState::Login(Login {
|
||||
handle: NetHandle {
|
||||
send: send.clone(),
|
||||
thread: th,
|
||||
},
|
||||
});
|
||||
// login_screen(self, ui).set_ptr(&self.main_ui, ui);
|
||||
}
|
||||
ClientEvent::ServerMsg(msg) => match msg {
|
||||
ServerMsg::SendMsg(msg) => {
|
||||
if let ClientState::LoggedIn(state) = &mut self.state
|
||||
&& let Some(msg_area) = &state.channel
|
||||
{
|
||||
let msg = msg_widget(&msg.user, &msg.content).add(ui);
|
||||
msg_area.get_mut().children.push(msg);
|
||||
}
|
||||
}
|
||||
ServerMsg::LoadMsgs(msgs) => {
|
||||
if let ClientState::LoggedIn(state) = &mut self.state
|
||||
&& let Some(msg_area) = &state.channel
|
||||
{
|
||||
for msg in msgs {
|
||||
state.msgs.push(msg.clone());
|
||||
let msg = msg_widget(&msg.user, &msg.content).add(ui);
|
||||
msg_area.get_mut().children.push(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
ServerMsg::Login { username } => {
|
||||
let ClientState::Login(state) = self.state.take() else {
|
||||
panic!("invalid state");
|
||||
};
|
||||
state.handle.send(ClientMsg::RequestMsgs);
|
||||
self.state = ClientState::LoggedIn(LoggedIn {
|
||||
network: state.handle,
|
||||
channel: None,
|
||||
msgs: Vec::new(),
|
||||
username,
|
||||
});
|
||||
main_view(self, ui).set_ptr(&self.main_ui, ui);
|
||||
}
|
||||
ServerMsg::Error(error) => {
|
||||
let msg = format!("{error:?}");
|
||||
self.notif.get_mut().inner = Some(werror(ui, &msg));
|
||||
}
|
||||
},
|
||||
ClientEvent::ServerMsg(_) => {}
|
||||
ClientEvent::Err(msg) => {
|
||||
self.notif.get_mut().inner = Some(werror(ui, &msg));
|
||||
rsc[self.notif].inner = Some(ui::werror(&msg, rsc));
|
||||
}
|
||||
ClientEvent::CacheUpdate => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn exit(&mut self, _ui: &mut Ui, _state: &UiState) {
|
||||
self.state.exit();
|
||||
self.dir.save(CLIENT_DATA, &self.data);
|
||||
}
|
||||
|
||||
fn window_event(&mut self, event: WindowEvent, ui: &mut Ui, state: &UiState) {
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event: WindowEvent,
|
||||
rsc: &mut DefaultRsc<Self>,
|
||||
render: &mut UiRenderState,
|
||||
) {
|
||||
if let WindowEvent::MouseInput {
|
||||
state: ElementState::Pressed,
|
||||
button: MouseButton::Middle,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
debug::debug(self, ui, state);
|
||||
self.debug(rsc, render);
|
||||
}
|
||||
}
|
||||
|
||||
fn window_attributes() -> WindowAttributes {
|
||||
WindowAttributes::default().with_title("OPENWORM")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,113 +1,124 @@
|
||||
use crate::ClientEvent;
|
||||
use crate::ClientSender;
|
||||
use dashmap::DashMap;
|
||||
use openworm::net::{
|
||||
ClientMsg, RecvHandler, SERVER_NAME, ServerMsg, SkipServerVerification, recv_uni, send_uni,
|
||||
ClientMsg, ClientMsgInst, RecvHandler, RequestId, RequestMsg, SERVER_NAME, ServerMsg,
|
||||
ServerMsgInst, SkipServerVerification, recv_uni, send_uni,
|
||||
};
|
||||
use quinn::{
|
||||
ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig,
|
||||
crypto::rustls::QuicClientConfig,
|
||||
crypto::rustls::QuicClientConfig, rustls::pki_types::CertificateDer,
|
||||
};
|
||||
use std::{
|
||||
net::{Ipv6Addr, SocketAddr, SocketAddrV6, ToSocketAddrs},
|
||||
sync::Arc,
|
||||
thread::JoinHandle,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use winit::{event_loop::EventLoopProxy, window::Window};
|
||||
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
||||
|
||||
pub const CLIENT_SOCKET: SocketAddr =
|
||||
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0));
|
||||
|
||||
pub struct ConnectInfo {
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
pub struct NetHandle {
|
||||
pub send: NetSender,
|
||||
pub thread: JoinHandle<()>,
|
||||
pub url: String,
|
||||
pub cert: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppHandle {
|
||||
pub proxy: EventLoopProxy<ClientEvent>,
|
||||
pub window: Arc<Window>,
|
||||
}
|
||||
|
||||
impl AppHandle {
|
||||
pub fn send(&self, event: ClientEvent) {
|
||||
self.proxy.send_event(event).unwrap_or_else(|_| panic!());
|
||||
self.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(handle: AppHandle, info: ConnectInfo) -> JoinHandle<()> {
|
||||
std::thread::spawn(move || {
|
||||
if let Err(msg) = connect_the(handle.clone(), info) {
|
||||
handle.send(ClientEvent::Err(msg));
|
||||
}
|
||||
})
|
||||
pub struct NetHandle {
|
||||
send: UnboundedSender<NetCtrlMsg>,
|
||||
event_sender: ClientSender,
|
||||
}
|
||||
|
||||
type NetResult<T> = Result<T, String>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NetSender {
|
||||
send: UnboundedSender<NetCtrlMsg>,
|
||||
}
|
||||
|
||||
pub enum NetCtrlMsg {
|
||||
Send(ClientMsg),
|
||||
Request(ClientMsg, oneshot::Sender<ServerMsg>),
|
||||
RequestSync(ClientMsg, Box<dyn FnOnce(ServerMsg) + Send + Sync>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
impl From<ClientMsg> for NetCtrlMsg {
|
||||
fn from(value: ClientMsg) -> Self {
|
||||
Self::Send(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl NetSender {
|
||||
pub fn send(&self, msg: impl Into<NetCtrlMsg>) {
|
||||
let _ = self.send.send(msg.into());
|
||||
}
|
||||
}
|
||||
type Resp<R: RequestMsg> = Result<R::Result, ()>;
|
||||
|
||||
impl NetHandle {
|
||||
pub fn send(&self, msg: impl Into<NetCtrlMsg>) {
|
||||
self.send.send(msg.into());
|
||||
fn send_(&self, msg: NetCtrlMsg) {
|
||||
let _ = self.send.send(msg);
|
||||
}
|
||||
|
||||
pub fn send(&self, msg: impl Into<ClientMsg>) {
|
||||
self.send_(NetCtrlMsg::Send(msg.into()));
|
||||
}
|
||||
|
||||
pub async fn request<R: RequestMsg>(&self, msg: R) -> Resp<R> {
|
||||
let (send, recv) = oneshot::channel();
|
||||
self.send_(NetCtrlMsg::Request(msg.into(), send));
|
||||
let Ok(recv) = recv.await else { return Err(()) };
|
||||
if let Some(res) = R::result(recv) {
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_sync<R: RequestMsg>(&self, msg: R) -> SyncRecv<R> {
|
||||
let (send, recv) = oneshot::channel();
|
||||
let sender = self.event_sender.clone();
|
||||
self.send_(NetCtrlMsg::RequestSync(
|
||||
msg.into(),
|
||||
Box::new(move |msg| {
|
||||
let _ = send.send(if let Some(res) = R::result(msg) {
|
||||
Ok(res)
|
||||
} else {
|
||||
Err(())
|
||||
});
|
||||
sender.run();
|
||||
}),
|
||||
));
|
||||
SyncRecv::<R> { recv }
|
||||
}
|
||||
|
||||
pub fn exit(self) {
|
||||
self.send(NetCtrlMsg::Exit);
|
||||
let _ = self.thread.join();
|
||||
self.send_(NetCtrlMsg::Exit);
|
||||
}
|
||||
}
|
||||
|
||||
// async fn connection_cert(addr: SocketAddr) -> NetResult<Connection> {
|
||||
// let dirs = directories_next::ProjectDirs::from("", "", "openworm").unwrap();
|
||||
// let mut roots = quinn::rustls::RootCertStore::empty();
|
||||
// match fs::read(dirs.data_local_dir().join("cert.der")) {
|
||||
// Ok(cert) => {
|
||||
// roots.add(CertificateDer::from(cert))?;
|
||||
// }
|
||||
// Err(ref e) if e.kind() == ErrorKind::NotFound => {
|
||||
// eprintln!("local server certificate not found");
|
||||
// }
|
||||
// Err(e) => {
|
||||
// eprintln!("failed to open local server certificate: {}", e);
|
||||
// }
|
||||
// }
|
||||
// let client_crypto = quinn::rustls::ClientConfig::builder()
|
||||
// .with_root_certificates(roots)
|
||||
// .with_no_client_auth();
|
||||
// let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?));
|
||||
// let mut endpoint = quinn::Endpoint::client(SocketAddr::from_str("[::]:0").unwrap())?;
|
||||
// endpoint.set_default_client_config(client_config);
|
||||
// endpoint
|
||||
// .connect(addr, SERVER_NAME)?
|
||||
// .await
|
||||
// .map_err(|e| format!("failed to connect: {}", e))
|
||||
// }
|
||||
pub struct SyncRecv<R: RequestMsg> {
|
||||
recv: oneshot::Receiver<Resp<R>>,
|
||||
}
|
||||
|
||||
impl<R: RequestMsg> SyncRecv<R> {
|
||||
pub fn try_recv(&mut self) -> Option<Resp<R>> {
|
||||
match self.recv.try_recv() {
|
||||
Ok(res) => Some(res),
|
||||
Err(oneshot::error::TryRecvError::Empty) => None,
|
||||
Err(oneshot::error::TryRecvError::Closed) => Some(Err(())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn connection_cert(
|
||||
addr: SocketAddr,
|
||||
cert: CertificateDer<'_>,
|
||||
) -> NetResult<(Endpoint, Connection)> {
|
||||
let mut roots = quinn::rustls::RootCertStore::empty();
|
||||
roots
|
||||
.add(cert)
|
||||
.map_err(|e| format!("Invalid Certificate: {e:?}"))?;
|
||||
let client_crypto = quinn::rustls::ClientConfig::builder()
|
||||
.with_root_certificates(roots)
|
||||
.with_no_client_auth();
|
||||
let client_config = ClientConfig::new(Arc::new(
|
||||
QuicClientConfig::try_from(client_crypto).map_err(|e| e.to_string())?,
|
||||
));
|
||||
let mut endpoint = quinn::Endpoint::client(CLIENT_SOCKET).map_err(|e| e.to_string())?;
|
||||
endpoint.set_default_client_config(client_config);
|
||||
let conn = endpoint
|
||||
.connect(addr, SERVER_NAME)
|
||||
.map_err(|e| e.to_string())?
|
||||
.await
|
||||
.map_err(|e| format!("failed to connect: {}", e))?;
|
||||
Ok((endpoint, conn))
|
||||
}
|
||||
|
||||
async fn connection_no_cert(addr: SocketAddr) -> NetResult<(Endpoint, Connection)> {
|
||||
let mut endpoint = Endpoint::client(CLIENT_SOCKET).map_err(|e| e.to_string())?;
|
||||
@@ -140,29 +151,62 @@ async fn connection_no_cert(addr: SocketAddr) -> NetResult<(Endpoint, Connection
|
||||
Ok((endpoint, con))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
|
||||
impl NetHandle {
|
||||
pub async fn connect(
|
||||
msg: impl MsgHandler,
|
||||
info: ConnectInfo,
|
||||
event_sender: ClientSender,
|
||||
) -> Result<Self, String> {
|
||||
let (send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel::<NetCtrlMsg>();
|
||||
|
||||
let cert = CertificateDer::from_slice(&info.cert);
|
||||
let addr = info
|
||||
.ip
|
||||
.url
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| e.to_string())?
|
||||
.next()
|
||||
.ok_or("no addresses found".to_string())?;
|
||||
let (endpoint, conn) = connection_no_cert(addr).await?;
|
||||
let (endpoint, conn) = connection_cert(addr, cert).await?;
|
||||
let conn_ = conn.clone();
|
||||
|
||||
handle.send(ClientEvent::Connect {
|
||||
send: NetSender { send },
|
||||
let mut req_id = RequestId::first();
|
||||
let recv = Arc::new(ServerRecv {
|
||||
msg,
|
||||
requests_sync: DashMap::default(),
|
||||
requests: DashMap::default(),
|
||||
});
|
||||
|
||||
let recv = ServerRecv { handle };
|
||||
tokio::spawn(recv_uni(conn_, recv.into()));
|
||||
|
||||
tokio::spawn(recv_uni(conn_, recv.clone()));
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg) = ui_recv.recv().await {
|
||||
let request_id = req_id.next();
|
||||
match msg {
|
||||
NetCtrlMsg::Send(msg) => {
|
||||
let msg = ClientMsgInst {
|
||||
id: request_id,
|
||||
msg,
|
||||
};
|
||||
if send_uni(&conn, msg).await.is_err() {
|
||||
println!("disconnected from server");
|
||||
break;
|
||||
}
|
||||
}
|
||||
NetCtrlMsg::Request(msg, send) => {
|
||||
let msg = ClientMsgInst {
|
||||
id: request_id,
|
||||
msg,
|
||||
};
|
||||
recv.requests.insert(request_id, send);
|
||||
if send_uni(&conn, msg).await.is_err() {
|
||||
println!("disconnected from server");
|
||||
break;
|
||||
}
|
||||
}
|
||||
NetCtrlMsg::RequestSync(msg, f) => {
|
||||
let msg = ClientMsgInst {
|
||||
id: request_id,
|
||||
msg,
|
||||
};
|
||||
recv.requests_sync.insert(request_id, f);
|
||||
if send_uni(&conn, msg).await.is_err() {
|
||||
println!("disconnected from server");
|
||||
break;
|
||||
@@ -175,16 +219,40 @@ async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct ServerRecv {
|
||||
handle: AppHandle,
|
||||
}
|
||||
|
||||
impl RecvHandler<ServerMsg> for ServerRecv {
|
||||
async fn msg(&self, msg: ServerMsg) {
|
||||
self.handle.send(ClientEvent::ServerMsg(msg));
|
||||
Ok(NetHandle { send, event_sender })
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MsgHandler: Sync + Send + 'static {
|
||||
fn run(&self, msg: ServerMsg) -> impl Future<Output = ()> + Send;
|
||||
}
|
||||
impl<F: AsyncFn(ServerMsg) + Sync + Send + 'static> MsgHandler for F
|
||||
where
|
||||
for<'a> F::CallRefFuture<'a>: Send,
|
||||
{
|
||||
async fn run(&self, msg: ServerMsg) {
|
||||
self(msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerRecv<F: MsgHandler> {
|
||||
requests: DashMap<RequestId, oneshot::Sender<ServerMsg>>,
|
||||
requests_sync: DashMap<RequestId, Box<dyn FnOnce(ServerMsg) + Send + Sync>>,
|
||||
msg: F,
|
||||
}
|
||||
|
||||
impl<F: MsgHandler> RecvHandler<ServerMsgInst> for ServerRecv<F> {
|
||||
async fn msg(&self, resp: ServerMsgInst) {
|
||||
if let Some(id) = resp.id {
|
||||
if let Some((_, send)) = self.requests.remove(&id) {
|
||||
let _ = send.send(resp.msg);
|
||||
} else if let Some((_, f)) = self.requests_sync.remove(&id) {
|
||||
f(resp.msg)
|
||||
}
|
||||
} else {
|
||||
self.msg.run(resp.msg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
pub const CLIENT_DATA: &str = "client_data";
|
||||
|
||||
#[derive(Debug, Default, bincode::Encode, bincode::Decode)]
|
||||
pub struct ClientData {
|
||||
pub ip: String,
|
||||
pub username: String,
|
||||
/// TODO: not store this as plain string?
|
||||
/// need to figure out crypto stuff
|
||||
/// or store session token
|
||||
pub password: String,
|
||||
}
|
||||
19
src/bin/client/session.rs
Normal file
19
src/bin/client/session.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use openworm::net::UserId;
|
||||
|
||||
use crate::{net::NetHandle, ui::UserCache};
|
||||
|
||||
pub struct Session {
|
||||
pub con: NetHandle,
|
||||
pub user_id: UserId,
|
||||
pub cache: UserCache,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new(con: NetHandle, user_id: UserId) -> Self {
|
||||
Self {
|
||||
cache: UserCache::new(con.clone()),
|
||||
con,
|
||||
user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use iris::prelude::*;
|
||||
use openworm::net::NetServerMsg;
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use crate::net::NetHandle;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Connect {
|
||||
pub handle: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
pub struct Login {
|
||||
pub handle: NetHandle,
|
||||
}
|
||||
|
||||
pub struct LoggedIn {
|
||||
pub network: NetHandle,
|
||||
pub msgs: Vec<NetServerMsg>,
|
||||
pub channel: Option<WidgetRef<Span>>,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
pub enum ClientState {
|
||||
Connect(Connect),
|
||||
Login(Login),
|
||||
LoggedIn(LoggedIn),
|
||||
}
|
||||
|
||||
impl Default for ClientState {
|
||||
fn default() -> Self {
|
||||
Self::Connect(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
pub fn take(&mut self) -> Self {
|
||||
std::mem::take(self)
|
||||
}
|
||||
pub fn exit(&mut self) {
|
||||
let s = self.take();
|
||||
match s {
|
||||
ClientState::Connect(_) => (),
|
||||
ClientState::Login(Login { handle }) => {
|
||||
handle.exit();
|
||||
}
|
||||
ClientState::LoggedIn(state) => {
|
||||
state.network.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/bin/client/ui/channel.rs
Normal file
67
src/bin/client/ui/channel.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use super::*;
|
||||
|
||||
pub fn view(rsc: &mut Rsc) -> StrongWidget {
|
||||
let msg_panel = msg_panel(rsc);
|
||||
let side_bar = rect(Color::BLACK.alpha(150)).width(260);
|
||||
|
||||
(side_bar, msg_panel).span(Dir::RIGHT).add_strong(rsc)
|
||||
}
|
||||
|
||||
pub fn msg_widget(username: &str, content: &str) -> impl WidgetIdFn<Rsc> {
|
||||
let content = wtext(content)
|
||||
.editable(EditMode::MultiLine)
|
||||
.size(SIZE)
|
||||
.wrap(true)
|
||||
.attr::<Selectable>(());
|
||||
let header = wtext(username).size(SIZE);
|
||||
(
|
||||
image(include_bytes!("../assets/sungals.png"))
|
||||
.sized((70, 70))
|
||||
.align(Align::TOP),
|
||||
(header, content)
|
||||
.span(Dir::DOWN)
|
||||
.gap(10)
|
||||
.width(rest(1))
|
||||
.align(Align::TOP),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.gap(10)
|
||||
.to_any()
|
||||
}
|
||||
|
||||
pub fn msg_panel(rsc: &mut Rsc) -> WeakWidget {
|
||||
let msg_area = Span::empty(Dir::DOWN).gap(15);
|
||||
|
||||
let send_text = wtext("")
|
||||
.editable(EditMode::MultiLine)
|
||||
.size(SIZE)
|
||||
.wrap(true)
|
||||
.hint(hint_text("send message"))
|
||||
.add(rsc);
|
||||
|
||||
let msg_area = msg_area.add(rsc);
|
||||
|
||||
(
|
||||
msg_area
|
||||
.scrollable()
|
||||
.pad(Padding::x(15).with_top(15))
|
||||
.height(rest(1)),
|
||||
send_text
|
||||
.on(Submit, move |ctx, rsc| {
|
||||
let content = ctx.widget.edit(rsc).take();
|
||||
let msg = msg_widget("ur mothe:", &content).add_strong(rsc);
|
||||
rsc[msg_area].children.push(msg);
|
||||
})
|
||||
.pad(15)
|
||||
.attr::<Selector>(send_text)
|
||||
.scrollable()
|
||||
.masked()
|
||||
.background(rect(Color::BLACK.brighter(0.05)).radius(15))
|
||||
.pad(15)
|
||||
.max_height(rel(0.5))
|
||||
.layer_offset(1),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.width(rest(1))
|
||||
.add(rsc)
|
||||
}
|
||||
@@ -2,3 +2,4 @@ use super::*;
|
||||
|
||||
pub const MODAL_BG: UiColor = UiColor::BLACK.brighter(0.05);
|
||||
pub const GREEN: UiColor = UiColor::rgb(0, 150, 0);
|
||||
pub const DARK: UiColor = UiColor::BLACK.brighter(0.02);
|
||||
|
||||
@@ -1,32 +1,115 @@
|
||||
use openworm::net::{CreateAccount, CreateAccountResp, Login, LoginResp};
|
||||
|
||||
use crate::{
|
||||
data::{AccountInfo, ClientData, ServerInfo, ServerList},
|
||||
net::{ConnectInfo, NetHandle},
|
||||
session::Session,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn start_ui(client: &mut Client, ui: &mut Ui) -> WidgetRef {
|
||||
pub fn start(rsc: &mut Rsc, data: &ClientData) -> WeakWidget {
|
||||
let mut accounts = Span::empty(Dir::DOWN);
|
||||
|
||||
let accts = data.accounts();
|
||||
if accts.is_empty() {
|
||||
accounts.push(
|
||||
wtext("no accounts")
|
||||
.size(20)
|
||||
.center_text()
|
||||
.color(Color::GRAY)
|
||||
.height(60)
|
||||
.add(ui),
|
||||
.add_strong(rsc),
|
||||
);
|
||||
} else {
|
||||
for account in accts.iter() {
|
||||
let button = Button::new_fg(
|
||||
wtext(&account.username)
|
||||
.size(20)
|
||||
.center_text()
|
||||
.height(60)
|
||||
.add(rsc),
|
||||
color::DARK,
|
||||
rsc,
|
||||
)
|
||||
.add(rsc);
|
||||
let account = account.clone();
|
||||
let cert_hex = data
|
||||
.data
|
||||
.load::<ServerList>()
|
||||
.get(&account.url)
|
||||
.unwrap()
|
||||
.cert_hex
|
||||
.clone();
|
||||
let cert = decode_hex(&cert_hex).unwrap();
|
||||
keyring::use_native_store(true).unwrap();
|
||||
rsc.events.register(button, Submit, move |ctx, rsc| {
|
||||
let account = account.clone();
|
||||
let cert = cert.clone();
|
||||
let password = ctx.state.data.password(&account);
|
||||
let event_sender = rsc.window_event.clone();
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
let mut fail = |reason: &str| {
|
||||
let reason = reason.to_string();
|
||||
ctx.update(move |ctx, rsc| {
|
||||
ctx.error(&reason, rsc);
|
||||
})
|
||||
};
|
||||
let con = match NetHandle::connect(
|
||||
async |msg| {
|
||||
println!("msg recv :joy:");
|
||||
},
|
||||
ConnectInfo {
|
||||
url: account.url.clone(),
|
||||
cert,
|
||||
},
|
||||
event_sender,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return fail(&e);
|
||||
}
|
||||
};
|
||||
|
||||
let connect = Button::submit("connect", ui);
|
||||
let create = Button::submit("create", ui);
|
||||
let connect_ = connect.clone();
|
||||
let create_ = create.clone();
|
||||
ui.on(connect.view(), Submit, move |_| {
|
||||
connect_.disable();
|
||||
create_.disable();
|
||||
let Ok(resp) = con
|
||||
.request(Login {
|
||||
username: account.username.clone(),
|
||||
password: password.clone(),
|
||||
})
|
||||
.await
|
||||
else {
|
||||
return fail("failed to create account");
|
||||
};
|
||||
let user_id = match resp {
|
||||
LoginResp::Ok { id } => id,
|
||||
LoginResp::UnknownUsername => {
|
||||
return fail("unknown username");
|
||||
}
|
||||
LoginResp::InvalidPassword => {
|
||||
return fail("invalid password");
|
||||
}
|
||||
};
|
||||
let session = Session::new(con, user_id);
|
||||
ctx.update(move |ctx, rsc| {
|
||||
main_view(rsc, session).set_ptr(ctx.main_ui, rsc);
|
||||
});
|
||||
});
|
||||
});
|
||||
accounts.push(button.add_strong(rsc));
|
||||
}
|
||||
}
|
||||
|
||||
let connect = Button::submit("connect", rsc);
|
||||
let create = Button::submit("create", rsc);
|
||||
rsc.events.register(connect, Submit, move |_, rsc| {
|
||||
connect.disable(rsc);
|
||||
create.disable(rsc);
|
||||
});
|
||||
|
||||
let connect_ = connect.clone();
|
||||
let create_ = create.clone();
|
||||
ui.on(create.view(), Submit, move |ctx| {
|
||||
connect_.disable();
|
||||
create_.disable();
|
||||
create_ui(ctx.ui).set_ptr(&ctx.state.main_ui, ctx.ui);
|
||||
rsc.events.register(create, Submit, move |ctx, rsc| {
|
||||
create_account(rsc).set_ptr(ctx.state.main_ui, rsc);
|
||||
});
|
||||
|
||||
(
|
||||
@@ -37,115 +120,109 @@ pub fn start_ui(client: &mut Client, ui: &mut Ui) -> WidgetRef {
|
||||
.span(Dir::DOWN)
|
||||
.gap(30)
|
||||
.modal(400)
|
||||
.add(ui)
|
||||
.add(rsc)
|
||||
}
|
||||
|
||||
pub fn create_ui(ui: &mut Ui) -> WidgetRef {
|
||||
wtext("hi").add(ui)
|
||||
pub fn create_account(rsc: &mut Rsc) -> WeakWidget {
|
||||
let url = field("", "server").add(rsc);
|
||||
let token = field("", "account creation token").add(rsc);
|
||||
let cert = field("", "certificate hex").add(rsc);
|
||||
let username = field("", "username").add(rsc);
|
||||
let password = field("", "password").add(rsc);
|
||||
|
||||
let create = Button::submit("create", rsc);
|
||||
rsc.events.register(create, Submit, move |ctx, rsc| {
|
||||
let url = rsc[url].content().trim().to_string();
|
||||
let token = rsc[token].content().trim().to_string();
|
||||
let cert_hex = rsc[cert].content().trim().to_string();
|
||||
let Some(cert) = decode_hex(&cert_hex) else {
|
||||
rsc[ctx.state.notif].inner = Some(werror("Invalid certificate hex", rsc));
|
||||
return;
|
||||
};
|
||||
let username = rsc[username].content();
|
||||
let password = rsc[password].content();
|
||||
|
||||
create.disable(rsc);
|
||||
let event_sender = rsc.window_event.clone();
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
let mut fail = |reason: &str| {
|
||||
let reason = reason.to_string();
|
||||
ctx.update(move |ctx, rsc| {
|
||||
ctx.error(&reason, rsc);
|
||||
create.enable(rsc);
|
||||
})
|
||||
};
|
||||
keyring::use_native_store(true).unwrap();
|
||||
let con = match NetHandle::connect(
|
||||
async |msg| {
|
||||
println!("msg recv :joy:");
|
||||
},
|
||||
ConnectInfo {
|
||||
url: url.clone(),
|
||||
cert,
|
||||
},
|
||||
event_sender,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return fail(&e);
|
||||
}
|
||||
};
|
||||
|
||||
let Ok(resp) = con
|
||||
.request(CreateAccount {
|
||||
username: username.clone(),
|
||||
password: password.clone(),
|
||||
token,
|
||||
})
|
||||
.await
|
||||
else {
|
||||
return fail("failed to create account");
|
||||
};
|
||||
let user_id = match resp {
|
||||
CreateAccountResp::Ok { id } => id,
|
||||
CreateAccountResp::UsernameExists => {
|
||||
return fail("username already exists");
|
||||
}
|
||||
CreateAccountResp::InvalidToken => {
|
||||
return fail("invalid account token");
|
||||
}
|
||||
};
|
||||
let session = Session::new(con, user_id);
|
||||
ctx.update(move |ctx, rsc| {
|
||||
main_view(rsc, session).set_ptr(ctx.main_ui, rsc);
|
||||
ctx.data.create_account(
|
||||
ServerInfo { cert_hex },
|
||||
AccountInfo { url, username },
|
||||
&password,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
(
|
||||
wtext("Create Account").text_align(Align::CENTER).size(30),
|
||||
field_box(url),
|
||||
field_box(token),
|
||||
field_box(cert),
|
||||
field_box(username),
|
||||
field_box(password),
|
||||
create,
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.gap(15)
|
||||
.modal(400)
|
||||
.add(rsc)
|
||||
}
|
||||
|
||||
// pub fn connect_screen(client: &mut Client, ui: &mut Ui, state: &UiState) -> WidgetRef {
|
||||
// let Client { data, proxy, .. } = client;
|
||||
// let ip = field_widget(&data.ip, "ip", ui);
|
||||
// let ip_ = ip.clone();
|
||||
// let handle = AppHandle {
|
||||
// proxy: proxy.clone(),
|
||||
// window: state.window.clone(),
|
||||
// };
|
||||
//
|
||||
// let submit = Button::submit("connect", ui);
|
||||
//
|
||||
// submit.on(Submit, move |ctx| {
|
||||
// let ClientState::Connect(state) = &mut ctx.state.state else {
|
||||
// return;
|
||||
// };
|
||||
// let ip = ip_.get().content();
|
||||
// state.handle = Some(connect(handle.clone(), ConnectInfo { ip }));
|
||||
// });
|
||||
//
|
||||
// (
|
||||
// wtext("connect to a server")
|
||||
// .text_align(Align::CENTER)
|
||||
// .size(30),
|
||||
// field_box(
|
||||
// // NOTE: should probably do this on submit
|
||||
// ip.on(Edited, |ctx| {
|
||||
// ctx.state.data.ip = ctx.widget.get().content();
|
||||
// })
|
||||
// .add(ui),
|
||||
// ui,
|
||||
// ),
|
||||
// submit,
|
||||
// )
|
||||
// .span(Dir::DOWN)
|
||||
// .gap(10)
|
||||
// .pad(15)
|
||||
// .background(rect(Color::BLACK.brighter(0.2)).radius(15))
|
||||
// .width(400)
|
||||
// .align(Align::CENTER)
|
||||
// .add(ui)
|
||||
// }
|
||||
|
||||
// pub fn login_screen(client: &mut Client, ui: &mut Ui) -> WidgetRef {
|
||||
// let Client { data, .. } = client;
|
||||
// let username = field_widget(&data.username, "username", ui);
|
||||
// let password = field_widget(&data.password, "password", ui);
|
||||
// let username_ = username.clone();
|
||||
// let password_ = password.clone();
|
||||
// let submit = Button::submit("login", ui);
|
||||
// submit
|
||||
// .on(move |client, _ui| {
|
||||
// let ClientState::Login(state) = &mut client.state else {
|
||||
// return;
|
||||
// };
|
||||
// let username = username_.get().content();
|
||||
// let password = password_.get().content();
|
||||
// state.handle.send(ClientMsg::Login { username, password });
|
||||
// })
|
||||
// .add(ui);
|
||||
// let username_ = username.clone();
|
||||
// let password_ = password.clone();
|
||||
// let create_button = Button::submit(
|
||||
// "create account",
|
||||
// move |client, _ui| {
|
||||
// let ClientState::Login(state) = &mut client.state else {
|
||||
// return;
|
||||
// };
|
||||
// let username = username_.get().content();
|
||||
// let password = password_.get().content();
|
||||
// state
|
||||
// .handle
|
||||
// .send(ClientMsg::CreateAccount { username, password });
|
||||
// },
|
||||
// ui,
|
||||
// );
|
||||
// create_button.on()
|
||||
// (
|
||||
// wtext("login to server").text_align(Align::CENTER).size(30),
|
||||
// field_box(
|
||||
// username
|
||||
// .on(Edited, |ctx| {
|
||||
// ctx.state.data.username = ctx.widget.get().content();
|
||||
// })
|
||||
// .add(ui),
|
||||
// ui,
|
||||
// ),
|
||||
// field_box(
|
||||
// password
|
||||
// .on(Edited, |ctx| {
|
||||
// ctx.state.data.password = ctx.widget.get().content();
|
||||
// })
|
||||
// .add(ui),
|
||||
// ui,
|
||||
// ),
|
||||
// submit,
|
||||
// create_button,
|
||||
// )
|
||||
// .span(Dir::DOWN)
|
||||
// .gap(10)
|
||||
// .pad(15)
|
||||
// .background(rect(Color::BLACK.brighter(0.2)).radius(15))
|
||||
// .width(400)
|
||||
// .align(Align::CENTER)
|
||||
// .add(ui)
|
||||
// }
|
||||
pub fn decode_hex(s: &str) -> Option<Vec<u8>> {
|
||||
if !s.len().is_multiple_of(2) {
|
||||
return None;
|
||||
}
|
||||
(0..s.len())
|
||||
.step_by(2)
|
||||
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
113
src/bin/client/ui/friends.rs
Normal file
113
src/bin/client/ui/friends.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
use openworm::net::{AddFriend, AddFriendResp, GenerateToken, RequestFriends, ServerPerms};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn view(rsc: &mut Rsc, session: &Session) -> StrongWidget {
|
||||
(
|
||||
gen_token(rsc, session),
|
||||
add_friend_area(rsc, session),
|
||||
friends_list(rsc, session),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.add_strong(rsc)
|
||||
}
|
||||
|
||||
fn add_friend_area(rsc: &mut Rsc, session: &Session) -> WeakWidget {
|
||||
let username_field = field("", "username").add(rsc);
|
||||
let add = Button::submit("add friend", rsc);
|
||||
let con = session.con.clone();
|
||||
rsc.events.register(add, Submit, move |ctx, rsc| {
|
||||
let con = con.clone();
|
||||
let username = username_field.edit(rsc).take();
|
||||
add.disable(rsc);
|
||||
rsc.tasks.spawn(async move |mut ctx| {
|
||||
let Ok(resp) = con.request(AddFriend { username }).await else {
|
||||
ctx.update(move |ctx, rsc| {
|
||||
ctx.error("failed to add user", rsc);
|
||||
});
|
||||
return;
|
||||
};
|
||||
ctx.update(move |ctx, rsc| {
|
||||
add.enable(rsc);
|
||||
match resp {
|
||||
AddFriendResp::Ok => {}
|
||||
AddFriendResp::UnknownUser => {
|
||||
ctx.error("unknown user", rsc);
|
||||
}
|
||||
AddFriendResp::CannotAddSelf => {
|
||||
ctx.error("cannot add self", rsc);
|
||||
}
|
||||
AddFriendResp::AlreadySent => {
|
||||
ctx.error("already sent request to user", rsc);
|
||||
}
|
||||
AddFriendResp::AlreadyFriends => {
|
||||
ctx.error("already friends with user", rsc);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
(field_box(username_field).width(300), add.width(200))
|
||||
.span(Dir::RIGHT)
|
||||
.add(rsc)
|
||||
}
|
||||
|
||||
fn gen_token(rsc: &mut Rsc, session: &Session) -> WeakWidget {
|
||||
let generate = Button::normal("generate token", rsc);
|
||||
let con = session.con.clone();
|
||||
let token = wtext("")
|
||||
.size(30)
|
||||
.editable(EditMode::SingleLine)
|
||||
.attr::<Selectable>(())
|
||||
.add(rsc);
|
||||
rsc.events.register(generate, Submit, move |_, rsc| {
|
||||
let con = con.clone();
|
||||
token.edit(rsc).set("loading...");
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
let resp = con
|
||||
.request(GenerateToken {
|
||||
perms: ServerPerms::NONE,
|
||||
})
|
||||
.await;
|
||||
ctx.update(move |_, rsc| match resp {
|
||||
Ok(resp) => {
|
||||
token.edit(rsc).set(&resp.token);
|
||||
}
|
||||
Err(_) => {
|
||||
token.edit(rsc).set("failed to generate token");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
(generate.width(200), token).span(Dir::RIGHT).add(rsc)
|
||||
}
|
||||
|
||||
fn friends_list(rsc: &mut Rsc, session: &Session) -> WeakWidget {
|
||||
let ptr = loading_area("loading friends").add(rsc);
|
||||
let con = session.con.clone();
|
||||
rsc.events.register(ptr, Draw, move |_, rsc| {
|
||||
let con = con.clone();
|
||||
// TODO: maybe have rsc.request method that takes in &con?
|
||||
// need to also handle error tho
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
let Ok(resp) = con.request(RequestFriends).await else {
|
||||
return;
|
||||
};
|
||||
ctx.update(move |_, rsc| {
|
||||
let mut all = Span::empty(Dir::DOWN);
|
||||
if !resp.incoming.is_empty() {
|
||||
all.push(section_label("incoming").add_strong(rsc));
|
||||
all.push(user_list(&resp.incoming, rsc).add_strong(rsc))
|
||||
}
|
||||
all.push(section_label("friends").add_strong(rsc));
|
||||
all.push(user_list(&resp.current, rsc).add_strong(rsc));
|
||||
if !resp.outgoing.is_empty() {
|
||||
all.push(section_label("outgoing").add_strong(rsc));
|
||||
all.push(user_list(&resp.outgoing, rsc).add_strong(rsc))
|
||||
}
|
||||
all.set_ptr(ptr, rsc);
|
||||
});
|
||||
});
|
||||
});
|
||||
ptr
|
||||
}
|
||||
134
src/bin/client/ui/login.rs
Normal file
134
src/bin/client/ui/login.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use iris::prelude::*;
|
||||
use openworm::net::ClientMsg;
|
||||
|
||||
use crate::{net::AppHandle, state::ClientState};
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn start_screen(client: &mut Client, ui: &mut Ui) -> WidgetHandle {
|
||||
let mut accounts = Span::empty(Dir::DOWN);
|
||||
|
||||
accounts.push(
|
||||
wtext("no accounts")
|
||||
.size(20)
|
||||
.center_text()
|
||||
.color(Color::GRAY)
|
||||
.height(60)
|
||||
.add(ui)
|
||||
.any(),
|
||||
);
|
||||
|
||||
(
|
||||
wtext("Select Account").text_align(Align::CENTER).size(30),
|
||||
accounts,
|
||||
(
|
||||
submit_button("connect", |_, _| {}),
|
||||
submit_button("create", |_, _| {}),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.gap(10),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.gap(10)
|
||||
.pad(15)
|
||||
.background(rect(Color::BLACK.brighter(0.2)).radius(15))
|
||||
.width(400)
|
||||
.align(Align::CENTER)
|
||||
.add(ui)
|
||||
.any()
|
||||
}
|
||||
|
||||
pub fn connect_screen(client: &mut Client, ui: &mut Ui, state: &UiState) -> WidgetHandle {
|
||||
let Client { data, proxy, .. } = client;
|
||||
let ip = field_widget(&data.ip, "ip", ui);
|
||||
let handle = AppHandle {
|
||||
proxy: proxy.clone(),
|
||||
window: state.window.clone(),
|
||||
};
|
||||
let submit = submit_button("connect", move |client, _ui| {
|
||||
let ClientState::Connect(state) = &mut client.state else {
|
||||
return;
|
||||
};
|
||||
let ip = ip_.get().content();
|
||||
state.handle = Some(connect(handle.clone(), ConnectInfo { ip }));
|
||||
});
|
||||
(
|
||||
wtext("connect to a server")
|
||||
.text_align(Align::CENTER)
|
||||
.size(30),
|
||||
field_box(
|
||||
// NOTE: should probably do this on submit
|
||||
ip.on(Edited, |ctx| {
|
||||
ctx.state.data.ip = ctx.widget.get().content();
|
||||
})
|
||||
.add(ui),
|
||||
ui,
|
||||
),
|
||||
submit,
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.gap(10)
|
||||
.pad(15)
|
||||
.background(rect(Color::BLACK.brighter(0.2)).radius(15))
|
||||
.width(400)
|
||||
.align(Align::CENTER)
|
||||
.add(ui)
|
||||
.any()
|
||||
}
|
||||
|
||||
pub fn login_screen(client: &mut Client, ui: &mut Ui) -> WidgetRef {
|
||||
let Client { data, .. } = client;
|
||||
let username = field_widget(&data.username, "username", ui);
|
||||
let password = field_widget(&data.password, "password", ui);
|
||||
let username_ = username.clone();
|
||||
let password_ = password.clone();
|
||||
let submit = submit_button("login", move |client, _ui| {
|
||||
let ClientState::Login(state) = &mut client.state else {
|
||||
return;
|
||||
};
|
||||
let username = username_.get().content();
|
||||
let password = password_.get().content();
|
||||
state.handle.send(ClientMsg::Login { username, password });
|
||||
});
|
||||
let username_ = username.clone();
|
||||
let password_ = password.clone();
|
||||
let create_button = submit_button("create account", move |client, _ui| {
|
||||
let ClientState::Login(state) = &mut client.state else {
|
||||
return;
|
||||
};
|
||||
let username = username_.get().content();
|
||||
let password = password_.get().content();
|
||||
state
|
||||
.handle
|
||||
.send(ClientMsg::CreateAccount { username, password });
|
||||
});
|
||||
(
|
||||
wtext("login to server").text_align(Align::CENTER).size(30),
|
||||
field_box(
|
||||
username
|
||||
.on(Edited, |ctx| {
|
||||
ctx.state.data.username = ctx.widget.get().content();
|
||||
})
|
||||
.add(ui),
|
||||
ui,
|
||||
),
|
||||
field_box(
|
||||
password
|
||||
.on(Edited, |ctx| {
|
||||
ctx.state.data.password = ctx.widget.get().content();
|
||||
})
|
||||
.add(ui),
|
||||
ui,
|
||||
),
|
||||
submit,
|
||||
create_button,
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.gap(10)
|
||||
.pad(15)
|
||||
.background(rect(Color::BLACK.brighter(0.2)).radius(15))
|
||||
.width(400)
|
||||
.align(Align::CENTER)
|
||||
.add(ui)
|
||||
.any()
|
||||
}
|
||||
@@ -1,82 +1,32 @@
|
||||
use std::hash::Hash;
|
||||
|
||||
use super::*;
|
||||
use crate::state::{ClientState, LoggedIn};
|
||||
use openworm::net::{ClientMsg, NetClientMsg};
|
||||
|
||||
pub const SIZE: u32 = 20;
|
||||
|
||||
pub fn main_view(client: &mut Client, ui: &mut Ui) -> WidgetRef {
|
||||
let ClientState::LoggedIn(state) = &mut client.state else {
|
||||
panic!("we ain't logged in buh");
|
||||
};
|
||||
let msg_panel = msg_panel(ui, state);
|
||||
let side_bar = rect(Color::BLACK.brighter(0.05)).width(80);
|
||||
|
||||
(side_bar, msg_panel).span(Dir::RIGHT).add(ui)
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||
pub enum MainView {
|
||||
Channel,
|
||||
Friends,
|
||||
Server,
|
||||
}
|
||||
|
||||
pub fn msg_widget(username: &str, content: &str) -> impl WidgetRet {
|
||||
let content = wtext(content)
|
||||
.editable(false)
|
||||
.size(SIZE)
|
||||
.wrap(true)
|
||||
.attr::<Selectable>(());
|
||||
let header = wtext(username).size(SIZE);
|
||||
(
|
||||
image(include_bytes!("../assets/sungals.png"))
|
||||
.sized((70, 70))
|
||||
.align(Align::TOP),
|
||||
(header, content)
|
||||
.span(Dir::DOWN)
|
||||
.gap(10)
|
||||
.width(rest(1))
|
||||
.align(Align::TOP),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.gap(10)
|
||||
.to_any()
|
||||
}
|
||||
|
||||
pub fn msg_panel(ui: &mut Ui, state: &mut LoggedIn) -> WidgetRef {
|
||||
let msg_area = Span::empty(Dir::DOWN).gap(15).add(ui);
|
||||
state.channel = Some(msg_area.clone());
|
||||
|
||||
let send_text = wtext("")
|
||||
.editable(false)
|
||||
.size(SIZE)
|
||||
.wrap(true)
|
||||
.hint(hint("send message"))
|
||||
.add(ui);
|
||||
|
||||
(
|
||||
msg_area
|
||||
.clone()
|
||||
.scroll()
|
||||
.pad(Padding::x(15).with_top(15))
|
||||
.height(rest(1)),
|
||||
send_text
|
||||
.clone()
|
||||
.on(Submit, move |ctx| {
|
||||
let ClientState::LoggedIn(state) = &mut ctx.state.state else {
|
||||
panic!("we ain't logged in buh");
|
||||
};
|
||||
let content = ctx.widget.get_mut().take();
|
||||
let msg = NetClientMsg {
|
||||
content: content.clone(),
|
||||
};
|
||||
state.network.send(ClientMsg::SendMsg(msg.clone()));
|
||||
let msg = msg_widget(&state.username, &content).add(ctx.ui);
|
||||
msg_area.get_mut().children.push(msg);
|
||||
})
|
||||
.pad(15)
|
||||
.attr::<Selector>(send_text)
|
||||
.scroll()
|
||||
.masked()
|
||||
.background(rect(Color::BLACK.brighter(0.05)).radius(15))
|
||||
.pad(15)
|
||||
.max_height(rel(0.5))
|
||||
.layer_offset(1),
|
||||
)
|
||||
.span(Dir::DOWN)
|
||||
.width(rest(1))
|
||||
.add(ui)
|
||||
pub fn main_view(rsc: &mut Rsc, session: Session) -> WeakWidget {
|
||||
let mut view = WidgetSelector::new(MainView::Channel, channel::view(rsc));
|
||||
view.set(MainView::Server, server::view(rsc, &session));
|
||||
view.set(MainView::Friends, friends::view(rsc, &session));
|
||||
let view = view.add(rsc);
|
||||
let [channel, friends, server] = tabs(
|
||||
rsc,
|
||||
view,
|
||||
[
|
||||
("channel", MainView::Channel),
|
||||
("friends", MainView::Friends),
|
||||
("server", MainView::Server),
|
||||
],
|
||||
);
|
||||
let top_bar = rect(Color::BLACK.alpha(150))
|
||||
.height(50)
|
||||
.foreground((channel, friends, server).span(Dir::RIGHT));
|
||||
(top_bar, view).span(Dir::DOWN).add(rsc)
|
||||
}
|
||||
|
||||
@@ -1,85 +1,79 @@
|
||||
use openworm::net::UserId;
|
||||
|
||||
use crate::Client;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn werror(ui: &mut Ui, msg: &str) -> WidgetRef {
|
||||
pub fn werror(msg: &str, rsc: &mut Rsc) -> StrongWidget {
|
||||
wtext(msg)
|
||||
.size(20)
|
||||
.pad(10)
|
||||
.background(rect(Color::RED).radius(10))
|
||||
.add(ui)
|
||||
.add_strong(rsc)
|
||||
}
|
||||
|
||||
pub fn hint(msg: impl Into<String>) -> TextBuilder {
|
||||
pub fn hint_text(msg: impl Into<String>) -> TextBuilder<Rsc> {
|
||||
wtext(msg).size(20).color(Color::GRAY)
|
||||
}
|
||||
|
||||
pub fn field_widget(name: &str, hint_text: &str, ui: &mut Ui) -> WidgetRef<TextEdit> {
|
||||
wtext(name)
|
||||
.editable(true)
|
||||
.size(20)
|
||||
.hint(hint(hint_text))
|
||||
.add(ui)
|
||||
pub fn large_hint_text(msg: impl Into<String>) -> TextBuilder<Rsc> {
|
||||
wtext(msg).size(30).color(Color::GRAY)
|
||||
}
|
||||
|
||||
pub fn field_box(field: WidgetRef<TextEdit>, ui: &mut Ui) -> WidgetRef {
|
||||
pub fn field(default: &str, hint: &str) -> impl FnOnce(&mut Rsc) -> TextEdit {
|
||||
wtext(default)
|
||||
.editable(EditMode::SingleLine)
|
||||
.size(20)
|
||||
.hint(hint_text(hint))
|
||||
}
|
||||
|
||||
pub fn field_box(field: WeakWidget<TextEdit>) -> impl WidgetIdFn<Rsc, Stack> {
|
||||
field
|
||||
.clone()
|
||||
.pad(10)
|
||||
.background(rect(Color::BLACK.brighter(0.1)).radius(15))
|
||||
.attr::<Selector>(field)
|
||||
.add(ui)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Copy, WidgetView)]
|
||||
pub struct Button {
|
||||
#[root]
|
||||
root: WeakWidget,
|
||||
rect: WeakWidget<Rect>,
|
||||
color: UiColor,
|
||||
root: WidgetRef,
|
||||
rect: WidgetRef<Rect>,
|
||||
enabled: Handle<bool>,
|
||||
}
|
||||
|
||||
impl WidgetView for Button {
|
||||
fn view(&self) -> &WidgetRef<Self::Widget> {
|
||||
&self.root
|
||||
}
|
||||
enabled: WeakState<bool>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(text: &str, color: UiColor, ui: &mut Ui) -> Self {
|
||||
let rect = rect(color).radius(15).add(ui);
|
||||
let enabled = Handle::from(true);
|
||||
let enabled_ = enabled.clone();
|
||||
let enabled__ = enabled.clone();
|
||||
pub fn new_fg(fg: WeakWidget, color: UiColor, rsc: &mut Rsc) -> Self {
|
||||
let rect = rect(color).radius(15).add(rsc);
|
||||
let enabled = rsc.create_state(rect, true);
|
||||
let root = rect
|
||||
.clone()
|
||||
.on(
|
||||
CursorSense::HoverStart | CursorSense::unclick(),
|
||||
move |ctx| {
|
||||
if !*enabled_.get() {
|
||||
move |ctx, rsc: &mut Rsc| {
|
||||
if !rsc[enabled] {
|
||||
return;
|
||||
}
|
||||
ctx.widget.get_mut().color = color.brighter(0.1);
|
||||
rsc[ctx.widget].color = color.brighter(0.1);
|
||||
},
|
||||
)
|
||||
.on(CursorSense::HoverEnd, move |ctx| {
|
||||
if !*enabled__.get() {
|
||||
.on(CursorSense::HoverEnd, move |ctx, rsc| {
|
||||
if !rsc[enabled] {
|
||||
return;
|
||||
}
|
||||
ctx.widget.get_mut().color = color;
|
||||
rsc[ctx.widget].color = color;
|
||||
})
|
||||
.height(60)
|
||||
.foreground(wtext(text).size(25).text_align(Align::CENTER))
|
||||
.add(ui);
|
||||
let root_ = root.clone();
|
||||
let enabled_ = enabled.clone();
|
||||
rect.clone()
|
||||
.on(CursorSense::click(), move |ctx| {
|
||||
if !*enabled_.get() {
|
||||
.foreground(fg)
|
||||
.add(rsc);
|
||||
rect.on(CursorSense::click(), move |ctx, rsc: &mut Rsc| {
|
||||
if !rsc[enabled] {
|
||||
return;
|
||||
}
|
||||
ctx.widget.get_mut().color = color.darker(0.2);
|
||||
ctx.ui.run_event(ctx.state, &root_, Submit, ());
|
||||
rsc[ctx.widget].color = color.darker(0.2);
|
||||
rsc.run_event::<Submit>(root, (), ctx.state);
|
||||
})
|
||||
.add(ui);
|
||||
.add(rsc);
|
||||
Self {
|
||||
root,
|
||||
rect,
|
||||
@@ -88,26 +82,90 @@ impl Button {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn submit(text: &str, ui: &mut Ui) -> Self {
|
||||
Self::new(text, color::GREEN, ui)
|
||||
pub fn new(text: &str, color: UiColor, rsc: &mut Rsc) -> Self {
|
||||
Self::new_fg(
|
||||
wtext(text).size(25).text_align(Align::CENTER).add(rsc),
|
||||
color,
|
||||
rsc,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn disable(&self) {
|
||||
*self.enabled.get_mut() = false;
|
||||
self.rect.get_mut().color = self.color.darker(0.8);
|
||||
pub fn submit(text: &str, rsc: &mut Rsc) -> Self {
|
||||
Self::new(text, color::GREEN, rsc)
|
||||
}
|
||||
|
||||
pub fn normal(text: &str, rsc: &mut Rsc) -> Self {
|
||||
Self::new(text, color::DARK, rsc)
|
||||
}
|
||||
|
||||
pub fn disable(&self, rsc: &mut Rsc) {
|
||||
rsc[self.enabled] = false;
|
||||
rsc[self.rect].color = self.color.darker(0.8);
|
||||
}
|
||||
|
||||
pub fn enable(&self, rsc: &mut Rsc) {
|
||||
rsc[self.enabled] = true;
|
||||
rsc[self.rect].color = self.color;
|
||||
}
|
||||
}
|
||||
|
||||
widget_trait! {
|
||||
pub trait Stuff;
|
||||
fn modal(self, width: impl UiNum) -> impl WidgetIdFn {
|
||||
|ui| {
|
||||
pub trait Stuff<Rsc: UiRsc + 'static>;
|
||||
fn modal(self, width: impl UiNum) -> impl WidgetIdFn<Rsc> {
|
||||
|rsc| {
|
||||
self
|
||||
.pad(15)
|
||||
.background(rect(color::MODAL_BG).radius(15))
|
||||
.width(width)
|
||||
.align(Align::CENTER)
|
||||
.add(ui)
|
||||
.add(rsc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tabs<T: Eq + std::hash::Hash + 'static + Copy, const N: usize>(
|
||||
rsc: &mut Rsc,
|
||||
view: WeakWidget<WidgetSelector<T>>,
|
||||
list: [(&'static str, T); N],
|
||||
) -> [WeakWidget; N] {
|
||||
list.map(|(name, sel)| {
|
||||
Button::normal(name, rsc)
|
||||
.on(Submit, move |_, rsc: &mut Rsc| {
|
||||
rsc[view].select(sel);
|
||||
})
|
||||
.add(rsc)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn loading_area(text: &str) -> impl FnOnce(&mut Rsc) -> WidgetPtr {
|
||||
move |rsc| {
|
||||
WidgetPtr::new(
|
||||
large_hint_text(text)
|
||||
.center_text()
|
||||
.width(rest(1))
|
||||
.add_strong(rsc),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn section_label(text: impl Into<String>) -> TextBuilder<Rsc> {
|
||||
wtext(text).size(30)
|
||||
}
|
||||
|
||||
pub fn user_list<'a>(ids: impl IntoIterator<Item = &'a UserId>, rsc: &mut Rsc) -> Span {
|
||||
let mut span = Span::empty(Dir::DOWN);
|
||||
for id in ids {
|
||||
let thing = (wtext(id.to_string()).size(20),)
|
||||
.span(Dir::RIGHT)
|
||||
.gap(30)
|
||||
.pad(15);
|
||||
span.push(thing.add_strong(rsc));
|
||||
}
|
||||
span
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn error(&self, text: &str, rsc: &mut Rsc) {
|
||||
rsc[self.notif].inner = Some(werror(text, rsc));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use crate::Client;
|
||||
use crate::{Rsc, session::Session};
|
||||
use iris::prelude::*;
|
||||
use len_fns::*;
|
||||
|
||||
mod channel;
|
||||
pub mod color;
|
||||
mod connect;
|
||||
mod friends;
|
||||
mod main;
|
||||
mod misc;
|
||||
pub mod color;
|
||||
mod server;
|
||||
mod user;
|
||||
|
||||
pub use connect::*;
|
||||
pub use main::*;
|
||||
pub use misc::*;
|
||||
|
||||
event_ctx!(Client);
|
||||
pub use user::*;
|
||||
|
||||
56
src/bin/client/ui/server.rs
Normal file
56
src/bin/client/ui/server.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use openworm::net::RequestUsers;
|
||||
|
||||
use crate::session::Session;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||
enum View {
|
||||
Info,
|
||||
User,
|
||||
}
|
||||
|
||||
pub fn view(rsc: &mut Rsc, session: &Session) -> StrongWidget {
|
||||
let mut view = WidgetSelector::new(View::Info, info(rsc));
|
||||
view.set(View::User, users(rsc, session));
|
||||
let view = view.add(rsc);
|
||||
let [info, server] = tabs(rsc, view, [("info", View::Info), ("users", View::User)]);
|
||||
|
||||
let side_bar = rect(Color::BLACK.alpha(150))
|
||||
.foreground((info, server).span(Dir::DOWN))
|
||||
.width(260);
|
||||
|
||||
(side_bar, view).span(Dir::RIGHT).add_strong(rsc)
|
||||
}
|
||||
|
||||
fn info(rsc: &mut Rsc) -> StrongWidget {
|
||||
wtext("server info").center_text().add_strong(rsc)
|
||||
}
|
||||
|
||||
fn users(rsc: &mut Rsc, session: &Session) -> StrongWidget {
|
||||
let ptr = loading_area("loading users").add(rsc);
|
||||
let con = session.con.clone();
|
||||
rsc.events.register(ptr, Draw, move |_, rsc| {
|
||||
let con = con.clone();
|
||||
rsc.spawn_task(async move |mut ctx| {
|
||||
let Ok(resp) = con.request(RequestUsers).await else {
|
||||
return;
|
||||
};
|
||||
ctx.update(move |_, rsc| {
|
||||
let mut span = Span::empty(Dir::DOWN);
|
||||
for user in resp.users {
|
||||
let thing = (
|
||||
wtext(user.id.to_string()).size(20),
|
||||
wtext(user.username).size(20),
|
||||
)
|
||||
.span(Dir::RIGHT)
|
||||
.gap(30)
|
||||
.pad(15);
|
||||
span.push(thing.add_strong(rsc));
|
||||
}
|
||||
span.set_ptr(ptr, rsc);
|
||||
});
|
||||
});
|
||||
});
|
||||
ptr.upgrade(rsc)
|
||||
}
|
||||
53
src/bin/client/ui/user.rs
Normal file
53
src/bin/client/ui/user.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use super::*;
|
||||
use crate::net::{NetHandle, SyncRecv};
|
||||
use iris::core::util::HashMap;
|
||||
use openworm::net::{RequestUserInfo, UserId, UserInfo};
|
||||
|
||||
pub struct UserCache {
|
||||
pub con: NetHandle,
|
||||
pub requests: HashMap<UserId, SyncRecv<RequestUserInfo>>,
|
||||
pub users: HashMap<UserId, UserInfo>,
|
||||
pub widgets: HashMap<UserId, Vec<WeakWidget<Text>>>,
|
||||
}
|
||||
|
||||
impl UserCache {
|
||||
pub fn new(con: NetHandle) -> Self {
|
||||
Self {
|
||||
con,
|
||||
requests: Default::default(),
|
||||
users: Default::default(),
|
||||
widgets: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn username(&mut self, id: UserId, rsc: &mut Rsc) -> WeakWidget {
|
||||
let text = if let Some(user) = self.users.get(&id) {
|
||||
&user.username
|
||||
} else {
|
||||
if !self.requests.contains_key(&id) {
|
||||
let recv = self.con.request_sync(RequestUserInfo { id });
|
||||
self.requests.insert(id, recv);
|
||||
}
|
||||
"loading..."
|
||||
};
|
||||
let wid = wtext(text).add(rsc);
|
||||
self.widgets.entry(id).or_default().push(wid);
|
||||
wid
|
||||
}
|
||||
|
||||
pub fn update(&mut self, rsc: &mut Rsc) {
|
||||
self.requests.retain(|id, req| {
|
||||
if let Some(resp) = req.try_recv() {
|
||||
if let Ok(info) = resp {
|
||||
for &widget in self.widgets.get(id).into_iter().flatten() {
|
||||
*rsc[widget].content = info.username.clone();
|
||||
}
|
||||
self.users.insert(*id, info);
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub struct DBMsg {
|
||||
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
ops::{Deref, DerefMut},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use bincode::{Decode, Encode};
|
||||
use openworm::net::BINCODE_CONFIG;
|
||||
use sled::Tree;
|
||||
|
||||
pub const DB_VERSION: u64 = 0;
|
||||
|
||||
#[derive(Encode, Decode)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode)]
|
||||
pub struct Msg {
|
||||
pub user: u64,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Db {
|
||||
pub db: sled::Db,
|
||||
pub msgs: DbMap<u64, Msg>,
|
||||
pub users: DbMap<u64, User>,
|
||||
pub usernames: DbMap<String, u64>,
|
||||
}
|
||||
|
||||
pub struct DbMap<K, V> {
|
||||
tree: Tree,
|
||||
_pd: PhantomData<(K, V)>,
|
||||
}
|
||||
|
||||
pub trait Key {
|
||||
type Output<'a>: AsRef<[u8]>
|
||||
where
|
||||
Self: 'a;
|
||||
fn bytes(&self) -> Self::Output<'_>;
|
||||
}
|
||||
|
||||
impl Key for String {
|
||||
type Output<'a> = &'a Self;
|
||||
fn bytes(&self) -> Self::Output<'_> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Key for str {
|
||||
type Output<'a> = &'a Self;
|
||||
fn bytes(&self) -> Self::Output<'_> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Key for u64 {
|
||||
type Output<'a> = [u8; 8];
|
||||
|
||||
fn bytes(&self) -> Self::Output<'_> {
|
||||
self.to_be_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Key, V: Encode + Decode<()>> DbMap<K, V> {
|
||||
pub fn insert(&self, k: &K, v: &V) {
|
||||
self.tree.insert_(k, v);
|
||||
}
|
||||
|
||||
pub fn get(&self, k: &K) -> Option<V> {
|
||||
self.tree.get_(k)
|
||||
}
|
||||
|
||||
pub fn init_unique(&self, k: &K) -> bool {
|
||||
self.tree
|
||||
.compare_and_swap(k.bytes(), None as Option<&[u8]>, Some(&[0]))
|
||||
.unwrap()
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn iter_all(&self) -> impl Iterator<Item = V> {
|
||||
self.tree.iter_all()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_db(path: impl AsRef<Path>) -> Db {
|
||||
let db = sled::open(path).expect("failed to open database");
|
||||
if !db.was_recovered() {
|
||||
println!("no previous db found, creating new");
|
||||
db.insert_("version", DB_VERSION);
|
||||
db.flush().unwrap();
|
||||
} else {
|
||||
let version: u64 = db.get_("version").expect("failed to read db version");
|
||||
println!("found existing db version {version}");
|
||||
if version != DB_VERSION {
|
||||
panic!("non matching db version! (auto update in the future)");
|
||||
}
|
||||
}
|
||||
Db {
|
||||
msgs: open_tree("msg", &db),
|
||||
users: open_tree("user", &db),
|
||||
usernames: open_tree("username", &db),
|
||||
db,
|
||||
}
|
||||
}
|
||||
|
||||
trait DbUtil {
|
||||
fn insert_<V: Encode>(&self, k: &(impl Key + ?Sized), v: V);
|
||||
fn get_<V: Decode<()>>(&self, k: &(impl Key + ?Sized)) -> Option<V>;
|
||||
fn iter_all<V: Decode<()>>(&self) -> impl Iterator<Item = V>;
|
||||
}
|
||||
|
||||
impl DbUtil for Tree {
|
||||
fn insert_<V: Encode>(&self, k: &(impl Key + ?Sized), v: V) {
|
||||
let bytes = bincode::encode_to_vec(v, BINCODE_CONFIG).unwrap();
|
||||
self.insert(k.bytes(), bytes).unwrap();
|
||||
}
|
||||
|
||||
fn get_<V: Decode<()>>(&self, k: &(impl Key + ?Sized)) -> Option<V> {
|
||||
let bytes = self.get(k.bytes()).unwrap()?;
|
||||
Some(
|
||||
bincode::decode_from_slice(&bytes, BINCODE_CONFIG)
|
||||
.unwrap()
|
||||
.0,
|
||||
)
|
||||
}
|
||||
|
||||
fn iter_all<V: Decode<()>>(&self) -> impl Iterator<Item = V> {
|
||||
self.iter().map(|r| {
|
||||
bincode::decode_from_slice(&r.unwrap().1, BINCODE_CONFIG)
|
||||
.unwrap()
|
||||
.0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_tree<K, V>(name: &str, db: &sled::Db) -> DbMap<K, V> {
|
||||
DbMap {
|
||||
tree: db.open_tree(name).unwrap(),
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Db {
|
||||
type Target = sled::Db;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Db {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.db
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Clone for DbMap<K, V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
tree: self.tree.clone(),
|
||||
_pd: self._pd,
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/bin/server/db/data.rs
Normal file
48
src/bin/server/db/data.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use iris::core::util::HashSet;
|
||||
pub use openworm::net::{AccountToken, ServerPerms};
|
||||
|
||||
pub type UserId = u64;
|
||||
pub type MsgId = i128;
|
||||
pub type ChannelId = u64;
|
||||
pub type ImageId = u64;
|
||||
|
||||
#[derive(bitcode::Encode, bitcode::Decode)]
|
||||
pub struct User {
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub pfp: Option<ImageId>,
|
||||
pub bio: String,
|
||||
pub friends: Friends,
|
||||
pub server_perms: ServerPerms,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn new(username: String, password_hash: String, perms: ServerPerms) -> Self {
|
||||
Self {
|
||||
username,
|
||||
password_hash,
|
||||
bio: String::new(),
|
||||
pfp: None,
|
||||
friends: Default::default(),
|
||||
server_perms: perms,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct Friends {
|
||||
pub current: HashSet<UserId>,
|
||||
pub outgoing: HashSet<UserId>,
|
||||
pub incoming: HashSet<UserId>,
|
||||
}
|
||||
|
||||
#[derive(bitcode::Encode, bitcode::Decode)]
|
||||
pub struct Msg {
|
||||
pub content: String,
|
||||
pub author: UserId,
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
pub name: String,
|
||||
pub desc: String,
|
||||
}
|
||||
49
src/bin/server/db/mod.rs
Normal file
49
src/bin/server/db/mod.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
mod data;
|
||||
mod util;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
pub use data::*;
|
||||
use util::*;
|
||||
|
||||
pub const DB_VERSION: u64 = 0;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Db {
|
||||
db: Database,
|
||||
pub account_tokens: DbMap<AccountToken, ServerPerms>,
|
||||
pub msgs: DbMap<MsgId, Msg>,
|
||||
pub users: DbMap<UserId, User>,
|
||||
pub usernames: DbMap<String, UserId>,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub fn open(path: impl AsRef<Path>) -> Db {
|
||||
let db = Database::open(path);
|
||||
let info: DbMap<String, u64> = DbMap::open("info", &db);
|
||||
if let Some(version) = info.get("version") {
|
||||
println!("found existing db version {version}");
|
||||
if version != DB_VERSION {
|
||||
panic!("non matching db version! (auto update in the future)");
|
||||
}
|
||||
} else {
|
||||
println!("no previous db found, creating new version {DB_VERSION}");
|
||||
info.insert("version", &DB_VERSION);
|
||||
}
|
||||
Db {
|
||||
account_tokens: DbMap::open("account_token", &db),
|
||||
msgs: DbMap::open("msg", &db),
|
||||
users: DbMap::open("user", &db),
|
||||
usernames: DbMap::open("username", &db),
|
||||
db,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Db {
|
||||
type Target = Database;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.db
|
||||
}
|
||||
}
|
||||
220
src/bin/server/db/util.rs
Normal file
220
src/bin/server/db/util.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use std::{marker::PhantomData, path::Path};
|
||||
|
||||
use bitcode::{DecodeOwned, Encode};
|
||||
use fjall::{KeyspaceCreateOptions, OptimisticTxDatabase, OptimisticTxKeyspace, Readable, Slice};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database(fjall::OptimisticTxDatabase);
|
||||
|
||||
pub struct DbMap<K, V> {
|
||||
db: Database,
|
||||
keyspace: OptimisticTxKeyspace,
|
||||
_pd: PhantomData<(K, V)>,
|
||||
}
|
||||
|
||||
impl<K, V> Clone for DbMap<K, V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
db: self.db.clone(),
|
||||
keyspace: self.keyspace.clone(),
|
||||
_pd: self._pd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> DbMap<K, V> {
|
||||
pub fn open(name: &str, db: &Database) -> DbMap<K, V> {
|
||||
DbMap {
|
||||
db: db.clone(),
|
||||
keyspace: db.0.keyspace(name, KeyspaceCreateOptions::default).unwrap(),
|
||||
_pd: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Key<Output = K>, V: Encode + DecodeOwned> DbMap<K, V> {
|
||||
// TODO: K2 IS NOT A SAFE ABSTRACTION!! need to have KeyLike which has key assoc type
|
||||
pub fn insert<K2: Key<Output = K> + ?Sized>(&self, k: &K2, v: &V) {
|
||||
let k = Slice::new(k.to_bytes().as_ref());
|
||||
let v = Slice::new(&bitcode::encode(v));
|
||||
self.keyspace.insert(k, v).unwrap();
|
||||
}
|
||||
|
||||
pub fn get<K2: Key<Output = K> + ?Sized>(&self, k: &K2) -> Option<V> {
|
||||
let k = Slice::new(k.to_bytes().as_ref());
|
||||
let v = self.keyspace.get(k).unwrap()?;
|
||||
Some(bitcode::decode(&v).unwrap())
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.keyspace.approximate_len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = (K, V)> {
|
||||
self.db.read_tx().iter(self)
|
||||
}
|
||||
|
||||
pub fn values(&self) -> impl Iterator<Item = V> {
|
||||
self.db.read_tx().values(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn open(path: impl AsRef<Path>) -> Self {
|
||||
let inner = OptimisticTxDatabase::builder(path.as_ref())
|
||||
.open()
|
||||
.expect("failed to open database");
|
||||
Self(inner)
|
||||
}
|
||||
pub fn read_tx(&self) -> ReadTx {
|
||||
ReadTx(self.0.read_tx())
|
||||
}
|
||||
|
||||
pub fn write_tx(&self) -> WriteTx {
|
||||
WriteTx(self.0.write_tx().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadTx(fjall::Snapshot);
|
||||
|
||||
impl ReadTx {
|
||||
pub fn values<K, V: DecodeOwned>(
|
||||
&self,
|
||||
map: &DbMap<K, V>,
|
||||
) -> impl Iterator<Item = V> + use<K, V> {
|
||||
self.0.iter(&map.keyspace).map(|g| {
|
||||
let v = g.value().unwrap();
|
||||
bitcode::decode(&v).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter<K: Key<Output = K>, V: DecodeOwned>(
|
||||
&self,
|
||||
map: &DbMap<K, V>,
|
||||
) -> impl Iterator<Item = (K, V)> + use<K, V> {
|
||||
self.0.iter(&map.keyspace).map(|g| {
|
||||
let (k, v) = g.into_inner().unwrap();
|
||||
(K::from_bytes(&k), bitcode::decode(&v).unwrap())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WriteTx(fjall::OptimisticWriteTx);
|
||||
|
||||
impl WriteTx {
|
||||
pub fn values<K, V: DecodeOwned>(
|
||||
&self,
|
||||
map: &DbMap<K, V>,
|
||||
) -> impl Iterator<Item = V> + use<K, V> {
|
||||
self.0.iter(&map.keyspace).map(|g| {
|
||||
let v = g.value().unwrap();
|
||||
bitcode::decode(&v).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn iter<K: Key<Output = K>, V: DecodeOwned>(
|
||||
&self,
|
||||
map: &DbMap<K, V>,
|
||||
) -> impl Iterator<Item = (K, V)> + use<K, V> {
|
||||
self.0.iter(&map.keyspace).map(|g| {
|
||||
let (k, v) = g.into_inner().unwrap();
|
||||
(K::from_bytes(&k), bitcode::decode(&v).unwrap())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get<K: Key<Output = K>, V: DecodeOwned>(&self, map: &DbMap<K, V>, k: &K) -> Option<V> {
|
||||
let k = Slice::new(k.to_bytes().as_ref());
|
||||
let v = self.0.get(&map.keyspace, k).unwrap()?;
|
||||
Some(bitcode::decode(&v).unwrap())
|
||||
}
|
||||
|
||||
pub fn has_key<K: Key<Output = K>, V>(&self, map: &DbMap<K, V>, k: K) -> bool {
|
||||
let k = Slice::new(k.to_bytes().as_ref());
|
||||
self.0.get(&map.keyspace, k).unwrap().is_some()
|
||||
}
|
||||
|
||||
pub fn remove<K: Key<Output = K>, V: DecodeOwned>(
|
||||
&mut self,
|
||||
map: &DbMap<K, V>,
|
||||
k: K,
|
||||
) -> Option<V> {
|
||||
let k = Slice::new(k.to_bytes().as_ref());
|
||||
let v = self.0.take(&map.keyspace, k).unwrap()?;
|
||||
Some(bitcode::decode(&v).unwrap())
|
||||
}
|
||||
|
||||
// TODO: K2 IS NOT A SAFE ABSTRACTION!! need to have KeyLike which has key assoc type
|
||||
pub fn insert<K: Key<Output = K>, K2: Key<Output = K> + ?Sized, V: Encode>(
|
||||
&mut self,
|
||||
map: &DbMap<K, V>,
|
||||
k: &K2,
|
||||
v: &V,
|
||||
) {
|
||||
let k = Slice::new(k.to_bytes().as_ref());
|
||||
let v = Slice::new(&bitcode::encode(v));
|
||||
self.0.insert(&map.keyspace, k, v);
|
||||
}
|
||||
|
||||
pub fn commit(self) -> bool {
|
||||
self.0.commit().unwrap().is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Key {
|
||||
type Input<'a>: AsRef<[u8]>
|
||||
where
|
||||
Self: 'a;
|
||||
type Output;
|
||||
fn to_bytes(&self) -> Self::Input<'_>;
|
||||
fn from_bytes(bytes: &[u8]) -> Self::Output;
|
||||
}
|
||||
|
||||
impl Key for String {
|
||||
type Input<'a> = &'a Self;
|
||||
type Output = String;
|
||||
fn to_bytes(&self) -> Self::Input<'_> {
|
||||
self
|
||||
}
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
Self::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Key for str {
|
||||
type Input<'a> = &'a Self;
|
||||
type Output = String;
|
||||
fn to_bytes(&self) -> Self::Input<'_> {
|
||||
self
|
||||
}
|
||||
fn from_bytes(bytes: &[u8]) -> String {
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl Key for u64 {
|
||||
type Input<'a> = [u8; 8];
|
||||
type Output = u64;
|
||||
|
||||
fn to_bytes(&self) -> Self::Input<'_> {
|
||||
self.to_be_bytes()
|
||||
}
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
Self::from_be_bytes(bytes.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Key for i128 {
|
||||
type Input<'a> = [u8; 16];
|
||||
type Output = i128;
|
||||
|
||||
fn to_bytes(&self) -> Self::Input<'_> {
|
||||
self.to_be_bytes()
|
||||
}
|
||||
fn from_bytes(bytes: &[u8]) -> Self {
|
||||
Self::from_be_bytes(bytes.try_into().unwrap())
|
||||
}
|
||||
}
|
||||
324
src/bin/server/handle.rs
Normal file
324
src/bin/server/handle.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use super::net::ClientSender;
|
||||
use crate::{
|
||||
ClientId, account_token,
|
||||
db::{Db, ServerPerms, User, UserId},
|
||||
net::ClientReplier,
|
||||
};
|
||||
use openworm::net::*;
|
||||
use scrypt::{
|
||||
Scrypt,
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ClientState {
|
||||
Login,
|
||||
Authed(UserId),
|
||||
}
|
||||
|
||||
pub struct ClientHandler {
|
||||
pub db: Db,
|
||||
pub send: ClientSender,
|
||||
pub senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>,
|
||||
pub id: ClientId,
|
||||
pub state: Arc<RwLock<ClientState>>,
|
||||
}
|
||||
|
||||
impl ClientHandler {
|
||||
pub async fn handle_msg(&self, msg: ClientMsg, replier: &ClientReplier) -> Option<()> {
|
||||
// TODO: there are some awful edge cases where you delete an account and it's in the middle
|
||||
// of a non TX transaction; make sure to use TX, maybe need another check_login closure
|
||||
// fortunately these should be very rare so I'ma ignore for now
|
||||
macro_rules! reply {
|
||||
($msg:expr) => {
|
||||
replier.send($msg).await;
|
||||
return None;
|
||||
};
|
||||
}
|
||||
let check_login = async || {
|
||||
if let ClientState::Authed(uid) = &*self.state.read().await {
|
||||
Some(*uid)
|
||||
} else {
|
||||
reply!(NotLoggedIn);
|
||||
}
|
||||
};
|
||||
let check_user = async || {
|
||||
if let ClientState::Authed(uid) = &*self.state.read().await {
|
||||
if let Some(user) = self.db.users.get(uid) {
|
||||
Some((*uid, user))
|
||||
} else {
|
||||
reply!(InvalidUser);
|
||||
}
|
||||
} else {
|
||||
reply!(NotLoggedIn);
|
||||
}
|
||||
};
|
||||
let check_server_perms = async |perms| {
|
||||
let id = check_login().await?;
|
||||
if self
|
||||
.db
|
||||
.users
|
||||
.get(&id)
|
||||
.is_some_and(|u| !u.server_perms.contains(perms))
|
||||
{
|
||||
reply!(NoPermission);
|
||||
} else {
|
||||
Some(())
|
||||
}
|
||||
};
|
||||
let db = &self.db;
|
||||
match msg {
|
||||
ClientMsg::GenerateToken(info) => {
|
||||
let check = if info.perms == ServerPerms::NONE {
|
||||
ServerPerms::ACCOUNT_TOKENS
|
||||
} else {
|
||||
ServerPerms::ALL
|
||||
};
|
||||
check_server_perms(check).await?;
|
||||
let token = account_token(&self.db, info.perms);
|
||||
self.db.account_tokens.insert(&token, &info.perms);
|
||||
reply!(GenerateTokenResp { token });
|
||||
}
|
||||
ClientMsg::CreateAccount(info) => {
|
||||
let CreateAccount {
|
||||
token,
|
||||
username,
|
||||
password,
|
||||
} = &info;
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let params = scrypt::Params::new(11, 8, 1, 32).unwrap();
|
||||
let hash = Scrypt
|
||||
.hash_password_customized(password.as_bytes(), None, None, params, &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let mut id;
|
||||
loop {
|
||||
let mut tx = db.write_tx();
|
||||
let Some(perms) = tx.remove(&db.account_tokens, token.to_string()) else {
|
||||
println!("invalid token: {:?}", self.send.remote());
|
||||
reply!(CreateAccountResp::InvalidToken);
|
||||
};
|
||||
if tx.has_key(&db.usernames, username.clone()) {
|
||||
reply!(CreateAccountResp::UsernameExists);
|
||||
}
|
||||
id = rand::random();
|
||||
while tx.has_key(&db.users, id) {
|
||||
id = rand::random();
|
||||
}
|
||||
tx.insert(
|
||||
&db.users,
|
||||
&id,
|
||||
&User::new(username.clone(), hash.clone(), perms),
|
||||
);
|
||||
tx.insert(&db.usernames, username, &id);
|
||||
if tx.commit() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("account created: \"{username}\"");
|
||||
*self.state.write().await = ClientState::Authed(id);
|
||||
reply!(CreateAccountResp::Ok { id });
|
||||
}
|
||||
ClientMsg::Login(info) => {
|
||||
let Login { username, password } = &info;
|
||||
let Some(id) = db.usernames.get(username) else {
|
||||
reply!(LoginResp::UnknownUsername);
|
||||
};
|
||||
let Some(user) = db.users.get(&id) else {
|
||||
panic!("invalid state! (should be a user)");
|
||||
};
|
||||
let hash = PasswordHash::new(&user.password_hash).unwrap();
|
||||
if Scrypt.verify_password(password.as_bytes(), &hash).is_err() {
|
||||
println!("invalid password: \"{username}\"");
|
||||
reply!(LoginResp::InvalidPassword);
|
||||
}
|
||||
*self.state.write().await = ClientState::Authed(id);
|
||||
reply!(LoginResp::Ok { id });
|
||||
}
|
||||
ClientMsg::RequestUsers(_) => {
|
||||
check_login().await?;
|
||||
check_server_perms(ServerPerms::ALL).await?;
|
||||
let users: Vec<_> = self
|
||||
.db
|
||||
.users
|
||||
.iter()
|
||||
.map(|(id, u)| ServerUser {
|
||||
id,
|
||||
username: u.username,
|
||||
})
|
||||
.collect();
|
||||
reply!(RequestUsersResp { users });
|
||||
}
|
||||
ClientMsg::AddFriend(info) => {
|
||||
let user_id = check_login().await?;
|
||||
loop {
|
||||
let mut tx = db.write_tx();
|
||||
let Some(mut user) = tx.get(&db.users, &user_id) else {
|
||||
reply!(InvalidUser);
|
||||
};
|
||||
let Some(other_id) = tx.get(&db.usernames, &info.username) else {
|
||||
reply!(AddFriendResp::UnknownUser);
|
||||
};
|
||||
if user.friends.outgoing.contains(&other_id) {
|
||||
reply!(AddFriendResp::AlreadySent);
|
||||
}
|
||||
if other_id == user_id {
|
||||
reply!(AddFriendResp::CannotAddSelf);
|
||||
}
|
||||
let Some(mut other) = tx.get(&db.users, &other_id) else {
|
||||
println!("WARNING: username without valid user!");
|
||||
reply!(AddFriendResp::UnknownUser);
|
||||
};
|
||||
if other.friends.current.contains(&user_id) {
|
||||
reply!(AddFriendResp::AlreadyFriends);
|
||||
}
|
||||
user.friends.outgoing.insert(other_id);
|
||||
other.friends.incoming.insert(user_id);
|
||||
tx.insert(&db.users, &user_id, &user);
|
||||
tx.insert(&db.users, &other_id, &other);
|
||||
if tx.commit() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
reply!(AddFriendResp::Ok);
|
||||
}
|
||||
ClientMsg::RemoveFriend(info) => {
|
||||
let user_id = check_login().await?;
|
||||
loop {
|
||||
let mut tx = db.write_tx();
|
||||
let mut user = tx.get(&db.users, &user_id)?;
|
||||
let other_id = info.id;
|
||||
let Some(mut other) = tx.get(&db.users, &other_id) else {
|
||||
println!("WARNING: username without valid user!");
|
||||
return None;
|
||||
};
|
||||
user.friends.current.remove(&other_id);
|
||||
other.friends.current.remove(&user_id);
|
||||
tx.insert(&db.users, &user_id, &user);
|
||||
tx.insert(&db.users, &other_id, &other);
|
||||
if tx.commit() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
ClientMsg::RequestFriends(_) => {
|
||||
let user = check_user().await?.1;
|
||||
reply!(RequestFriendsResp {
|
||||
current: user.friends.current,
|
||||
incoming: user.friends.incoming,
|
||||
outgoing: user.friends.outgoing,
|
||||
});
|
||||
}
|
||||
ClientMsg::AnswerFriendRequest(answer) => {
|
||||
let user_id = check_login().await?;
|
||||
loop {
|
||||
let mut tx = db.write_tx();
|
||||
let mut user = tx.get(&db.users, &user_id)?;
|
||||
let other_id = answer.id;
|
||||
let Some(mut other) = tx.get(&db.users, &other_id) else {
|
||||
println!("WARNING: username without valid user!");
|
||||
return None;
|
||||
};
|
||||
match answer.action {
|
||||
FriendRequestAction::Accept => {
|
||||
user.friends.incoming.remove(&other_id);
|
||||
other.friends.outgoing.remove(&user_id);
|
||||
user.friends.current.insert(other_id);
|
||||
other.friends.current.insert(user_id);
|
||||
}
|
||||
FriendRequestAction::Deny => {
|
||||
user.friends.incoming.remove(&other_id);
|
||||
other.friends.outgoing.remove(&user_id);
|
||||
}
|
||||
}
|
||||
tx.insert(&db.users, &user_id, &user);
|
||||
tx.insert(&db.users, &other_id, &other);
|
||||
if tx.commit() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
ClientMsg::RequestUserInfo(info) => {
|
||||
check_user().await?;
|
||||
let Some(other_user) = db.users.get(&info.id) else {
|
||||
reply!(InvalidUser);
|
||||
};
|
||||
// TODO: check perms... (privacy settings)
|
||||
reply!(UserInfo {
|
||||
username: other_user.username
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RecvHandler<ClientMsgInst> for ClientHandler {
|
||||
async fn connect(&self) -> () {}
|
||||
async fn msg(&self, req: ClientMsgInst) {
|
||||
let replier = self.send.replier(req.id);
|
||||
self.handle_msg(req.msg, &replier).await;
|
||||
}
|
||||
|
||||
async fn disconnect(&self, reason: DisconnectReason) -> () {
|
||||
match reason {
|
||||
DisconnectReason::Closed | DisconnectReason::Timeout => (),
|
||||
DisconnectReason::Other(e) => println!("connection issue: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ClientMsg::SendMsg(msg) => {
|
||||
// let uid = check_login().await?;
|
||||
// let msg = Msg {
|
||||
// author: uid,
|
||||
// content: msg.content,
|
||||
// };
|
||||
// // TODO: it is technically possible to send 2 messages at the exact same time...
|
||||
// // should probably append a number if one already exists at that time,
|
||||
// // but also I can't see this ever happening...?
|
||||
// // should be an easy fix later (write tx)
|
||||
// let timestamp = time::OffsetDateTime::now_utc().unix_timestamp_nanos();
|
||||
// self.db.msgs.insert(×tamp, &msg);
|
||||
// let mut handles = Vec::new();
|
||||
// let msg = LoadMsg {
|
||||
// content: msg.content,
|
||||
// author: uid,
|
||||
// };
|
||||
// for (&id, send) in self.senders.read().await.iter() {
|
||||
// if id == self.id {
|
||||
// continue;
|
||||
// }
|
||||
// let send = send.clone();
|
||||
// let msg = msg.clone();
|
||||
// let fut = async move {
|
||||
// let _ = send
|
||||
// .send(LoadMsg {
|
||||
// content: msg.content,
|
||||
// author: msg.author,
|
||||
// })
|
||||
// .await;
|
||||
// };
|
||||
// handles.push(tokio::spawn(fut));
|
||||
// }
|
||||
// for h in handles {
|
||||
// h.await.unwrap();
|
||||
// }
|
||||
// None
|
||||
// }
|
||||
// ClientMsg::RequestMsgs => {
|
||||
// check_login().await?;
|
||||
// let msgs = self
|
||||
// .db
|
||||
// .msgs
|
||||
// .values()
|
||||
// .map(|msg| LoadMsg {
|
||||
// content: msg.content,
|
||||
// author: msg.author,
|
||||
// })
|
||||
// .collect();
|
||||
// replier.send(ServerMsg::LoadMsgs(msgs)).await;
|
||||
// }
|
||||
@@ -1,21 +1,18 @@
|
||||
// mod data;
|
||||
mod db;
|
||||
mod handle;
|
||||
mod net;
|
||||
|
||||
use crate::db::{Db, Msg, User, open_db};
|
||||
use crate::{
|
||||
db::{Db, ServerPerms},
|
||||
handle::{ClientHandler, ClientState},
|
||||
};
|
||||
use clap::Parser;
|
||||
use net::{ClientSender, ConAccepter, listen};
|
||||
use openworm::{
|
||||
net::{
|
||||
ClientMsg, DisconnectReason, NetServerMsg, RecvHandler, ServerError, ServerMsg,
|
||||
install_crypto_provider,
|
||||
},
|
||||
net::{AccountToken, ClientMsgInst, RecvHandler, install_crypto_provider},
|
||||
rsc::DataDir,
|
||||
};
|
||||
use scrypt::{
|
||||
Scrypt,
|
||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
|
||||
};
|
||||
use rand::distr::{Alphanumeric, SampleString};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
@@ -41,24 +38,30 @@ fn main() {
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn run_server(port: u16) {
|
||||
let dir = DataDir::default();
|
||||
let path = dir.get();
|
||||
let db: Db = open_db(path.join("server.db"));
|
||||
let dir = DataDir::new(Some("server"));
|
||||
let db = Db::open(dir.path.join("db"));
|
||||
let handler = ServerListener {
|
||||
senders: Default::default(),
|
||||
count: 0.into(),
|
||||
db: db.clone(),
|
||||
};
|
||||
let (endpoint, handle) = listen(port, path, handler);
|
||||
if db.users.is_empty() {
|
||||
let token = account_token(&db, ServerPerms::ALL);
|
||||
println!("no users found, token for admin: {token}");
|
||||
}
|
||||
let (endpoint, handle) = listen(port, &dir.path, handler);
|
||||
let _ = ctrl_c().await;
|
||||
println!("stopping server");
|
||||
println!("closing connections...");
|
||||
endpoint.close(0u32.into(), &[]);
|
||||
let _ = handle.await;
|
||||
endpoint.wait_idle().await;
|
||||
println!("saving...");
|
||||
db.flush_async().await.unwrap();
|
||||
println!("saved");
|
||||
}
|
||||
|
||||
pub fn account_token(db: &Db, perms: ServerPerms) -> AccountToken {
|
||||
let token = Alphanumeric.sample_string(&mut rand::rng(), 16);
|
||||
db.account_tokens.insert(&token, &perms);
|
||||
token
|
||||
}
|
||||
|
||||
type ClientId = u64;
|
||||
@@ -69,14 +72,8 @@ struct ServerListener {
|
||||
count: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ClientState {
|
||||
Login,
|
||||
Authed(u64),
|
||||
}
|
||||
|
||||
impl ConAccepter for ServerListener {
|
||||
async fn accept(&self, send: ClientSender) -> impl RecvHandler<ClientMsg> {
|
||||
async fn accept(&self, send: ClientSender) -> impl RecvHandler<ClientMsgInst> {
|
||||
let id = self.count.fetch_add(1, Ordering::Release);
|
||||
self.senders.write().await.insert(id, send.clone());
|
||||
ClientHandler {
|
||||
@@ -88,133 +85,3 @@ impl ConAccepter for ServerListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientHandler {
|
||||
db: Db,
|
||||
send: ClientSender,
|
||||
senders: Arc<RwLock<HashMap<ClientId, ClientSender>>>,
|
||||
id: ClientId,
|
||||
state: Arc<RwLock<ClientState>>,
|
||||
}
|
||||
|
||||
impl RecvHandler<ClientMsg> for ClientHandler {
|
||||
async fn connect(&self) -> () {
|
||||
println!("connected: {:?}", self.send.remote().ip());
|
||||
}
|
||||
async fn msg(&self, msg: ClientMsg) {
|
||||
match msg {
|
||||
ClientMsg::SendMsg(msg) => {
|
||||
let ClientState::Authed(uid) = &*self.state.read().await else {
|
||||
let _ = self.send.send(ServerError::NotLoggedIn).await;
|
||||
return;
|
||||
};
|
||||
let msg = Msg {
|
||||
user: *uid,
|
||||
content: msg.content,
|
||||
};
|
||||
let id = self.db.generate_id().unwrap();
|
||||
self.db.msgs.insert(&id, &msg);
|
||||
let mut handles = Vec::new();
|
||||
let user: User = self.db.users.get(uid).unwrap();
|
||||
let msg = NetServerMsg {
|
||||
content: msg.content,
|
||||
user: user.username,
|
||||
};
|
||||
for (&id, send) in self.senders.read().await.iter() {
|
||||
if id == self.id {
|
||||
continue;
|
||||
}
|
||||
let send = send.clone();
|
||||
let msg = msg.clone();
|
||||
let fut = async move {
|
||||
let _ = send.send(msg).await;
|
||||
};
|
||||
handles.push(tokio::spawn(fut));
|
||||
}
|
||||
for h in handles {
|
||||
h.await.unwrap();
|
||||
}
|
||||
}
|
||||
ClientMsg::RequestMsgs => {
|
||||
let ClientState::Authed(_uid) = &*self.state.read().await else {
|
||||
let _ = self.send.send(ServerError::NotLoggedIn).await;
|
||||
return;
|
||||
};
|
||||
let msgs = self
|
||||
.db
|
||||
.msgs
|
||||
.iter_all()
|
||||
.map(|msg| {
|
||||
let user = self
|
||||
.db
|
||||
.users
|
||||
.get(&msg.user)
|
||||
.map(|user| user.username.to_string())
|
||||
.unwrap_or("deleted user".to_string());
|
||||
NetServerMsg {
|
||||
content: msg.content,
|
||||
user,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let _ = self.send.send(ServerMsg::LoadMsgs(msgs)).await;
|
||||
}
|
||||
ClientMsg::CreateAccount { username, password } => {
|
||||
if !self.db.usernames.init_unique(&username) {
|
||||
let _ = self.send.send(ServerError::UsernameTaken).await;
|
||||
return;
|
||||
}
|
||||
let id = self.db.generate_id().unwrap();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let params = scrypt::Params::new(11, 8, 1, 32).unwrap();
|
||||
let hash = Scrypt
|
||||
.hash_password_customized(password.as_bytes(), None, None, params, &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
self.db.users.insert(
|
||||
&id,
|
||||
&User {
|
||||
username: username.clone(),
|
||||
password_hash: hash,
|
||||
},
|
||||
);
|
||||
println!("account created: \"{username}\"");
|
||||
self.db.usernames.insert(&username, &id);
|
||||
*self.state.write().await = ClientState::Authed(id);
|
||||
let _ = self.send.send(ServerMsg::Login { username }).await;
|
||||
}
|
||||
ClientMsg::Login { username, password } => {
|
||||
let Some(id) = self.db.usernames.get(&username) else {
|
||||
let _ = self.send.send(ServerError::UnknownUsername).await;
|
||||
return;
|
||||
};
|
||||
let Some(user) = self.db.users.get(&id) else {
|
||||
panic!("invalid state! (should be a user)");
|
||||
};
|
||||
let hash = PasswordHash::new(&user.password_hash).unwrap();
|
||||
if Scrypt.verify_password(password.as_bytes(), &hash).is_err() {
|
||||
println!("invalid password: \"{username}\"");
|
||||
let _ = self.send.send(ServerError::InvalidPassword).await;
|
||||
return;
|
||||
}
|
||||
println!("login: \"{username}\"");
|
||||
*self.state.write().await = ClientState::Authed(id);
|
||||
let _ = self.send.send(ServerMsg::Login { username }).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn disconnect(&self, reason: DisconnectReason) -> () {
|
||||
println!("disconnected: {:?}", self.send.remote().ip());
|
||||
match reason {
|
||||
DisconnectReason::Closed | DisconnectReason::Timeout => (),
|
||||
DisconnectReason::Other(e) => println!("connection issue: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientState {
|
||||
pub fn is_authed(&self) -> bool {
|
||||
matches!(self, Self::Authed(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use openworm::net::{
|
||||
ClientMsg, RecvHandler, SERVER_NAME, SendResult, ServerMsg, recv_uni, send_uni,
|
||||
ClientMsgInst, RecvHandler, RequestId, SERVER_NAME, SendResult, ServerMsg, ServerMsgInst,
|
||||
recv_uni, send_uni,
|
||||
};
|
||||
use quinn::{
|
||||
Connection, Endpoint, ServerConfig,
|
||||
@@ -36,19 +37,12 @@ pub fn init_endpoint(port: u16, data_path: &Path) -> Endpoint {
|
||||
panic!("failed to read certificate: {}", e);
|
||||
}
|
||||
};
|
||||
// let server_crypto = quinn::rustls::ServerConfig::builder()
|
||||
// .with_no_client_auth()
|
||||
// .with_single_cert(vec![cert], key)
|
||||
// .unwrap();
|
||||
//
|
||||
// let server_config = quinn::ServerConfig::with_crypto(Arc::new(
|
||||
// QuicServerConfig::try_from(server_crypto).unwrap(),
|
||||
// ));
|
||||
|
||||
print!("cert hex: ");
|
||||
for x in cert.iter() {
|
||||
print!("{:02x}", x);
|
||||
}
|
||||
println!();
|
||||
let server_config = ServerConfig::with_single_cert(vec![cert], key).unwrap();
|
||||
// let transport_config = Arc::get_mut(&mut server_config.transport).unwrap();
|
||||
// transport_config.max_concurrent_uni_streams(0_u8.into());
|
||||
|
||||
let server_socket: SocketAddr = SocketAddr::V6(SocketAddrV6::new(SERVER_HOST, port, 0, 0));
|
||||
quinn::Endpoint::server(server_config, server_socket).unwrap()
|
||||
}
|
||||
@@ -62,17 +56,43 @@ impl ClientSender {
|
||||
pub fn remote(&self) -> SocketAddr {
|
||||
self.conn.remote_address()
|
||||
}
|
||||
|
||||
pub fn replier(&self, id: RequestId) -> ClientReplier {
|
||||
ClientReplier {
|
||||
conn: self.conn.clone(),
|
||||
req_id: id,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send(&self, msg: impl Into<ServerMsg>) -> SendResult {
|
||||
let msg = msg.into();
|
||||
let msg = ServerMsgInst {
|
||||
id: None,
|
||||
msg: msg.into(),
|
||||
};
|
||||
send_uni(&self.conn, msg).await
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientReplier {
|
||||
conn: Connection,
|
||||
req_id: RequestId,
|
||||
}
|
||||
|
||||
impl ClientReplier {
|
||||
pub async fn send(&self, msg: impl Into<ServerMsg>) {
|
||||
let msg = ServerMsgInst {
|
||||
id: Some(self.req_id),
|
||||
msg: msg.into(),
|
||||
};
|
||||
let _ = send_uni(&self.conn, msg).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ConAccepter: Send + Sync + 'static {
|
||||
fn accept(
|
||||
&self,
|
||||
send: ClientSender,
|
||||
) -> impl Future<Output = impl RecvHandler<ClientMsg>> + Send;
|
||||
) -> impl Future<Output = impl RecvHandler<ClientMsgInst>> + Send;
|
||||
}
|
||||
|
||||
pub fn listen(
|
||||
|
||||
122
src/net/data.rs
Normal file
122
src/net/data.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
use iris::core::util::HashSet;
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct RequestUserInfo {
|
||||
pub id: UserId,
|
||||
}
|
||||
|
||||
pub type RequestUserInfoResp = UserInfo;
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct UserInfo {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
pub type UserId = u64;
|
||||
pub type AccountToken = String;
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct CreateAccount {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub enum CreateAccountResp {
|
||||
Ok { id: UserId },
|
||||
UsernameExists,
|
||||
InvalidToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct Login {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub enum LoginResp {
|
||||
Ok { id: UserId },
|
||||
UnknownUsername,
|
||||
InvalidPassword,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct RequestUsers;
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct RequestUsersResp {
|
||||
pub users: Vec<ServerUser>,
|
||||
}
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct ServerUser {
|
||||
pub id: UserId,
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct RequestFriends;
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct RequestFriendsResp {
|
||||
pub current: HashSet<UserId>,
|
||||
pub incoming: HashSet<UserId>,
|
||||
pub outgoing: HashSet<UserId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct AddFriend {
|
||||
pub username: String,
|
||||
}
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub enum AddFriendResp {
|
||||
Ok,
|
||||
UnknownUser,
|
||||
CannotAddSelf,
|
||||
AlreadySent,
|
||||
AlreadyFriends,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct RemoveFriend {
|
||||
pub id: UserId,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct AnswerFriendRequest {
|
||||
pub id: UserId,
|
||||
pub action: FriendRequestAction,
|
||||
}
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub enum FriendRequestAction {
|
||||
Accept,
|
||||
Deny,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct GenerateToken {
|
||||
pub perms: ServerPerms,
|
||||
}
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct GenerateTokenResp {
|
||||
pub token: AccountToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct ServerPerms(u32);
|
||||
impl ServerPerms {
|
||||
pub const NONE: Self = Self(0);
|
||||
pub const ACCOUNT_TOKENS: Self = Self(1 << 0);
|
||||
pub const ALL: Self = Self(u32::MAX);
|
||||
}
|
||||
impl ServerPerms {
|
||||
pub fn contains(&self, other: Self) -> bool {
|
||||
(self.0 & other.0) == other.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct NotLoggedIn;
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct NoPermission;
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct InvalidUser;
|
||||
@@ -1,62 +1,16 @@
|
||||
use bincode::config::Configuration;
|
||||
|
||||
mod data;
|
||||
mod msg;
|
||||
mod no_cert;
|
||||
mod request;
|
||||
mod transfer;
|
||||
|
||||
pub use data::*;
|
||||
pub use msg::*;
|
||||
pub use no_cert::*;
|
||||
pub use request::*;
|
||||
pub use transfer::*;
|
||||
|
||||
pub const SERVER_NAME: &str = "openworm";
|
||||
pub const BINCODE_CONFIG: Configuration = bincode::config::standard();
|
||||
|
||||
#[derive(Debug, bincode::Encode, bincode::Decode)]
|
||||
pub enum ClientMsg {
|
||||
SendMsg(NetClientMsg),
|
||||
RequestMsgs,
|
||||
CreateAccount { username: String, password: String },
|
||||
Login { username: String, password: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, bincode::Encode, bincode::Decode)]
|
||||
pub enum ServerMsg {
|
||||
SendMsg(NetServerMsg),
|
||||
LoadMsgs(Vec<NetServerMsg>),
|
||||
Login { username: String },
|
||||
Error(ServerError),
|
||||
}
|
||||
|
||||
#[derive(Debug, bincode::Encode, bincode::Decode)]
|
||||
pub enum ServerError {
|
||||
NotLoggedIn,
|
||||
UnknownUsername,
|
||||
InvalidPassword,
|
||||
UsernameTaken,
|
||||
}
|
||||
|
||||
impl From<ServerError> for ServerMsg {
|
||||
fn from(value: ServerError) -> Self {
|
||||
Self::Error(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub type ServerResp<T> = Result<T, String>;
|
||||
|
||||
#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
|
||||
pub struct NetClientMsg {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, bincode::Encode, bincode::Decode)]
|
||||
pub struct NetServerMsg {
|
||||
pub content: String,
|
||||
pub user: String,
|
||||
}
|
||||
|
||||
impl From<NetServerMsg> for ServerMsg {
|
||||
fn from(value: NetServerMsg) -> Self {
|
||||
Self::SendMsg(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn install_crypto_provider() {
|
||||
quinn::rustls::crypto::ring::default_provider()
|
||||
|
||||
55
src/net/msg.rs
Normal file
55
src/net/msg.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use super::{RequestMsg, data::*};
|
||||
|
||||
msg_type!(ClientMsg: {
|
||||
0: CreateAccount => CreateAccountResp,
|
||||
1: Login => LoginResp,
|
||||
2: RequestUsers => RequestUsersResp,
|
||||
3: RequestFriends => RequestFriendsResp,
|
||||
4: AddFriend => AddFriendResp,
|
||||
5: RemoveFriend,
|
||||
6: AnswerFriendRequest,
|
||||
7: GenerateToken => GenerateTokenResp,
|
||||
8: RequestUserInfo => RequestUserInfoResp,
|
||||
});
|
||||
msg_type!(ServerMsg: {
|
||||
0: NotLoggedIn,
|
||||
1: NoPermission,
|
||||
2: InvalidUser,
|
||||
3: CreateAccountResp,
|
||||
4: LoginResp,
|
||||
5: RequestUsersResp,
|
||||
6: RequestFriendsResp,
|
||||
7: AddFriendResp,
|
||||
8: GenerateTokenResp,
|
||||
9: RequestUserInfoResp,
|
||||
});
|
||||
|
||||
macro_rules! msg_type {
|
||||
($msg:ident: {$($num:literal: $name:ident $(=> $resp:ident)?,)*}) => {
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub enum $msg {$(
|
||||
$name($name) = $num,
|
||||
)*}
|
||||
$(
|
||||
impl From<$name> for $msg {
|
||||
fn from(value: $name) -> Self {
|
||||
Self::$name(value)
|
||||
}
|
||||
}
|
||||
$(
|
||||
impl RequestMsg for $name {
|
||||
type Result = $resp;
|
||||
fn result(msg: ServerMsg) -> Option<Self::Result> {
|
||||
if let ServerMsg::$resp(res) = msg {
|
||||
Some(res)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
)?
|
||||
)*
|
||||
};
|
||||
}
|
||||
use msg_type;
|
||||
36
src/net/request.rs
Normal file
36
src/net/request.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use super::*;
|
||||
use std::num::NonZeroU32;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct RequestId(NonZeroU32);
|
||||
|
||||
impl RequestId {
|
||||
pub const fn first() -> Self {
|
||||
Self(NonZeroU32::MAX)
|
||||
}
|
||||
|
||||
pub const fn next(&mut self) -> Self {
|
||||
self.0 = match self.0.checked_add(1) {
|
||||
Some(v) => v,
|
||||
None => NonZeroU32::MIN,
|
||||
};
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct ClientMsgInst {
|
||||
pub id: RequestId,
|
||||
pub msg: ClientMsg,
|
||||
}
|
||||
|
||||
#[derive(Debug, bitcode::Encode, bitcode::Decode)]
|
||||
pub struct ServerMsgInst {
|
||||
pub id: Option<RequestId>,
|
||||
pub msg: ServerMsg,
|
||||
}
|
||||
|
||||
pub trait RequestMsg: Into<ClientMsg> {
|
||||
type Result: Send + Sync + 'static;
|
||||
fn result(msg: ServerMsg) -> Option<Self::Result>;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::net::BINCODE_CONFIG;
|
||||
use quinn::Connection;
|
||||
use tokio::io::{AsyncReadExt as _, AsyncWriteExt};
|
||||
use tracing::Instrument as _;
|
||||
@@ -17,8 +16,8 @@ pub trait RecvHandler<M>: Send + Sync + 'static {
|
||||
}
|
||||
|
||||
pub type SendResult = Result<(), ()>;
|
||||
pub async fn send_uni<M: bincode::Encode>(conn: &Connection, msg: M) -> SendResult {
|
||||
let bytes = bincode::encode_to_vec(msg, BINCODE_CONFIG).unwrap();
|
||||
pub async fn send_uni<M: bitcode::Encode>(conn: &Connection, msg: M) -> SendResult {
|
||||
let bytes = bitcode::encode(&msg);
|
||||
let mut send = conn.open_uni().await.map_err(|_| ())?;
|
||||
|
||||
send.write_u64(bytes.len() as u64).await.map_err(|_| ())?;
|
||||
@@ -34,7 +33,7 @@ pub enum DisconnectReason {
|
||||
Other(String),
|
||||
}
|
||||
|
||||
pub async fn recv_uni<M: bincode::Decode<()>>(
|
||||
pub async fn recv_uni<M: bitcode::DecodeOwned>(
|
||||
conn: Connection,
|
||||
handler: Arc<impl RecvHandler<M>>,
|
||||
) -> DisconnectReason {
|
||||
@@ -58,7 +57,7 @@ pub async fn recv_uni<M: bincode::Decode<()>>(
|
||||
async move {
|
||||
let len = recv.read_u64().await.unwrap();
|
||||
let bytes = recv.read_to_end(len as usize).await.unwrap();
|
||||
let (msg, _) = bincode::decode_from_slice::<M, _>(&bytes, BINCODE_CONFIG).unwrap();
|
||||
let msg = bitcode::decode::<M>(&bytes).unwrap();
|
||||
handler.msg(msg).await;
|
||||
}
|
||||
.instrument(tracing::info_span!("request")),
|
||||
|
||||
120
src/rsc.rs
120
src/rsc.rs
@@ -1,45 +1,103 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::Path,
|
||||
io::Write,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use directories_next::ProjectDirs;
|
||||
|
||||
use crate::net::BINCODE_CONFIG;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DataDir {
|
||||
dirs: ProjectDirs,
|
||||
}
|
||||
|
||||
impl Default for DataDir {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dirs: ProjectDirs::from("", "", "openworm").unwrap(),
|
||||
}
|
||||
}
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl DataDir {
|
||||
pub fn get(&self) -> &Path {
|
||||
self.dirs.data_local_dir()
|
||||
pub fn new(dir: Option<&str>) -> Self {
|
||||
let dirs = ProjectDirs::from("", "", "openworm").unwrap();
|
||||
let mut path = dirs.data_local_dir().to_path_buf();
|
||||
if let Some(dir) = dir {
|
||||
path = path.join(dir);
|
||||
}
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
pub trait DataRsc: serde::Serialize + Default {
|
||||
fn name() -> &'static str;
|
||||
fn path() -> String {
|
||||
Self::name().to_string() + ".ron"
|
||||
}
|
||||
fn parse_version(text: &str, version: u32) -> Result<Self, String>;
|
||||
fn version() -> u32;
|
||||
}
|
||||
|
||||
pub struct DataGuard<T: DataRsc> {
|
||||
path: PathBuf,
|
||||
val: T,
|
||||
}
|
||||
|
||||
impl DataDir {
|
||||
pub fn load<T: DataRsc>(&self) -> DataGuard<T> {
|
||||
let path = self.path.join(T::path());
|
||||
let invalid = |info: &str| {
|
||||
println!("warning: invalid config @ {path:?}: {info}");
|
||||
DataGuard {
|
||||
path: path.clone(),
|
||||
val: T::default(),
|
||||
}
|
||||
};
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(text) => {
|
||||
let mut lines = text.lines();
|
||||
let Some(first) = lines.next() else {
|
||||
return invalid("empty file");
|
||||
};
|
||||
let version_str: String = first
|
||||
.chars()
|
||||
.skip_while(|c| *c != 'v')
|
||||
.skip(1)
|
||||
.take_while(|c| !c.is_whitespace())
|
||||
.collect();
|
||||
let Ok(version): Result<u32, _> = version_str.parse() else {
|
||||
return invalid("invalid version");
|
||||
};
|
||||
let text: String = lines.collect();
|
||||
match T::parse_version(&text, version) {
|
||||
Ok(val) => DataGuard { path, val },
|
||||
Err(e) => invalid(&e),
|
||||
}
|
||||
}
|
||||
Err(_) => DataGuard {
|
||||
path,
|
||||
val: T::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DataRsc> std::ops::Deref for DataGuard<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.val
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DataRsc> std::ops::DerefMut for DataGuard<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.val
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DataRsc> Drop for DataGuard<T> {
|
||||
fn drop(&mut self) {
|
||||
let dir = self.path.parent().unwrap();
|
||||
fs::create_dir_all(dir).unwrap();
|
||||
let mut file = File::create(dir.join(T::path())).unwrap();
|
||||
let ron = ron::ser::to_string_pretty(&self.val, ron::ser::PrettyConfig::new()).unwrap();
|
||||
let data = format!("// v{}\n{}\n", T::version(), ron);
|
||||
if let Err(e) = file.write_all(data.as_bytes()) {
|
||||
println!("Failed to write config @ {:?}: {e}", self.path);
|
||||
}
|
||||
|
||||
pub fn load<T: bincode::Decode<()> + Default>(&self, path: &str) -> T {
|
||||
let path = self.get().join(path);
|
||||
match fs::read(path) {
|
||||
Ok(bytes) => match bincode::decode_from_slice(&bytes, BINCODE_CONFIG) {
|
||||
Ok((data, _)) => data,
|
||||
Err(_) => todo!(),
|
||||
},
|
||||
Err(_) => T::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save<T: bincode::Encode>(&self, path: &str, data: &T) {
|
||||
let dir = self.get();
|
||||
fs::create_dir_all(dir).unwrap();
|
||||
let mut file = File::create(dir.join(path)).unwrap();
|
||||
bincode::encode_into_std_write(data, &mut file, BINCODE_CONFIG).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user