Compare commits

21 Commits
main ... work

Author SHA1 Message Date
54eb7d4261 work 2026-02-26 19:18:33 -05:00
17eaf3e324 friending work 2026-02-21 00:19:57 -05:00
3c242a7b77 oop 2026-02-18 22:10:04 -05:00
6fad5b1852 server side preparation 2026-02-18 21:52:22 -05:00
1c6b6de8f6 oop iris 2026-02-18 16:50:11 -05:00
97fdbbf968 stuff 2026-02-18 16:47:35 -05:00
f9d8fccf40 iris update 2026-02-17 18:15:46 -05:00
227a622adc ui stuff 2026-02-17 17:39:42 -05:00
61e9c2ac5c ACCOUNT CREATION AND LOGIN 2026-02-16 23:56:07 -05:00
79da5e1146 cert work 2026-02-15 22:21:09 -05:00
d6c98bcf10 work 2026-02-15 21:21:56 -05:00
8be13e14bc work (new network + db initial working state) 2026-02-15 16:59:47 -05:00
a8b55f669f work 2026-01-26 17:09:40 -05:00
53ed4775ae work 2026-01-26 13:53:51 -05:00
e1eff49be9 BRUHH 2026-01-22 23:35:52 -05:00
b56b71b70c bruh 2026-01-22 23:34:04 -05:00
bcbd6cb6c8 work 2026-01-22 23:33:14 -05:00
ace356381a work 2026-01-20 21:13:42 -05:00
6821e546db iris update 2026-01-19 21:43:13 -05:00
e67c55d67d update iris & get button working again 2026-01-19 21:26:59 -05:00
040b70e1c5 work 2026-01-12 18:42:34 -05:00
37 changed files with 5295 additions and 1422 deletions

3528
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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

Submodule iris updated: 7f4846a2d3...1102dc7338

View File

@@ -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,
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View 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>()
}
}

View 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
}
}

View File

@@ -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();
}
}

View File

@@ -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")
}
}

View File

@@ -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(())
Ok(NetHandle { send, event_sender })
}
}
struct ServerRecv {
handle: AppHandle,
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;
}
}
impl RecvHandler<ServerMsg> for ServerRecv {
async fn msg(&self, msg: ServerMsg) {
self.handle.send(ClientEvent::ServerMsg(msg));
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;
}
}
}

View File

@@ -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
View 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,
}
}
}

View File

@@ -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();
}
}
}
}

View 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)
}

View File

@@ -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);

View File

@@ -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()
}

View 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
View 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()
}

View File

@@ -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)
}

View File

@@ -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));
}
}

View File

@@ -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::*;

View 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
View 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
}
});
}
}

View File

@@ -1,3 +0,0 @@
pub struct DBMsg {
}

View File

@@ -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
View 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
View 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
View 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
View 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(&timestamp, &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;
// }

View File

@@ -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(_))
}
}

View File

@@ -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
View 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;

View File

@@ -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
View 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
View 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>;
}

View File

@@ -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")),

View File

@@ -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 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!(),
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(),
},
Err(_) => T::default(),
}
}
}
pub fn save<T: bincode::Encode>(&self, path: &str, data: &T) {
let dir = self.get();
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(path)).unwrap();
bincode::encode_into_std_write(data, &mut file, BINCODE_CONFIG).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);
}
}
}