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]
|
[package]
|
||||||
name = "openworm"
|
name = "openworm"
|
||||||
@@ -17,14 +17,20 @@ tracing = "0.1.41"
|
|||||||
iris = { path = "./iris" }
|
iris = { path = "./iris" }
|
||||||
wgpu = "27.0.1"
|
wgpu = "27.0.1"
|
||||||
winit = "0.30.12"
|
winit = "0.30.12"
|
||||||
bincode = "2.0.1"
|
|
||||||
zstd = "0.13.3"
|
zstd = "0.13.3"
|
||||||
ron = "0.12.0"
|
ron = "0.12.0"
|
||||||
sled = "0.34.7"
|
|
||||||
clap = { version = "4.5.53", features = ["derive"] }
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
scrypt = "0.11.0"
|
scrypt = "0.11.0"
|
||||||
ed25519-dalek = { version = "3.0.0-pre.2", features = ["rand_core"] }
|
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]]
|
[[bin]]
|
||||||
name = "openworm-client"
|
name = "openworm-client"
|
||||||
@@ -34,8 +40,8 @@ path = "src/bin/client/main.rs"
|
|||||||
name = "openworm-server"
|
name = "openworm-server"
|
||||||
path = "src/bin/server/main.rs"
|
path = "src/bin/server/main.rs"
|
||||||
|
|
||||||
[profile.dev]
|
# [profile.dev]
|
||||||
codegen-backend = "cranelift"
|
# codegen-backend = "cranelift"
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
# [profile.dev.package."*"]
|
||||||
opt-level = 1
|
# 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 crate::{Client, Rsc};
|
||||||
use iris::prelude::*;
|
use iris::core::UiRenderState;
|
||||||
|
|
||||||
pub fn debug(_client: &mut Client, ui: &mut Ui, _state: &UiState) {
|
impl Client {
|
||||||
ui.debug_layers();
|
pub fn debug(&mut self, _rsc: &mut Rsc, render: &mut UiRenderState) {
|
||||||
// let mut file = std::fs::OpenOptions::new()
|
render.debug_layers();
|
||||||
// .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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
|
#![feature(async_fn_traits)]
|
||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
use crate::{
|
use crate::data::ClientData;
|
||||||
net::{NetHandle, NetSender},
|
|
||||||
rsc::{CLIENT_DATA, ClientData},
|
|
||||||
state::{ClientState, LoggedIn, Login},
|
|
||||||
ui::*,
|
|
||||||
};
|
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use openworm::{
|
use openworm::net::{ServerMsg, install_crypto_provider};
|
||||||
net::{ClientMsg, ServerMsg, install_crypto_provider},
|
|
||||||
rsc::DataDir,
|
|
||||||
};
|
|
||||||
use winit::{
|
use winit::{
|
||||||
event::{ElementState, MouseButton, WindowEvent},
|
event::{ElementState, MouseButton, WindowEvent},
|
||||||
window::WindowAttributes,
|
window::WindowAttributes,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod account;
|
mod data;
|
||||||
mod debug;
|
mod debug;
|
||||||
mod net;
|
mod net;
|
||||||
mod rsc;
|
mod session;
|
||||||
mod state;
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -28,17 +20,19 @@ fn main() {
|
|||||||
DefaultApp::<Client>::run();
|
DefaultApp::<Client>::run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(DefaultUiState)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
dir: DataDir,
|
ui_state: DefaultUiState,
|
||||||
data: ClientData,
|
data: ClientData,
|
||||||
state: ClientState,
|
main_ui: WeakWidget<WidgetPtr>,
|
||||||
main_ui: WidgetRef<WidgetPtr>,
|
notif: WeakWidget<WidgetPtr>,
|
||||||
notif: WidgetRef<WidgetPtr>,
|
|
||||||
proxy: Proxy<ClientEvent>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type Rsc = DefaultRsc<Client>;
|
||||||
|
pub type ClientSender = EventSender<Rsc>;
|
||||||
|
|
||||||
pub enum ClientEvent {
|
pub enum ClientEvent {
|
||||||
Connect { send: NetSender },
|
CacheUpdate,
|
||||||
ServerMsg(ServerMsg),
|
ServerMsg(ServerMsg),
|
||||||
Err(String),
|
Err(String),
|
||||||
}
|
}
|
||||||
@@ -46,14 +40,10 @@ pub enum ClientEvent {
|
|||||||
impl DefaultAppState for Client {
|
impl DefaultAppState for Client {
|
||||||
type Event = ClientEvent;
|
type Event = ClientEvent;
|
||||||
|
|
||||||
fn window_attrs() -> WindowAttributes {
|
fn new(mut ui_state: DefaultUiState, rsc: &mut DefaultRsc<Self>) -> Self {
|
||||||
WindowAttributes::default().with_title("OPENWORM")
|
let notif = WidgetPtr::default().add(rsc);
|
||||||
}
|
// let popup = WidgetPtr::default().add(rsc);
|
||||||
|
let main_ui = WidgetPtr::default().add(rsc);
|
||||||
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);
|
|
||||||
let bg = (
|
let bg = (
|
||||||
image(include_bytes!("./assets/fuit.jpg")),
|
image(include_bytes!("./assets/fuit.jpg")),
|
||||||
rect(Color::BLACK.alpha((0.8 * 255.0) as u8)),
|
rect(Color::BLACK.alpha((0.8 * 255.0) as u8)),
|
||||||
@@ -62,96 +52,55 @@ impl DefaultAppState for Client {
|
|||||||
|
|
||||||
(
|
(
|
||||||
bg,
|
bg,
|
||||||
main_ui.clone(),
|
main_ui,
|
||||||
notif.clone().pad(Padding::top(10)).align(Align::TOP_CENTER),
|
notif.pad(Padding::top(10)).align(Align::TOP_CENTER),
|
||||||
)
|
)
|
||||||
.stack()
|
.stack()
|
||||||
.set_root(ui);
|
.set_root(rsc, &mut ui_state);
|
||||||
|
|
||||||
let mut s = Self {
|
let data = ClientData::load();
|
||||||
data: dir.load(CLIENT_DATA),
|
ui::start(rsc, &data).set_ptr(main_ui, rsc);
|
||||||
state: Default::default(),
|
|
||||||
dir,
|
Self {
|
||||||
main_ui: main_ui.clone(),
|
ui_state,
|
||||||
|
data,
|
||||||
|
main_ui,
|
||||||
notif,
|
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 {
|
match event {
|
||||||
ClientEvent::Connect { send } => {
|
ClientEvent::ServerMsg(_) => {}
|
||||||
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::Err(msg) => {
|
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) {
|
fn window_event(
|
||||||
self.state.exit();
|
&mut self,
|
||||||
self.dir.save(CLIENT_DATA, &self.data);
|
event: WindowEvent,
|
||||||
}
|
rsc: &mut DefaultRsc<Self>,
|
||||||
|
render: &mut UiRenderState,
|
||||||
fn window_event(&mut self, event: WindowEvent, ui: &mut Ui, state: &UiState) {
|
) {
|
||||||
if let WindowEvent::MouseInput {
|
if let WindowEvent::MouseInput {
|
||||||
state: ElementState::Pressed,
|
state: ElementState::Pressed,
|
||||||
button: MouseButton::Middle,
|
button: MouseButton::Middle,
|
||||||
..
|
..
|
||||||
} = event
|
} = 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::{
|
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::{
|
use quinn::{
|
||||||
ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig,
|
ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig,
|
||||||
crypto::rustls::QuicClientConfig,
|
crypto::rustls::QuicClientConfig, rustls::pki_types::CertificateDer,
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
net::{Ipv6Addr, SocketAddr, SocketAddrV6, ToSocketAddrs},
|
net::{Ipv6Addr, SocketAddr, SocketAddrV6, ToSocketAddrs},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
thread::JoinHandle,
|
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::{mpsc::UnboundedSender, oneshot};
|
||||||
use winit::{event_loop::EventLoopProxy, window::Window};
|
|
||||||
|
|
||||||
pub const CLIENT_SOCKET: SocketAddr =
|
pub const CLIENT_SOCKET: SocketAddr =
|
||||||
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0));
|
SocketAddr::V6(SocketAddrV6::new(Ipv6Addr::UNSPECIFIED, 0, 0, 0));
|
||||||
|
|
||||||
pub struct ConnectInfo {
|
pub struct ConnectInfo {
|
||||||
pub ip: String,
|
pub url: String,
|
||||||
}
|
pub cert: Vec<u8>,
|
||||||
|
|
||||||
pub struct NetHandle {
|
|
||||||
pub send: NetSender,
|
|
||||||
pub thread: JoinHandle<()>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppHandle {
|
pub struct NetHandle {
|
||||||
pub proxy: EventLoopProxy<ClientEvent>,
|
send: UnboundedSender<NetCtrlMsg>,
|
||||||
pub window: Arc<Window>,
|
event_sender: ClientSender,
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NetResult<T> = Result<T, String>;
|
type NetResult<T> = Result<T, String>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct NetSender {
|
|
||||||
send: UnboundedSender<NetCtrlMsg>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum NetCtrlMsg {
|
pub enum NetCtrlMsg {
|
||||||
Send(ClientMsg),
|
Send(ClientMsg),
|
||||||
|
Request(ClientMsg, oneshot::Sender<ServerMsg>),
|
||||||
|
RequestSync(ClientMsg, Box<dyn FnOnce(ServerMsg) + Send + Sync>),
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ClientMsg> for NetCtrlMsg {
|
type Resp<R: RequestMsg> = Result<R::Result, ()>;
|
||||||
fn from(value: ClientMsg) -> Self {
|
|
||||||
Self::Send(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetSender {
|
|
||||||
pub fn send(&self, msg: impl Into<NetCtrlMsg>) {
|
|
||||||
let _ = self.send.send(msg.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NetHandle {
|
impl NetHandle {
|
||||||
pub fn send(&self, msg: impl Into<NetCtrlMsg>) {
|
fn send_(&self, msg: NetCtrlMsg) {
|
||||||
self.send.send(msg.into());
|
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) {
|
pub fn exit(self) {
|
||||||
self.send(NetCtrlMsg::Exit);
|
self.send_(NetCtrlMsg::Exit);
|
||||||
let _ = self.thread.join();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// async fn connection_cert(addr: SocketAddr) -> NetResult<Connection> {
|
pub struct SyncRecv<R: RequestMsg> {
|
||||||
// let dirs = directories_next::ProjectDirs::from("", "", "openworm").unwrap();
|
recv: oneshot::Receiver<Resp<R>>,
|
||||||
// let mut roots = quinn::rustls::RootCertStore::empty();
|
}
|
||||||
// match fs::read(dirs.data_local_dir().join("cert.der")) {
|
|
||||||
// Ok(cert) => {
|
impl<R: RequestMsg> SyncRecv<R> {
|
||||||
// roots.add(CertificateDer::from(cert))?;
|
pub fn try_recv(&mut self) -> Option<Resp<R>> {
|
||||||
// }
|
match self.recv.try_recv() {
|
||||||
// Err(ref e) if e.kind() == ErrorKind::NotFound => {
|
Ok(res) => Some(res),
|
||||||
// eprintln!("local server certificate not found");
|
Err(oneshot::error::TryRecvError::Empty) => None,
|
||||||
// }
|
Err(oneshot::error::TryRecvError::Closed) => Some(Err(())),
|
||||||
// Err(e) => {
|
}
|
||||||
// eprintln!("failed to open local server certificate: {}", e);
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
// let client_crypto = quinn::rustls::ClientConfig::builder()
|
async fn connection_cert(
|
||||||
// .with_root_certificates(roots)
|
addr: SocketAddr,
|
||||||
// .with_no_client_auth();
|
cert: CertificateDer<'_>,
|
||||||
// let client_config = ClientConfig::new(Arc::new(QuicClientConfig::try_from(client_crypto)?));
|
) -> NetResult<(Endpoint, Connection)> {
|
||||||
// let mut endpoint = quinn::Endpoint::client(SocketAddr::from_str("[::]:0").unwrap())?;
|
let mut roots = quinn::rustls::RootCertStore::empty();
|
||||||
// endpoint.set_default_client_config(client_config);
|
roots
|
||||||
// endpoint
|
.add(cert)
|
||||||
// .connect(addr, SERVER_NAME)?
|
.map_err(|e| format!("Invalid Certificate: {e:?}"))?;
|
||||||
// .await
|
let client_crypto = quinn::rustls::ClientConfig::builder()
|
||||||
// .map_err(|e| format!("failed to connect: {}", e))
|
.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)> {
|
async fn connection_no_cert(addr: SocketAddr) -> NetResult<(Endpoint, Connection)> {
|
||||||
let mut endpoint = Endpoint::client(CLIENT_SOCKET).map_err(|e| e.to_string())?;
|
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))
|
Ok((endpoint, con))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
impl NetHandle {
|
||||||
async fn connect_the(handle: AppHandle, info: ConnectInfo) -> NetResult<()> {
|
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 (send, mut ui_recv) = tokio::sync::mpsc::unbounded_channel::<NetCtrlMsg>();
|
||||||
|
|
||||||
|
let cert = CertificateDer::from_slice(&info.cert);
|
||||||
let addr = info
|
let addr = info
|
||||||
.ip
|
.url
|
||||||
.to_socket_addrs()
|
.to_socket_addrs()
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.next()
|
.next()
|
||||||
.ok_or("no addresses found".to_string())?;
|
.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();
|
let conn_ = conn.clone();
|
||||||
|
|
||||||
handle.send(ClientEvent::Connect {
|
let mut req_id = RequestId::first();
|
||||||
send: NetSender { send },
|
let recv = Arc::new(ServerRecv {
|
||||||
|
msg,
|
||||||
|
requests_sync: DashMap::default(),
|
||||||
|
requests: DashMap::default(),
|
||||||
});
|
});
|
||||||
|
tokio::spawn(recv_uni(conn_, recv.clone()));
|
||||||
let recv = ServerRecv { handle };
|
tokio::spawn(async move {
|
||||||
tokio::spawn(recv_uni(conn_, recv.into()));
|
|
||||||
|
|
||||||
while let Some(msg) = ui_recv.recv().await {
|
while let Some(msg) = ui_recv.recv().await {
|
||||||
|
let request_id = req_id.next();
|
||||||
match msg {
|
match msg {
|
||||||
NetCtrlMsg::Send(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() {
|
if send_uni(&conn, msg).await.is_err() {
|
||||||
println!("disconnected from server");
|
println!("disconnected from server");
|
||||||
break;
|
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 RecvHandler<ServerMsg> for ServerRecv {
|
impl<F: AsyncFn(ServerMsg) + Sync + Send + 'static> MsgHandler for F
|
||||||
async fn msg(&self, msg: ServerMsg) {
|
where
|
||||||
self.handle.send(ClientEvent::ServerMsg(msg));
|
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 MODAL_BG: UiColor = UiColor::BLACK.brighter(0.05);
|
||||||
pub const GREEN: UiColor = UiColor::rgb(0, 150, 0);
|
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::*;
|
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 mut accounts = Span::empty(Dir::DOWN);
|
||||||
|
|
||||||
|
let accts = data.accounts();
|
||||||
|
if accts.is_empty() {
|
||||||
accounts.push(
|
accounts.push(
|
||||||
wtext("no accounts")
|
wtext("no accounts")
|
||||||
.size(20)
|
.size(20)
|
||||||
.center_text()
|
.center_text()
|
||||||
.color(Color::GRAY)
|
.color(Color::GRAY)
|
||||||
.height(60)
|
.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 Ok(resp) = con
|
||||||
let create = Button::submit("create", ui);
|
.request(Login {
|
||||||
let connect_ = connect.clone();
|
username: account.username.clone(),
|
||||||
let create_ = create.clone();
|
password: password.clone(),
|
||||||
ui.on(connect.view(), Submit, move |_| {
|
})
|
||||||
connect_.disable();
|
.await
|
||||||
create_.disable();
|
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();
|
rsc.events.register(create, Submit, move |ctx, rsc| {
|
||||||
let create_ = create.clone();
|
create_account(rsc).set_ptr(ctx.state.main_ui, rsc);
|
||||||
ui.on(create.view(), Submit, move |ctx| {
|
|
||||||
connect_.disable();
|
|
||||||
create_.disable();
|
|
||||||
create_ui(ctx.ui).set_ptr(&ctx.state.main_ui, ctx.ui);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
(
|
(
|
||||||
@@ -37,115 +120,109 @@ pub fn start_ui(client: &mut Client, ui: &mut Ui) -> WidgetRef {
|
|||||||
.span(Dir::DOWN)
|
.span(Dir::DOWN)
|
||||||
.gap(30)
|
.gap(30)
|
||||||
.modal(400)
|
.modal(400)
|
||||||
.add(ui)
|
.add(rsc)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_ui(ui: &mut Ui) -> WidgetRef {
|
pub fn create_account(rsc: &mut Rsc) -> WeakWidget {
|
||||||
wtext("hi").add(ui)
|
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 {
|
pub fn decode_hex(s: &str) -> Option<Vec<u8>> {
|
||||||
// let Client { data, proxy, .. } = client;
|
if !s.len().is_multiple_of(2) {
|
||||||
// let ip = field_widget(&data.ip, "ip", ui);
|
return None;
|
||||||
// let ip_ = ip.clone();
|
}
|
||||||
// let handle = AppHandle {
|
(0..s.len())
|
||||||
// proxy: proxy.clone(),
|
.step_by(2)
|
||||||
// window: state.window.clone(),
|
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
|
||||||
// };
|
.collect()
|
||||||
//
|
}
|
||||||
// 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)
|
|
||||||
// }
|
|
||||||
|
|||||||
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 super::*;
|
||||||
use crate::state::{ClientState, LoggedIn};
|
|
||||||
use openworm::net::{ClientMsg, NetClientMsg};
|
|
||||||
|
|
||||||
pub const SIZE: u32 = 20;
|
pub const SIZE: u32 = 20;
|
||||||
|
|
||||||
pub fn main_view(client: &mut Client, ui: &mut Ui) -> WidgetRef {
|
#[derive(PartialEq, Eq, Hash, Clone, Copy)]
|
||||||
let ClientState::LoggedIn(state) = &mut client.state else {
|
pub enum MainView {
|
||||||
panic!("we ain't logged in buh");
|
Channel,
|
||||||
};
|
Friends,
|
||||||
let msg_panel = msg_panel(ui, state);
|
Server,
|
||||||
let side_bar = rect(Color::BLACK.brighter(0.05)).width(80);
|
|
||||||
|
|
||||||
(side_bar, msg_panel).span(Dir::RIGHT).add(ui)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn msg_widget(username: &str, content: &str) -> impl WidgetRet {
|
pub fn main_view(rsc: &mut Rsc, session: Session) -> WeakWidget {
|
||||||
let content = wtext(content)
|
let mut view = WidgetSelector::new(MainView::Channel, channel::view(rsc));
|
||||||
.editable(false)
|
view.set(MainView::Server, server::view(rsc, &session));
|
||||||
.size(SIZE)
|
view.set(MainView::Friends, friends::view(rsc, &session));
|
||||||
.wrap(true)
|
let view = view.add(rsc);
|
||||||
.attr::<Selectable>(());
|
let [channel, friends, server] = tabs(
|
||||||
let header = wtext(username).size(SIZE);
|
rsc,
|
||||||
(
|
view,
|
||||||
image(include_bytes!("../assets/sungals.png"))
|
[
|
||||||
.sized((70, 70))
|
("channel", MainView::Channel),
|
||||||
.align(Align::TOP),
|
("friends", MainView::Friends),
|
||||||
(header, content)
|
("server", MainView::Server),
|
||||||
.span(Dir::DOWN)
|
],
|
||||||
.gap(10)
|
);
|
||||||
.width(rest(1))
|
let top_bar = rect(Color::BLACK.alpha(150))
|
||||||
.align(Align::TOP),
|
.height(50)
|
||||||
)
|
.foreground((channel, friends, server).span(Dir::RIGHT));
|
||||||
.span(Dir::RIGHT)
|
(top_bar, view).span(Dir::DOWN).add(rsc)
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,85 +1,79 @@
|
|||||||
|
use openworm::net::UserId;
|
||||||
|
|
||||||
|
use crate::Client;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub fn werror(ui: &mut Ui, msg: &str) -> WidgetRef {
|
pub fn werror(msg: &str, rsc: &mut Rsc) -> StrongWidget {
|
||||||
wtext(msg)
|
wtext(msg)
|
||||||
.size(20)
|
.size(20)
|
||||||
.pad(10)
|
.pad(10)
|
||||||
.background(rect(Color::RED).radius(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)
|
wtext(msg).size(20).color(Color::GRAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn field_widget(name: &str, hint_text: &str, ui: &mut Ui) -> WidgetRef<TextEdit> {
|
pub fn large_hint_text(msg: impl Into<String>) -> TextBuilder<Rsc> {
|
||||||
wtext(name)
|
wtext(msg).size(30).color(Color::GRAY)
|
||||||
.editable(true)
|
|
||||||
.size(20)
|
|
||||||
.hint(hint(hint_text))
|
|
||||||
.add(ui)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
field
|
||||||
.clone()
|
|
||||||
.pad(10)
|
.pad(10)
|
||||||
.background(rect(Color::BLACK.brighter(0.1)).radius(15))
|
.background(rect(Color::BLACK.brighter(0.1)).radius(15))
|
||||||
.attr::<Selector>(field)
|
.attr::<Selector>(field)
|
||||||
.add(ui)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Copy, WidgetView)]
|
||||||
pub struct Button {
|
pub struct Button {
|
||||||
|
#[root]
|
||||||
|
root: WeakWidget,
|
||||||
|
rect: WeakWidget<Rect>,
|
||||||
color: UiColor,
|
color: UiColor,
|
||||||
root: WidgetRef,
|
enabled: WeakState<bool>,
|
||||||
rect: WidgetRef<Rect>,
|
|
||||||
enabled: Handle<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WidgetView for Button {
|
|
||||||
fn view(&self) -> &WidgetRef<Self::Widget> {
|
|
||||||
&self.root
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Button {
|
impl Button {
|
||||||
pub fn new(text: &str, color: UiColor, ui: &mut Ui) -> Self {
|
pub fn new_fg(fg: WeakWidget, color: UiColor, rsc: &mut Rsc) -> Self {
|
||||||
let rect = rect(color).radius(15).add(ui);
|
let rect = rect(color).radius(15).add(rsc);
|
||||||
let enabled = Handle::from(true);
|
let enabled = rsc.create_state(rect, true);
|
||||||
let enabled_ = enabled.clone();
|
|
||||||
let enabled__ = enabled.clone();
|
|
||||||
let root = rect
|
let root = rect
|
||||||
.clone()
|
|
||||||
.on(
|
.on(
|
||||||
CursorSense::HoverStart | CursorSense::unclick(),
|
CursorSense::HoverStart | CursorSense::unclick(),
|
||||||
move |ctx| {
|
move |ctx, rsc: &mut Rsc| {
|
||||||
if !*enabled_.get() {
|
if !rsc[enabled] {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.widget.get_mut().color = color.brighter(0.1);
|
rsc[ctx.widget].color = color.brighter(0.1);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.on(CursorSense::HoverEnd, move |ctx| {
|
.on(CursorSense::HoverEnd, move |ctx, rsc| {
|
||||||
if !*enabled__.get() {
|
if !rsc[enabled] {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.widget.get_mut().color = color;
|
rsc[ctx.widget].color = color;
|
||||||
})
|
})
|
||||||
.height(60)
|
.height(60)
|
||||||
.foreground(wtext(text).size(25).text_align(Align::CENTER))
|
.foreground(fg)
|
||||||
.add(ui);
|
.add(rsc);
|
||||||
let root_ = root.clone();
|
rect.on(CursorSense::click(), move |ctx, rsc: &mut Rsc| {
|
||||||
let enabled_ = enabled.clone();
|
if !rsc[enabled] {
|
||||||
rect.clone()
|
|
||||||
.on(CursorSense::click(), move |ctx| {
|
|
||||||
if !*enabled_.get() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.widget.get_mut().color = color.darker(0.2);
|
rsc[ctx.widget].color = color.darker(0.2);
|
||||||
ctx.ui.run_event(ctx.state, &root_, Submit, ());
|
rsc.run_event::<Submit>(root, (), ctx.state);
|
||||||
})
|
})
|
||||||
.add(ui);
|
.add(rsc);
|
||||||
Self {
|
Self {
|
||||||
root,
|
root,
|
||||||
rect,
|
rect,
|
||||||
@@ -88,26 +82,90 @@ impl Button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn submit(text: &str, ui: &mut Ui) -> Self {
|
pub fn new(text: &str, color: UiColor, rsc: &mut Rsc) -> Self {
|
||||||
Self::new(text, color::GREEN, ui)
|
Self::new_fg(
|
||||||
|
wtext(text).size(25).text_align(Align::CENTER).add(rsc),
|
||||||
|
color,
|
||||||
|
rsc,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable(&self) {
|
pub fn submit(text: &str, rsc: &mut Rsc) -> Self {
|
||||||
*self.enabled.get_mut() = false;
|
Self::new(text, color::GREEN, rsc)
|
||||||
self.rect.get_mut().color = self.color.darker(0.8);
|
}
|
||||||
|
|
||||||
|
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! {
|
widget_trait! {
|
||||||
pub trait Stuff;
|
pub trait Stuff<Rsc: UiRsc + 'static>;
|
||||||
fn modal(self, width: impl UiNum) -> impl WidgetIdFn {
|
fn modal(self, width: impl UiNum) -> impl WidgetIdFn<Rsc> {
|
||||||
|ui| {
|
|rsc| {
|
||||||
self
|
self
|
||||||
.pad(15)
|
.pad(15)
|
||||||
.background(rect(color::MODAL_BG).radius(15))
|
.background(rect(color::MODAL_BG).radius(15))
|
||||||
.width(width)
|
.width(width)
|
||||||
.align(Align::CENTER)
|
.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 iris::prelude::*;
|
||||||
use len_fns::*;
|
|
||||||
|
|
||||||
|
mod channel;
|
||||||
|
pub mod color;
|
||||||
mod connect;
|
mod connect;
|
||||||
|
mod friends;
|
||||||
mod main;
|
mod main;
|
||||||
mod misc;
|
mod misc;
|
||||||
pub mod color;
|
mod server;
|
||||||
|
mod user;
|
||||||
|
|
||||||
pub use connect::*;
|
pub use connect::*;
|
||||||
pub use main::*;
|
pub use main::*;
|
||||||
pub use misc::*;
|
pub use misc::*;
|
||||||
|
pub use user::*;
|
||||||
event_ctx!(Client);
|
|
||||||
|
|||||||
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 db;
|
||||||
|
mod handle;
|
||||||
mod net;
|
mod net;
|
||||||
|
|
||||||
use crate::db::{Db, Msg, User, open_db};
|
use crate::{
|
||||||
|
db::{Db, ServerPerms},
|
||||||
|
handle::{ClientHandler, ClientState},
|
||||||
|
};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use net::{ClientSender, ConAccepter, listen};
|
use net::{ClientSender, ConAccepter, listen};
|
||||||
use openworm::{
|
use openworm::{
|
||||||
net::{
|
net::{AccountToken, ClientMsgInst, RecvHandler, install_crypto_provider},
|
||||||
ClientMsg, DisconnectReason, NetServerMsg, RecvHandler, ServerError, ServerMsg,
|
|
||||||
install_crypto_provider,
|
|
||||||
},
|
|
||||||
rsc::DataDir,
|
rsc::DataDir,
|
||||||
};
|
};
|
||||||
use scrypt::{
|
use rand::distr::{Alphanumeric, SampleString};
|
||||||
Scrypt,
|
|
||||||
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
|
|
||||||
};
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
sync::{
|
sync::{
|
||||||
@@ -41,24 +38,30 @@ fn main() {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn run_server(port: u16) {
|
pub async fn run_server(port: u16) {
|
||||||
let dir = DataDir::default();
|
let dir = DataDir::new(Some("server"));
|
||||||
let path = dir.get();
|
let db = Db::open(dir.path.join("db"));
|
||||||
let db: Db = open_db(path.join("server.db"));
|
|
||||||
let handler = ServerListener {
|
let handler = ServerListener {
|
||||||
senders: Default::default(),
|
senders: Default::default(),
|
||||||
count: 0.into(),
|
count: 0.into(),
|
||||||
db: db.clone(),
|
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;
|
let _ = ctrl_c().await;
|
||||||
println!("stopping server");
|
println!("stopping server");
|
||||||
println!("closing connections...");
|
println!("closing connections...");
|
||||||
endpoint.close(0u32.into(), &[]);
|
endpoint.close(0u32.into(), &[]);
|
||||||
let _ = handle.await;
|
let _ = handle.await;
|
||||||
endpoint.wait_idle().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;
|
type ClientId = u64;
|
||||||
@@ -69,14 +72,8 @@ struct ServerListener {
|
|||||||
count: AtomicU64,
|
count: AtomicU64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
|
||||||
pub enum ClientState {
|
|
||||||
Login,
|
|
||||||
Authed(u64),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConAccepter for ServerListener {
|
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);
|
let id = self.count.fetch_add(1, Ordering::Release);
|
||||||
self.senders.write().await.insert(id, send.clone());
|
self.senders.write().await.insert(id, send.clone());
|
||||||
ClientHandler {
|
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::{
|
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::{
|
use quinn::{
|
||||||
Connection, Endpoint, ServerConfig,
|
Connection, Endpoint, ServerConfig,
|
||||||
@@ -36,19 +37,12 @@ pub fn init_endpoint(port: u16, data_path: &Path) -> Endpoint {
|
|||||||
panic!("failed to read certificate: {}", e);
|
panic!("failed to read certificate: {}", e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// let server_crypto = quinn::rustls::ServerConfig::builder()
|
print!("cert hex: ");
|
||||||
// .with_no_client_auth()
|
for x in cert.iter() {
|
||||||
// .with_single_cert(vec![cert], key)
|
print!("{:02x}", x);
|
||||||
// .unwrap();
|
}
|
||||||
//
|
println!();
|
||||||
// let server_config = quinn::ServerConfig::with_crypto(Arc::new(
|
|
||||||
// QuicServerConfig::try_from(server_crypto).unwrap(),
|
|
||||||
// ));
|
|
||||||
|
|
||||||
let server_config = ServerConfig::with_single_cert(vec![cert], key).unwrap();
|
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));
|
let server_socket: SocketAddr = SocketAddr::V6(SocketAddrV6::new(SERVER_HOST, port, 0, 0));
|
||||||
quinn::Endpoint::server(server_config, server_socket).unwrap()
|
quinn::Endpoint::server(server_config, server_socket).unwrap()
|
||||||
}
|
}
|
||||||
@@ -62,17 +56,43 @@ impl ClientSender {
|
|||||||
pub fn remote(&self) -> SocketAddr {
|
pub fn remote(&self) -> SocketAddr {
|
||||||
self.conn.remote_address()
|
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 {
|
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
|
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 {
|
pub trait ConAccepter: Send + Sync + 'static {
|
||||||
fn accept(
|
fn accept(
|
||||||
&self,
|
&self,
|
||||||
send: ClientSender,
|
send: ClientSender,
|
||||||
) -> impl Future<Output = impl RecvHandler<ClientMsg>> + Send;
|
) -> impl Future<Output = impl RecvHandler<ClientMsgInst>> + Send;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn listen(
|
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 no_cert;
|
||||||
|
mod request;
|
||||||
mod transfer;
|
mod transfer;
|
||||||
|
|
||||||
|
pub use data::*;
|
||||||
|
pub use msg::*;
|
||||||
pub use no_cert::*;
|
pub use no_cert::*;
|
||||||
|
pub use request::*;
|
||||||
pub use transfer::*;
|
pub use transfer::*;
|
||||||
|
|
||||||
pub const SERVER_NAME: &str = "openworm";
|
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() {
|
pub fn install_crypto_provider() {
|
||||||
quinn::rustls::crypto::ring::default_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 std::sync::Arc;
|
||||||
|
|
||||||
use crate::net::BINCODE_CONFIG;
|
|
||||||
use quinn::Connection;
|
use quinn::Connection;
|
||||||
use tokio::io::{AsyncReadExt as _, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt as _, AsyncWriteExt};
|
||||||
use tracing::Instrument as _;
|
use tracing::Instrument as _;
|
||||||
@@ -17,8 +16,8 @@ pub trait RecvHandler<M>: Send + Sync + 'static {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub type SendResult = Result<(), ()>;
|
pub type SendResult = Result<(), ()>;
|
||||||
pub async fn send_uni<M: bincode::Encode>(conn: &Connection, msg: M) -> SendResult {
|
pub async fn send_uni<M: bitcode::Encode>(conn: &Connection, msg: M) -> SendResult {
|
||||||
let bytes = bincode::encode_to_vec(msg, BINCODE_CONFIG).unwrap();
|
let bytes = bitcode::encode(&msg);
|
||||||
let mut send = conn.open_uni().await.map_err(|_| ())?;
|
let mut send = conn.open_uni().await.map_err(|_| ())?;
|
||||||
|
|
||||||
send.write_u64(bytes.len() as u64).await.map_err(|_| ())?;
|
send.write_u64(bytes.len() as u64).await.map_err(|_| ())?;
|
||||||
@@ -34,7 +33,7 @@ pub enum DisconnectReason {
|
|||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recv_uni<M: bincode::Decode<()>>(
|
pub async fn recv_uni<M: bitcode::DecodeOwned>(
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
handler: Arc<impl RecvHandler<M>>,
|
handler: Arc<impl RecvHandler<M>>,
|
||||||
) -> DisconnectReason {
|
) -> DisconnectReason {
|
||||||
@@ -58,7 +57,7 @@ pub async fn recv_uni<M: bincode::Decode<()>>(
|
|||||||
async move {
|
async move {
|
||||||
let len = recv.read_u64().await.unwrap();
|
let len = recv.read_u64().await.unwrap();
|
||||||
let bytes = recv.read_to_end(len as usize).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;
|
handler.msg(msg).await;
|
||||||
}
|
}
|
||||||
.instrument(tracing::info_span!("request")),
|
.instrument(tracing::info_span!("request")),
|
||||||
|
|||||||
120
src/rsc.rs
120
src/rsc.rs
@@ -1,45 +1,103 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
path::Path,
|
io::Write,
|
||||||
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use directories_next::ProjectDirs;
|
use directories_next::ProjectDirs;
|
||||||
|
|
||||||
use crate::net::BINCODE_CONFIG;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DataDir {
|
pub struct DataDir {
|
||||||
dirs: ProjectDirs,
|
pub path: PathBuf,
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for DataDir {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
dirs: ProjectDirs::from("", "", "openworm").unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DataDir {
|
impl DataDir {
|
||||||
pub fn get(&self) -> &Path {
|
pub fn new(dir: Option<&str>) -> Self {
|
||||||
self.dirs.data_local_dir()
|
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